The felt never saw a showdown, and a fold-out pot was counted twice

FixMediaService
Shipped
September 8, 2026 at 5:25 PM UTC
Author
Kamo
Commit
3dd1b79

Two defects that were invisible for the same reason, and the data a poker table needs to present a result at all. THE SHOWDOWN NOBODY COULD WATCH. finishHand() clears table.currentHandId and then, twenty-two lines later, publishes HAND_ENDED — and viewFor() derives the whole hand from that field. So every frame announcing a result carried hand == null: no board, no pot, no revealed cards, no winner, no hand name. Ten people watched the river go in and then watched the felt go blank, and the deliberate BETWEEN_HANDS_SECONDS gap that scheduleDealIfReady exists to create — "so the felt has a moment to show who won" — was a gap in which there was nothing to show. viewFor now falls back to the most recent COMPLETE hand for RESULT_LINGER_SECONDS after it ended: a view-time fallback, no column, no DDL, and a table nobody comes back to settles into "waiting" rather than into an hour-old result. A POT COUNTED TWICE. advanceStreet() sweeps streetCommitted into the pot at the end of every street, and the fold-out path does not go through it — advance() calls finishHand() directly. So those rows kept the last street's bets while hand.pot had already been set to a total that included them, and handView returned pot + the sum of streetCommitted. Preflop 100/200, a raise to 600, both blinds fold: a 900 pot reported as 1,800. It was invisible for exactly as long as the finished hand was invisible, which is why it belongs in this commit — the linger is what puts it on screen, in 30px type. finishHand now sweeps the rows, and the reader treats a complete hand as settled regardless of what wrote it. WHAT THE FELT NEEDED. Three additions, all recomputed from the rows the hand was paid from rather than stored, so none of them can drift from the stacks on screen: pot.winnerSeats per-pot winners, from PotSolver's own potWinners(). A side pot can go to somebody the main pot did not, and a felt that draws one total going to one player is drawing a hand that was not played. Empty mid-hand: every contesting seat is scored zero then, so the honest answer is not "everybody". seat.bestFive which five of the seven actually made a shown hand, from a new HandEvaluator.bestFive() — the same 21-subset search evaluate7 already does, so the five returned are by construction the five that produced the score. Reconstructing them from the packed rank would have to re-derive which ace was the kicker, and any disagreement lights a card that did not win. seat.returnedThisHand an uncalled bet, handed straight back. Not winnings and not a share of a pot — nobody contested it, so it never entered one — and it is why a stack can rise by more than wonThisHand. And showdownRankKey plus showdownRankValues, so the hand's name crosses the wire as a key rather than as English words. describeKey() has existed since this feature shipped and nothing called it; the felt is about to put the hand's name in front of every player at the loudest moment of the hand, and shipping that in English to twenty other languages would be making an old defect much louder. viewFor computes ONE PotSolver.Settlement per frame and hands it to both the seats and the pots, rather than solving the same hand twice per recipient. Tests: HandEvaluatorBestFiveTest proves evaluate5(bestFive(c)) == evaluate7(c) and that the five returned are a subset of the seven, exhaustively over every seven-card hand three reduced decks can make (31,824 each, chosen so flushes, wheels and quads all occur) plus 100,000 full-deck hands from a fixed seed, and pins the hands where a wrong five still scores right — two trips, six to a flush, quads with two candidate kickers. HoldemTableViewTest gains the finished hand, the loser's explanation, per-pot winners, the stale result ageing out, the live hand giving nothing away, both halves of the double count, and the uncalled bet.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing