## Context The current PokeR app implements Texas Hold'em poker but contains 10 critical bugs in its game flow logic. The core issues stem from three architectural problems: (1) ambiguous `currentBet` semantics that conflate "player contribution" with "bet matched," (2) lack of last-aggressor tracking for betting round completion, and (3) no side pot support for all-in scenarios. The hand evaluator and card systems are solid; the game orchestration layer needs complete reconstruction. ## Goals / Non-Goals **Goals:** - Implement correct Texas Hold'em betting mechanics per official rules - Fix all identified bugs without introducing new ones - Support side pots for multi-player all-in scenarios - Prevent race conditions between human input and AI turns - Maintain existing Svelte 5 runes-based architecture and component structure **Non-Goals:** - Adding new game variants (Omaha, Stud, etc.) - Implementing GTO or strategic AI improvements - Multiplayer networking - Educational features (pot odds, hand ranges) - UI/visual redesign ## Decisions ### Decision 1: Separate `betMatched` flag from `currentBet` **Current:** `PlayerSeat.currentBet` serves dual purpose — tracks actual chips contributed AND whether player has matched the current bet level. This causes `applyCheck` to corrupt state by setting `currentBet = state.currentBet`. **Decision:** Add explicit `betMatched: boolean` field to `PlayerSeat`. `currentBet` becomes purely "chips contributed this round." Check simply sets `betMatched = true` without modifying chip counts. ``` PlayerSeat { // ... existing fields currentBet: number // actual chips contributed this round betMatched: boolean // has player matched or checked this round? } ``` **Rationale:** Eliminates the fundamental semantic confusion that causes cascading bugs. Call amount becomes `state.currentBet - player.currentBet` with no corruption from check actions. ### Decision 2: Last-aggressor betting round completion **Current:** `completeBettingRound` checks if all active players have `currentBet >= currentMaxBet`. This fails when all-in players are included and doesn't handle the "return to last aggressor" rule. **Decision:** Track `lastAggressorIndex` in GameState. A betting round ends when: - Action returns to `lastAggressorIndex` AND that player has matched, OR - All active (non-all-in) players have `betMatched = true`, OR - Only one active player remains ``` // Round completion logic function isRoundComplete(state): boolean { const activePlayers = state.players.filter(p => p.status === 'active'); // Single active player — round over if (activePlayers.length <= 1) return true; // No raises occurred — everyone checked/called if (state.lastAggressorIndex === -1) { return activePlayers.every(p => p.betMatched); } // Raises occurred — must return to last aggressor who matches const lastAgg = state.players[state.lastAggressorIndex]; return lastAgg.status === 'active' && state.currentTurn === state.lastAggressorIndex && lastAgg.betMatched; } ``` **Rationale:** Texas Hold'em rules require action to return to the last aggressor. This is the standard implementation pattern used by poker software. ### Decision 3: Side pot algorithm **Current:** No side pot support. All chips go into a single pot, causing incorrect distributions when players go all-in at different levels. **Decision:** Implement standard side pot calculation: 1. Sort all-in players by their bet amount (ascending) 2. Create pots from bottom up — each pot contains chips above the next lower all-in level 3. Track which players are eligible for each pot (active or all-in at that level or above) ``` SidePot { amount: number eligiblePlayerIds: string[] } // Algorithm sketch: function calculatePots(players, currentPot): { mainPot: number, sidePots: SidePot[] } // Sort players by bet amount // Create nested pots from smallest all-in to largest // Main pot = chips matched by all remaining active/all-in players ``` **Rationale:** Standard poker software approach. Keeps implementation clean and matches real-world rules exactly. ### Decision 4: Unified game state machine in `+page.svelte` **Current:** `processGame()` has complex branching logic with async AI turns, state discarding, and multiple exit paths. Race conditions occur when human actions interrupt AI chains. **Decision:** Replace with a clear state machine pattern: - `aiActing: boolean` flag disables controls during AI turns - Single `processTurn()` function handles all post-action processing - State transitions are always applied atomically (`gameState = newState`) - AI turns use sequential promise chains, not nested timeouts ``` function processTurn() { // 1. Check for early win (single active player) // 2. Check if betting round is complete // 3. If complete: advance to next stage or showdown // 4. If not complete: check if next player is AI, schedule turn } // In handleAction: if (aiActing) return; // Guard against race conditions gameState = applyAction(gameState, action); processTurn(); ``` **Rationale:** Eliminates race conditions and state discarding bugs. Single source of truth for game flow logic. ### Decision 5: Validation enforcement **Current:** `validateAction` exists but is never called by `applyRaise`, `applyFold`, etc. The AI and human actions bypass validation entirely. **Decision:** All action functions call `validateAction` internally and return unchanged state on invalid actions. The UI layer also validates before rendering buttons. ``` function applyRaise(state, playerId, amount) { const validation = validateAction(playerId, 'raise', amount, state); if (!validation.valid) return state; // ... proceed with raise logic } ``` **Rationale:** Defense in depth — both the action layer and UI layer enforce rules. Prevents illegal states from being created. ## Risks / Trade-offs [Side pot complexity] → Side pots add significant complexity to showdown logic. Mitigation: Thoroughly test with edge cases (2 all-ins, 3 all-ins, all-in on flop/turn/river). [State expansion] → Adding `betMatched`, `lastAggressorIndex`, and `sidePots` to GameState increases mutation surface. Mitigation: Use immutable state updates consistently, reset all fields in `startNewHand`. [AI timing changes] → Switching from nested timeouts to promise chains changes AI behavior timing. Mitigation: Keep 400ms delay per AI action for consistent feel. [Performance] → Side pot calculation at showdown could be expensive with many players. Mitigation: 6-max table limits complexity; algorithm is O(n log n) with n ≤ 6. [currentTurn deadlock] → `completeBettingRound` originally set `currentTurn = dealerPosition + 1`, which could land on a folded/all-in player, causing an infinite loop in the game state machine. Fixed by using `findFirstActivePlayer()` to always point to an active player. ## Open Questions - Should we implement burn cards (standard in real poker, currently not implemented)? - How should all-in raises that don't constitute a full raise be handled (half-pot rule)? - Should the BB option (BB can check pre-flop if no raise) be explicitly documented in the UI?