Game Loop

Executive Summary

This document describes the production gameplay loop.

The supported contract is the combination of:

  • table lifecycle and state transitions in src/game/table/
  • event delivery and reconciliation in src/websocket/, packages/shared/src/, and client/src/hooks/useGameSocket.ts
  • Table V2 rendering and animation behavior in client/src/pages/game/V2PokerTable.tsx and client/src/components/game/v2/
  • deterministic scenarios in npm run gameplay:verify

Known future work is listed separately at the end of this document. It is not part of the supported runtime contract.

Core Modules

AreaFilesRuntime role
Table lifecyclesrc/game/table/table.tsOwns table configuration, seating, hand start, forced posts, variant selection, action timers, and next-hand scheduling.
Action and round flowsrc/game/table/table-actions.tsValidates actions, advances betting rounds and streets, runs showdowns, distributes chips, applies rake, and completes hands.
State serializationsrc/game/table/table-state.tsProduces personalized table snapshots for seated players, spectators, and debug/admin views.
Player lifecyclesrc/game/table/table-players.tsHandles join, leave, sit out, sit back in, pending rebuys, and forced leave/fold behavior.
Variant progressionsrc/game/flow/variant-state-config.ts, src/game/flow/mixed-game-engine.tsMaps runtime street progression and, for Mixed Game, rotates variants only at safe hand boundaries.
Turn and betting ordersrc/game/flow/turn-manager.ts, src/game/flow/stud-turn-order.tsTracks current actor, acted players, raise reopening, and Stud door-card order.
Showdownsrc/game/flow/showdown.tsResolves standard and Hi-Lo showdowns, side pots, tied lows, and odd-chip assignment.
WebSocket bridgesrc/websocket/table-manager.ts, src/websocket/broadcast.ts, src/websocket/pipeline/Turns table events into public broadcasts, private guaranteed-delivery prompts, personalized snapshots, and sequence-aware resyncs.
Shared client reconciliationpackages/shared/src/hooks/, packages/shared/src/stores/gameStore.tsApplies sequence tracking, stale-event rejection, hand transitions, and mixed-game metadata consistently across clients.
Web clientclient/src/hooks/useGameSocket.ts, client/src/store/Reconciles table_state, public events, private prompts, reconnect snapshots, and animation state into Table V2.
Table V2 UIclient/src/pages/game/V2PokerTable.tsx, client/src/components/game/v2/Renders board/no-board variants, Stud visibility, Hi-Lo breakdowns, mixed-game labels, and winner/forced-bet/chip animations.

Table Lifecycle

Table.startHand() starts only from WAITING or COMPLETE, requires at least two eligible players, and refuses to start while the table is paused, closed, awaiting settlement, or held by a tournament hand-for-hand gate.

Current hand-start sequence:

  1. Clear any pending auto-start timer.
  2. Apply any pending Mixed Game rotation before the next hand is initialized.
  3. Reset per-hand state through TableStateManager.resetHandState().
  4. Increment handNumber and transition to STARTING.
  5. Rotate the dealer button after the first hand and calculate actor/forced-post positions for the active variant.
  6. Emit hand_started and run the game:hand:started plugin hook.
  7. Post forced bets and deal, in the order the variant requires:
    • Hold’em and Omaha: optional antes plus small blind and big blind, then hole cards.
    • Short Deck: antes for all eligible players plus a button blind, then hole cards. The first player after the button acts first.
    • Stud and Stud Hi-Lo: third street is dealt first, because the bring-in depends on the door cards. The lowest door card posts the bring-in, with ties broken by suit (clubs lowest, then diamonds, hearts, spades). Voluntary action continues clockwise from the bring-in, with fixed-limit completion.
  8. Emit canonical forced_bets data. Blind games also emit legacy blinds_posted.
  9. Emit private hole_cards payloads with hand context for seated players.
  10. Transition into the first betting round for the active variant.
  11. Start the action timer for the current actor.
  12. Emit betting_started and personalized table_state snapshots.

