A multiplayer poker server cannot settle a hand by adding every contribution together and paying the player with the strongest cards. A player who committed 100 chips cannot win a contest funded by another player’s additional 400 chips.
There is another complication: folding removes a player’s right to win matched chips, but those chips remain in the pot.
We build pokr, a browser poker app for private games with virtual chips. A useful way to understand this accounting problem is to separate three questions:
- Who contributed to this layer of chips?
- Who can win it?
- Was any part never matched by another player?
Here is a small TypeScript model that keeps those questions separate.
Work with cumulative contributions
Use each player’s contribution across the entire hand, not just their bet on the current street. Keep the folded flag separately.
For this example, imagine the following settlement snapshot, before returning any final unmatched excess:
| Player | Total committed | State |
|---|---|---|
| A | 100 | All-in |
| B | 300 | All-in |
| C | 500 | Live |
| D | 200 | Folded |
D contributed earlier and then folded to later action while still having chips behind. A and B, who are all-in, are not subsequently folding to another bet.
The total committed is 1,100 chips. That is our first accounting constraint, but it does not tell us who should receive them.
Slice at every contribution level
Sort the distinct positive contributions: 100, 200, 300, 500.
For each level, multiply the distance from the previous level by the number of players who reached it:
layer amount = (current level − previous level) × contributor count
This gives:
| Contribution interval | Contributors | Amount | Eligible players |
|---|---|---|---|
| 0–100 | A, B, C, D | 400 | A, B, C |
| 100–200 | B, C, D | 300 | B, C |
| 200–300 | B, C | 200 | B, C |
| 300–500 | C | 200 | Refund to C |
D’s chips increase the first two amounts. D never appears in their eligibility lists.
The middle two layers have identical eligible players, so a presentation layer can combine them into one 500-chip side pot. Keeping them separate initially makes the arithmetic easier to inspect.
The final layer has only one contributor. Those 200 chips were never matched: they are a refund, independent of C’s hand strength.
A small implementation
This is standalone teaching code, not the complete pokr engine. It constructs settlement layers; it does not evaluate cards or move balances.
type Contribution = {
id: string;
committed: number;
folded: boolean;
};
type Layer =
| { kind: "pot"; amount: number; eligible: string[] }
| { kind: "refund"; amount: number; to: string };
function splitContributions(
players: readonly Contribution[],
): Layer[] {
const total = players.reduce((n, p) => n + p.committed, 0);
if (
new Set(players.map(p => p.id)).size !== players.length ||
players.some(p =>
!Number.isSafeInteger(p.committed) || p.committed < 0
) ||
!Number.isSafeInteger(total)
) {
throw new Error("Use unique IDs and safe integer chip totals");
}
const levels = [...new Set(
players.filter(p => p.committed > 0).map(p => p.committed),
)].sort((a, b) => a - b);
const layers: Layer[] = [];
let previous = 0;
for (const level of levels) {
const contributors = players.filter(p => p.committed >= level);
const amount = (level - previous) * contributors.length;
previous = level;
if (contributors.length === 1) {
layers.push({
kind: "refund",
amount,
to: contributors[0].id,
});
continue;
}
const eligible = contributors
.filter(p => !p.folded)
.map(p => p.id);
if (eligible.length === 0) {
throw new Error("Matched chips require hand-state resolution");
}
layers.push({ kind: "pot", amount, eligible });
}
return layers;
}
Notice the order: identify contributors first, then filter eligibility.
Filtering folded players out of the input would erase their committed chips. Treating every remaining layer as a pot would misclassify an unmatched bet as winnings.
The explicit error also matters. If every contributor to a matched layer has folded, contribution totals alone are insufficient for this simplified routine. A complete engine needs the hand-ending state and its documented rules for handling abandoned pots, forced folds, and interruptions. Silently discarding the layer is not a settlement policy.
Settle each pot independently
Suppose A has the strongest hand, followed by C, then B.
A receives the 400-chip main pot. A is absent from the other eligibility lists, so the evaluator compares only B and C for those layers. C receives their combined 500 chips and the separate 200-chip refund.
The result is:
A: 400
B: 0
C: 700
D: 0
Total: 1,100
These are gross credits, not profit. C committed 500 and receives 700, for a net change of +200.
Keep refunds distinguishable from winnings in settlement records. Otherwise, statistics or animations can report a returned bet as a victory.
For tied hands, split each pot independently and apply an explicit, deterministic odd-chip rule. Do not let incidental array order decide it.
Test ownership as well as conservation
Two useful accounting checks are:
sum(layer amounts) = sum(committed chips)
sum(winnings) + sum(refunds) = sum(committed chips)
Both can pass while chips go to the wrong player. Test exact recipients too:
- Equal contributions produce a shared pot with no refund.
- A short stack with the best hand wins only eligible layers.
- A folded contributor increases pot amounts but cannot win them.
- An unmatched excess returns even when its owner loses.
- Tied winners receive the full pot after odd-chip allocation.
- Reordering input players preserves pot eligibility and amounts.
- Empty or zero-contribution inputs produce no layers.
- Unresolved matched layers fail explicitly.
For a visual companion, our interactive guide to all-ins and side pots lets you change contributions and inspect the resulting pots.
The useful abstraction is a series of contribution layers, each carrying its own ownership rules. Once those rules are explicit, pot totals become something you can explain, inspect, and test.
Top comments (0)