Files
jheapsandClaude Opus 5 cd57c16050 Let the table tell the dealer who won
An option — off by default, and off means the tool is exactly what it was.

Turn it on and the community cards go on the felt. Each street asks for the cards as
it is dealt: the picker opens on the flop the moment the flop button is pressed, and
again on the turn and the river, because the moment a dealer is holding those cards is
the only moment they know what they are without turning the board round. Three taps,
then one, then one. The picker is the same sheet as the hole cards, which now knows
about the middle of the table too — the seven of clubs cannot be on the flop and in
somebody's hand, and until now it could.

Turning the players' cards over is unchanged. That already worked the way it should.

What is new is what happens next. Resolve Pot stops asking who won and says so:

    Alexandra wins $50 with three of a kind, nines.

with every hand at the table read out under its player's name, the winner already
ticked, and the button reading "Pay Alexandra". A chop comes out as "Christopher and
Mike from work split it, $22.50 each, with a pair of aces", both ticked. The dealer
still confirms — the tool reads the cards, a person still says the pot is good, and a
hand entered wrong is corrected by unticking rather than by starting over.

It declines rather than guesses. A board that isn't five cards, a player who hasn't
turned over, the same card entered in two places: each says which of those it is and
hands the question back, and the wizard is the one it always was. Who is *ahead* and
who *won* are different claims and only the second one can pay somebody.

Side pots are read per round, not once. The best hand takes the main pot and is then
out of the question; the side pot goes to the best of who is left, which is a different
answer and the one the tool has to ask for again.

handRank.js is the only module here that can pay the wrong person without anybody
noticing, so it is built to be checkable rather than clever: rank five cards, then take
the best of the twenty-one five-card hands in seven. Beside the written cases — every
category against every other, the wheel and the steel wheel, three kickers deep, the
counterfeited two pair, the board that plays — it is checked against a second evaluator
written a different way, on twenty thousand random seven-card hands and four thousand
random pairs of them. An ace that keeps its value in the wheel, a dropped two-pair
kicker and a flush read only by its top card were each introduced on purpose and each
caught.

The board takes the middle of the felt, sized from the room the seat ring actually
leaves — measured against every seat rather than the ring's widest point, which is only
the answer for a seat sitting exactly on the horizontal axis and had two pucks on top
of the board while claiming 77px of clearance either side. Where there is no middle to
take, it moves to a strip above the table: a phone on its side has a felt 528x83 with
55px pucks, so the ring closes over the centre entirely and a board in it is a board
with players painted on top.

That strip found an older bug. It costs the felt height, and a felt that changes size
had only ever been re-measured, never laid out again: the seats follow it on their own,
being placed in percentages, but a bet chip anchored inside a seat is placed in pixels
off that seat's centre. Measured at 640x360, the chips were positioned for a felt 83px
tall and painted on one of 60 — seat 3's $5 came to rest on seat 4's puck, and two
pairs of seat pucks were overlapping. The bar growing mid-hand does the same thing: a
side pot appearing is 55px off the felt. Both fixed.

The sweep grew a third table — the same five players in the same nine chairs as the
second, with the board on, so the only thing that differs between those two rows is the
board itself. It earned its place on the first run.

728 unit tests. 216 audits: what is left is the seat ring hanging past the rail on a
landscape phone, the known residual, plus five or six pixels more of it on the layouts
where the strip takes the felt's height, and a blind badge over a stack on a packed
twelve-seat portrait table.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-04 11:18:33 -06:00
..
2026-09-01 12:34:53 -06:00
2026-09-04 11:18:33 -06:00
2026-09-01 12:52:56 -06:00

Hold'em dealer tests

Unit tests for the money logic behind /Casino/TexasHoldem — the betting engine, the side-pot calculator, the split calculator and the table state.