Hand completion is symmetric:

  1. Finish the street, or end early when only one player remains. That player wins without a showdown and any uncalled bet is returned first.
  2. If at most one player can still act and others are all-in, run out the remaining streets automatically. Contested all-in hands are revealed in a showdown event with revealTiming: 'all_in_before_runout' before the runout cards are dealt.
  3. Run the standard or Hi-Lo showdown when more than one live player reaches it, and emit showdown with best-hand and Hi-Lo context.
  4. Apply rake once, to the contested total. Uncalled returns are paid back at face value without rake.
  5. Distribute winnings and odd chips.
  6. Emit hand_complete. The WebSocket layer broadcasts hand_result to clients.
  7. Record Mixed Game progress, then apply pending config changes and rebuys.
  8. Schedule the next hand.

For Mixed Game tables, variant changes are deferred until hand boundaries. The next hand starts with the rotated game configuration already applied, including betting structure, card/dealing behavior, and table-state metadata.

Street Progression

Board Variants

Hold’em, Omaha, Omaha Hi-Lo, and Short Deck use the standard board progression:

preflop -> flop -> turn -> river -> showdown -> complete

At each round boundary:

  1. Round bets are snapshotted for collect-to-pot animation.
  2. Acted flags and per-round bet state reset.
  3. The next community-card street is dealt.
  4. community_cards is emitted with the new cards and cumulative allCards.
  5. betting_started is emitted with the next actor, current bet, pot, street, and bettingRound.

Stud Variants

Stud and Stud Hi-Lo use explicit runtime street semantics:

third -> fourth -> fifth -> sixth -> seventh -> showdown -> complete

Internally, the compatibility state machine still maps Stud streets onto base GameState values. The supported external contract no longer relies on clients inferring that mapping. Hand-critical public and private payloads carry explicit street and bettingRound fields, so Table V2 and reconnect logic can treat Stud streets as first-class runtime data.

Current Stud behavior:

  • third street deals two down and one up card
  • fourth through sixth streets deal one up card each
  • seventh street deals one down card
  • bring-in starts on third street, later streets act high-hand-first from exposed cards
  • no community-card area is rendered in Table V2 during live Stud hands
  • if eight players reach seventh street, the deck cannot cover eight more cards, so a single face-up community card is shared by all players as their seventh card

Event Contract

Public Gameplay Events

The current production contract is:

  • table_state: authoritative personalized snapshot for seated players and spectator-safe snapshot for watchers
  • forced_bets: canonical forced-post event for live chip movement and forced-post labels
  • betting_started: current betting round context
  • community_cards: board reveal events for board variants
  • player_acted: validated player actions and stack contribution changes
  • pot_update: running pot and side-pot changes
  • showdown: revealed/mucked cards plus high and low breakdowns when applicable
  • hand_result: final winners, payout amounts, and Hi-Lo result context

Every hand-critical public event is emitted with table context:

  • tableId
  • handNumber
  • gameType
  • street
  • bettingRound

Sequenced public events are enriched with _seq and _ts. Critical public events run through the guaranteed-delivery path before Table V2 reduces them.

forced_bets is the canonical forced-post contract. Current supported runtime usage is:

  • Hold’em and Omaha: small_blind, big_blind, and optional ante
  • Short Deck: ante and button_blind
  • Stud and Stud Hi-Lo: bring_in

blinds_posted still exists only as a guarded legacy fallback. New runtime or UI work should not depend on it.

Private Hand-Critical Events

The supported private events are:

  • hole_cards
  • action_required

Both include the same table context as public hand-critical events:

  • tableId
  • handNumber
  • gameType
  • street
  • bettingRound

They are delivered through the user-targeted guaranteed-delivery path so reconnect and stale-event handling share the same context model as public events.

Resync Contract

request_table_state is the recovery entry point for seated-player reconnects and stale-sequence repair:

  1. The server responds with the current personalized table_state without inventing a new gameplay sequence.
  2. Shared sequence helpers seed the current sequence from that response.
  3. The server resends the current action_required prompt when the requester is still the current actor.
  4. The client rejects stale hole_cards and action_required payloads by hand number and street.

