Handling side pots correctly is one of the most critical logic challenges in a poker engine, as errors here directly result in financial loss and regulatory failure. The solution requires a deterministic, multi-pass evaluation algorithm that decouples pot creation from hand evaluation.
The core principle is: A player can only win a pot they contributed to. The engine must logically segregate the total chips on the table into distinct "Pots" (Main, Side 1, Side 2, etc.) based on the minimum effective stack at each stage of the betting.
1. The Data Model: Pot Segregation
Do not store a single totalPot integer. Instead, maintain a list of Pot objects, each tracking:
-
id: Unique identifier. -
eligiblePlayers: ASetof player IDs allowed to compete for this pot. -
amount: Total chips in the pot. -
contributions: A map of{ playerId: amount }to track exactly who put in what (crucial for refunds if a player is eliminated early or for audit trails).
State Transition Logic:
When a player goes All-In, the engine must immediately check if this All-In amount is less than the current highestBet. If so, it triggers a Side Pot Creation event.
2. The Algorithm: Multi-Pass Side Pot Calculation
The most robust implementation uses a two-pass algorithm (or a single pass with deferred distribution) to ensure mathematical accuracy.
Pass 1: Determine Pot Boundaries (The "Stack Normalization" Phase)
Before dealing cards or evaluating hands, the engine must calculate exactly how many pots exist and their sizes.
- Sort Players by Stack: Get all active players and sort them by their
currentStack(chips remaining before the current betting round). - Identify Effective Stacks: Iterate through the sorted players to find "breakpoints."
- Example: Player A (100), Player B (100), Player C (50), Player D (200).
- The first "layer" is limited by the smallest stack (Player C: 50). Everyone contributes 50 to the Main Pot.
- Player C is now "All-In" for the Main Pot layer.
- Remaining stacks: A (50), B (50), D (150).
- The next "layer" is limited by the next smallest (A/B: 50). A and B contribute 50 to Side Pot 1.
- Player D contributes 50 to Side Pot 1.
- Remaining stacks: D (100).
- Side Pot 2: D contributes the remaining 100. Only D is eligible? No, D is the only one left, but usually, this implies D is raising against someone else.
- Correction: The logic is:
- Main Pot: Everyone contributes
min(stack, current_bet). - Side Pot 1: Players with
stack > min_betcontribute the difference up to the next lowest stack. - Side Pot N: Repeat until all chips are allocated.
- Main Pot: Everyone contributes
Pseudocode for Pot Calculation:
function calculateSidePots(players: Player[], currentHighestBet: number): Pot[] {
// 1. Filter active players (not folded)
const activePlayers = players.filter(p => p.status === 'active' || p.status === 'all_in');
// 2. Sort by remaining stack (chips they can still put in)
activePlayers.sort((a, b) => a.remainingStack - b.remainingStack);
const pots: Pot[] = [];
let currentLayerLimit = 0;
let layerIndex = 0;
// 3. Iterate through stack layers
for (let i = 0; i < activePlayers.length; i++) {
const player = activePlayers[i];
const amountToContribute = Math.min(player.remainingStack, currentHighestBet - currentLayerLimit);
// If the player has no more chips to contribute to this layer, skip
if (amountToContribute <= 0) continue;
// Create a new pot layer if this is a new stack tier
if (player.remainingStack > currentLayerLimit) {
const newPot = new Pot(layerIndex++);
// Add all players who have enough stack for this layer
activePlayers.forEach(p => {
if (p.remainingStack >= currentLayerLimit + amountToContribute) {
const contribution = Math.min(p.remainingStack - currentLayerLimit, amountToContribute);
newPot.addContribution(p.id, contribution);
p.remainingStack -= contribution;
}
});
currentLayerLimit += amountToContribute;
pots.push(newPot);
}
}
return pots;
}
Pass 2: Hand Evaluation & Distribution
Once pots are defined, the engine evaluates hands independently for each pot.
- Iterate Pots: Start with the Main Pot, then Side 1, etc.
- Filter Eligible Players: For
Pot X, only players inPot X.eligiblePlayersare considered. - Evaluate: Run the
HandEvaluatorfor eligible players using their hole cards and community cards. - Distribute:
- If multiple players tie for the best hand in
Pot X, split the pot. - If a player went all-in and lost, they are removed from the
eligiblePlayersset for subsequent pots.
- If multiple players tie for the best hand in
Critical Edge Case: The "Unmatched" All-In
If Player A (100) calls Player B (150) All-In, and Player B raises to 200:
- Player A puts in 100.
- Player B puts in 100 (matched) + 50 (side pot).
- Main Pot: 200 (100 each).
- Side Pot: 0 (Player A cannot contribute).
- Player B wins the Side Pot automatically (no one else can contest it), but the engine must still run the logic to confirm the Main Pot winner.
3. Architecture & Data Flow
The "Pot Manager" Microservice
In a large-scale architecture, the pot logic should be isolated in a dedicated service or a pure function within the Game Engine.
- Input:
List<Player>,CurrentHighestBet,BettingHistory. - Output:
List<Pot>with winners and amounts. - State: The
Potobjects are immutable snapshots stored in the event log.
Database Schema (PostgreSQL)
Store pot structures as JSONB or normalized tables for auditability.
CREATE TABLE game_pots (
id UUID PRIMARY KEY,
game_id UUID NOT NULL,
pot_type VARCHAR(20) NOT NULL, -- 'MAIN', 'SIDE_1', 'SIDE_2'
total_amount BIGINT NOT NULL,
eligible_players JSONB NOT NULL, -- ["player_id_1", "player_id_2"]
contributions JSONB NOT NULL, -- {"player_id_1": 100, "player_id_2": 100}
winner_ids JSONB, -- ["player_id_1"]
amount_per_winner JSONB, -- {"player_id_1": 200}
created_at TIMESTAMP DEFAULT NOW()
);
4. Scalability & Performance
- Algorithmic Complexity: The sorting and layering algorithm is $O(N \log N)$ where $N$ is the number of players. Since $N \le 10$ (usually 9 or 10 max), this is effectively $O(1)$ and executes in microseconds.
- Concurrency: Pot calculation is a read-heavy operation (triggered at showdown) but must be consistent. Since it's a pure function of the game state, it can be executed on a separate worker thread without locking the main game loop.
- Memory: Do not create new objects for every calculation if possible. Reuse object pools for
PotandContributionobjects to reduce GC pressure in high-frequency environments (e.g., 100k hands/hour).
5. Security & Compliance
- Audit Trail: Every side pot creation must be logged as an event:
SidePotCreated(potId, amount, eligiblePlayers). - Determinism: The algorithm must produce the exact same result on any server. No floating-point math; use integer arithmetic (cents/smallest currency unit) exclusively.
- Refund Logic: If a player folds after a side pot is created but before the showdown, they are simply removed from the
eligiblePlayersset. Their contribution remains in the pot. If a player goes all-in and folds before the showdown (impossible in poker, but hypothetically), the logic must handle refunds correctly. (In poker, once all-in, you are in the pot regardless of folding).
Real-World Example: Three-Way All-In
- Player A: 500 chips
- Player B: 300 chips
- Player C: 150 chips
- Current Bet: 100 (everyone calls 100).
- Action:
- C goes All-In (150).
- A and B call (150 each).
- Main Pot: 450 (150 each). Eligible: A, B, C.
- Remaining: A (350), B (150).
- B goes All-In (150 more).
- A calls (150).
- Side Pot 1: 300 (150 from A, 150 from B). Eligible: A, B. (C is ineligible).
- Remaining: A (200).
- A goes All-In (200).
- Side Pot 2: 200 (200 from A). Eligible: A only? No, A is the only one left to contribute, but B and C are already all-in.
- Correction: If A has 200 left and B/C are all-in, A cannot raise against them. A just calls.
- Scenario Change: A raises to 500. B calls 300 (all in). C calls 150 (all in).
- Main: 450 (150 each).
- Side 1: 300 (150 from A, 150 from B).
- Side 2: 200 (200 from A).
- Winners:
- Main Pot: Best hand among A, B, C.
- Side 1: Best hand among A, B.
- Side 2: A (automatic win, no one else eligible).
Implementation Checklist
- Normalize Stacks: Always work with remaining stacks, not total bets.
- Layering: Create pots from smallest stack to largest.
- Eligibility: Strictly enforce that a player cannot win a pot they didn't contribute to.
- Tie-Breaking: Handle split pots within each layer independently.
- Logging: Record every pot distribution event.
FAQs
Q1: Can a player win a side pot if they are the only one who contributed to it?
Yes. If Player A raises, Player B folds, and Player C goes all-in for less than the raise, Player A and C play for the Main Pot. If Player A has enough chips to raise again and Player B is out, any further chips A puts in form a "Side Pot" where A is the only eligible player. A wins this pot automatically. The engine must still create the pot object for audit purposes.
Q2: What happens if two players tie for the best hand in a side pot?
The side pot is split equally between the tied players. Any odd chip (if the amount is odd) is typically awarded to the player with the highest card in the hand (or by position rules defined in the specific game variant), or simply given to the player to the left of the dealer button if tie-breakers aren't specified. The engine must implement a "split pot" function that handles integer division and remainder distribution.
Q3: How does the engine handle "All-In" players who fold?
In poker, a player who goes All-In cannot fold. They remain in the hand until the showdown for any pot they contributed to. The FSM state machine must prevent a "Fold" action for any player with status == 'all_in'. The only way they leave the active betting is if they lose the pot or the hand ends.
Q4: Is it possible to have a side pot with only one player?
Yes, but only if that player is the only one eligible. This usually happens if one player raises, everyone else folds, and the raiser has chips left over (which is impossible in a standard hand unless it's a specific tournament rule or a multi-way all-in scenario where the side pot is created by the remaining stack of the aggressor against no one else). In a standard 3-way all-in, Side Pot 2 might only have Player A if B and C are all-in and A raises further, and B/C cannot call. Wait, if B and C are all-in, they can't call. So A's extra chips go into a pot where only A is eligible. A wins it.
Q5: How do you handle the "Dead Button" or special tournament rules with side pots?
The side pot logic is independent of button position. The button determines the order of action, but the side pot calculation is purely mathematical based on stack sizes. The "Dead Button" (where a player sits out but chips remain) is handled by ensuring the eligiblePlayers set only includes players with status != 'folded' and status != 'sitting_out' (if sitting out means they can't win). The engine must respect the specific tournament rules regarding "sitting out" players in the eligiblePlayers filter.
Top comments (0)