Skip to content

2026-07-23 — Engine showdown soundness: dead hands never win, chips are never destroyed

A randomized conservation soak found the poker engine's showdown could name a folded player as a pot winner and silently destroy the chips (_players[seat]?.addToStack on a nulled seat — 5,517 of 6,185 chips vanished in the reproducer hand). A deeper investigation verified a second, conserved flavor invisible to conservation checks: an all-in player was stripped from the eligibility of the pot they funded whenever betting continued behind them, so the pot paid the wrong live player every time.

Fixed in the engine as a pot-eligibility subsystem (packages/game-logic/src/poker/):

  • first-class fold record (Dealer._folded, serialized) — a null seat can never distinguish a fold from an all-in again;
  • eligibility is maintenance, not rewrite: folds sweep every pot; an all-in-capped pot closes and later-street bets open a new pot (proper cross-street side pots);
  • showdown contestants = stamped eligibility ∩ (dealt-in ∧ ¬folded), paid through stable dealt-in references; an unpayable pot throws a typed ShowdownIntegrityError before any pot pays, leaving the hand in progress for the felt-liveness watchdog's loud freeze;
  • restore identity: fromJson now wires ONE Player object set across Engine/Dealer/BettingRound — a restored mid-hand engine used to throw on the first continued action (a real pre-existing mid-hand-resume bug, found and fixed here);
  • the soak is now a permanent two-property test: conservation (catches destruction) + winner-membership (catches misdirection) per hand across 2×2,500 seeded hands.

Evidence pass: zero D1-shaped occurrences in staging history; the 216 negative-delta anomaly envelopes are all fold-win OHH recording artifacts (separate finding).

See Engine Showdown Soundness.