node --test tests/holdem/*.test.js

No install step, no package.json, no dependencies. The browser modules are plain script-tag files that each end with a module.exports guard, so Node can require them as-is; load.js puts them on globalThis under the same names they use to find each other in the page, in the load order from TexasHoldem.cshtml.

What's covered

File Covers
potCalculator.test.js Main and side pots, dead money, uncalled bets, odd-cent distribution
betting.test.js Blinds, street completion, raises reopening the action, all-in run-outs
state.test.js Button rotation, which seats post blinds, session standings
splitCalculator.test.js Chopped pots, odd chips, unmakeable denominations
handRank.test.js What beats what: categories, the wheel, kickers, best five of seven
readingTheShowdown.test.js The board, the picker that fills it, and who wins a side pot
invariants.test.js 1000 randomised hands per property (see below)

The layout audit

The unit tests above cover the money. uiAudit.js covers the thing you can only see in a browser: whether a dealer can actually reach every control.

node tests/holdem/runUiAudit.js          # needs Playwright and the site running

It checks WCAG AA contrast, touch target sizes, touch-action and tap highlights, that fields stay selectable and are large enough not to trigger iOS zoom, that nothing important is covered or off-screen with no way to scroll to it, that no grid column has collapsed to nothing, that no text is painted outside its box, that no seat covers another seat's writing, and that no bet chip is hidden behind a seat or another chip. It runs over the table, all five overlays and Quick Split, at thirteen viewports from a 360px phone to a 1600px desktop and the same phones on their side — each viewport twice, once with a mouse and once with a thumb, and at three table shapes, mid-hand: the sweep posts blinds, deals and calls three times before it measures anything, so the bets layer has chips on it. It used to audit a table nobody had ever bet at, which is a third of what is on the felt during a hand left unlooked at.

"Covered" and "out of view" are told apart before anything is hit-tested. An element clipped away by a panel that scrolls is not covered by whatever happens to be painted at that height, and reporting it that way is a diagnosis of something that isn't happening: the pot wizard's third winner, below the fold in a list, came back for four viewports running as "fully covered by button Confirm Winner(s)".

More than one table shape is also newer than it sounds. The sweep ran on one — five players at a nine-handed table — for its whole life, and how much room the felt has depends on how many people are sitting down far more than on anything a viewport can report. A full table is where the seats crowd and it had never been looked at: the first sweep that did found one player's name or stack painted under another on eighteen of the configurations, on every phone in the list.

The third shape is the same five players with the board turned on and five cards on it. It is deliberately the same roster in the same chairs as the second, so the only thing that differs between the two rows is the board — a seventh player would have told us that a seven-handed table crowds, which is true, already known, and not what that row is there to find out. It earned its place immediately: the board takes the middle of the felt, or a strip above it where the middle is too small, and the strip costs the felt height. The seats follow a felt that changes size on their own, being placed in percentages of it; the chips anchored inside them do not, being placed in pixels. They were laid out for a felt 23px taller than the one they were painted on.

That last part, and two of those checks, are newer than this paragraph claiming them. runAll() did not include the contrast check at all, and touch targets sat behind an opt-in flag nothing passed, so "the audit is clean" meant nine checks rather than eleven for a long run of rounds. Both were written and tested and simply never called. The mouse/thumb pair matters for the same reason: the 44px rule stands itself down where hover: hover says there is a pointer, and a headless desktop browser is a pointer, so a single pass had never measured a touch target at any viewport. Between them they found a 9px seat label at 3.39:1, a 37px-tall amount field and a 30px delete button on Quick Split.

A check that decides it doesn't apply says so in the output rather than reporting no offenders, which reads identically to a pass. The same goes for the two exemptions that exist: a disabled control, which WCAG itself excuses, and a link inside a sentence, whose size is set by the line it sits in.

Deliberately not in CI: the unit suite is dependency-free and runs in about five seconds, while this one needs a browser and a running site. Run it after any change that adds or resizes a control. With no Playwright to hand, paste uiAudit.js into devtools and call LayoutAudit.runAll().

The contrast check is built so it cannot report a false positive: backgrounds composite through alpha, and where a gradient is involved every colour stop is tried and the most flattering one used. A finding means the text fails even under the reading most generous to it. Two earlier attempts at this got it wrong in both directions — one read only background-color and scored text on a gradient against nothing, the other treated low-alpha stops as opaque — so uiAudit.js is checked against known-answer cases before it is believed.

Touch targets are measured by probing outward with elementFromPoint rather than by reading the element box, because the box lies in both directions: a small control can be padded out with a transparent ::after and respond well beyond its painted size, and a checkbox inside a label is hit by tapping the label. The 44px rule is only enforced where hover: none says there's no mouse.

It exists because these checks were being re-typed into a console every time something changed, and each time they only covered whatever had just been touched. That is how the standings table shipped with a player-name column zero pixels wide and kept it for two rounds.

Why the hand evaluator is tested the way it is

handRank.js is the only module here that can pay the wrong person without anybody noticing. A pot wizard that asks "who won?" is never more wrong than the dealer ticking the box; one that answers the question itself is wrong on its own account, silently, and a table that has come to trust it will not check.

So besides the hand-written cases — every category against every other, the wheel, three kickers deep, the counterfeited two pair, the board that plays — it is checked against a second evaluator written a different way, on twenty thousand random seven-card hands and four thousand random pairs of them. The second one is in the test file and shares no helper with the module: it scores a hand by counting ranks into a sortable list rather than by asking about shapes. That pass is most of the suite's running time and it is worth it; three deliberately introduced bugs — an ace that keeps its value in the wheel, a dropped two-pair kicker, a flush read only by its top card — are each caught by it as well as by the cases written for them.

Why the randomised suite exists

Every correctness bug found in this tool so far has been shape-dependent: blinds posted to empty chairs, the button skipping past players, heads-up acting out of order, an overbet announced as a side pot. None of them show up on a full six-handed table with equal stacks, which is the table anyone writes a fixed example for.

invariants.test.js deals a thousand hands across random seat counts, random empty chairs, random stack depths and random dealer input, and checks the things a dealer would notice being wrong:

  • chips are never created or destroyed during play
  • the pot breakdown plus the uncalled returns always equal the money that went in
  • paying the pot out settles it to the cent
  • once uncalled chips are handed back, nobody is still betting above the whole table
  • the button and both blinds always land on players who are actually in the hand

The generator is seeded, so a failure reproduces from the seed printed in the message rather than being a flake to re-run.