Client Reconciliation Model

client/src/hooks/useGameSocket.ts is the runtime bridge between WebSocket events and Table V2. The supported reconciliation model is:

  1. Apply table_state as the authoritative snapshot.
  2. Preserve hero-private cards when later public snapshots omit them.
  3. Apply sequenced public events in order through shared sequence helpers.
  4. Reject duplicate or stale events by sequence, hand number, street, and betting round.
  5. Preserve showdown cards and winner state through the hand-transition window.
  6. Queue chip animations from forced_bets, player_acted, collect, and payout events.
  7. Defer next-hand state until the winner-cleanup window finishes.
  8. Restore state and the current action prompt after reconnect via request_table_state.

Table V2 depends on that model for:

  • board vs Stud layout switching
  • Stud face-up and face-down card visibility
  • Short Deck ante/button-blind chip movement without compatibility SB/BB badges
  • Hi-Lo showdown overlays for split, scoop, no-low, and tied-low outcomes
  • Mixed Game active/next game labels and clean variant-to-variant resets
  • reduced-motion and animation-disabled behavior without losing readable state

Production Variant Baseline

Game typeSupported runtime behavior
TEXAS_HOLDEM52-card deck, 2 private hole cards, blinds, standard preflop/flop/turn/river progression, standard showdown, side pots, rake with no-flop-no-drop by default, sequenced reconnect-safe client state, and Table V2 board/chip/winner rendering.
OMAHA4 private hole cards, board runout, exactly 2 hole plus 3 board evaluation, pot-limit default betting, and Table V2 private-card rendering.
OMAHA_55 private hole cards, board runout, exactly 2 hole plus 3 board evaluation, pot-limit default, 8-seat cap for 52-card deck capacity, and Table V2 five-card private-card layout.
OMAHA_HI_LOOmaha high-hand rules plus 8-or-better low qualification, split/scoop/no-low/tied-low showdown payload parity, and Table V2 Hi-Lo presentation.
SHORT_DECK36-card deck with ranks 2-5 removed, ante-for-all plus button blind forced posts, repo-default Short Deck ranking where flush beats full house, no SB/BB compatibility badges, and normalized forced-bet animations.
STUD_7_CARDThird street starts with two down and one up card, bring-in by lowest door card with suit tie-break, completion/fixed-limit street progression through seventh street, per-card visibility in table state, no board area, and Table V2 BI/up-card/down-card rendering.
STUD_HI_LOStud runtime behavior plus 8-or-better low qualification, split/scoop/no-low/tied-low payload parity, and Hi-Lo Table V2 display.
MIXED_GAMEOrdered rotation across TEXAS_HOLDEM, OMAHA, OMAHA_5, OMAHA_HI_LO, SHORT_DECK, STUD_7_CARD and STUD_HI_LO, changing after a configured handsPerVariant and only at hand boundaries, persisted current/next-game state, per-variant betting/dealing/card-count/seat-cap overrides (8 seats for OMAHA_5 and Stud components, otherwise 10), reconnect-safe mixedGame snapshots, and Table V2 active/next game display with clean board/Stud layout resets.

The deterministic verification command is npm run gameplay:verify.

Local Runtime Verification

For gameplay or Table V2 work, the expected local sequence is:

./scripts/game.sh start
./scripts/game.sh status
curl -sS http://localhost:3000/health/live
curl -sS http://localhost:3000/health
npm run gameplay:verify
./scripts/game.sh stop

With the game running, check visual and table behaviour in a browser at http://localhost:5173.

Known Future Work

The following items are not required to treat the current runtime as supported:

  • broader browser automation beyond the current deterministic suite
  • additional mixed rotations or game families beyond the supported component variants already in the runtime
  • future UI redesign work for Table V2
  • operational scaling work such as Redis-backed multiplayer fan-out, larger load testing, and capacity tuning
Try the live demo Pricing

Have questions about Poker Engine? We reply fast.