Files
JoshHeaps.Net/tests/holdem
jheapsandClaude Opus 5 1f3dda180f Make the table big enough for the game around it
Looked at the felt at three times scale and the seats were falling off it. Not
sitting at a rail — hanging past the outer edge of the table into the page,
half a puck on green and half on nothing. Measured:

  1280x900, nine-handed   five of nine seats past the rail, worst 49px
  844x390,  six-handed    all six, worst 20px
  430x932,  six-handed    two, worst 12px
  360x640,  nine-handed   none

The ring sat at a flat 44% by 42% of the seat layer at every size, two numbers
picked against a phone. A fixed pair can only be right for one ratio of puck to
felt, and the felt's ratio changes with every viewport — so it was a different
amount of wrong on each one, which is exactly what makes something look like a
mistake rather than a choice.

The radius is derived now: the widest ring whose outermost seat still finishes
inside the table, worked out from the measured table, seat layer and puck. It
can only ever pull seats in — the old pair is kept as the ceiling — and it falls
back to that pair whenever there is nothing to measure, which is every call from
the unit tests and the first render before the felt is laid out.

Three things this turned up on the way, each caught by the sweep:

  * The seat layer is not centred on the table. Its insets differ top from
    bottom, so the first version — which assumed symmetry and worked from half
    widths — left a phone with 12px still over the rail. It works in absolute
    pixels against the nearer edge now.

  * The radius and the puck size depend on each other: the ring is limited by
    how big a puck is, the puck is sized by the `seatsTight` class, and that
    class was decided from the gaps between seats, measured at a radius that
    hadn't been worked out yet. Fixing the gap measurement then made a
    nine-handed phone overhang that had been fine, because the two were chasing
    each other. It is settled by trying the candidates in preference order
    instead: the roomy layout if everything fits, the tight one if not, and if
    neither fits, tight pucks on a ring widened only as far as it takes to stop
    seats colliding — because two seats fighting over a tap is a worse failure
    than a puck over the rail. A dealer can see an overhang; they cannot see
    that the seat they pressed wasn't the one that answered.

  * The spacing threshold was a flat 84px, the figure a 96px puck was judged
    against. Asking a 68px tight puck to keep a 96px puck's distance is a test
    it can never pass, so every landscape table fell through to the fallback and
    took an overhang it didn't need. It is a ratio now, not a number.

Result: worst overhang anywhere goes 49px to 9px, and 59 of 65 configurations
are completely clean. Desktop, tablet and every phone held upright: nothing over
the rail at all.

Residual, reported rather than tuned away: six configurations still overhang by
1-9px, all of them landscape at 640x360 and 667x375, where the felt is about
110-125px tall and no ring puts readable pucks inside that. The audit check is
left strict so they stay visible.

uiAudit gains "seats and chips stay on the table", which runs at every viewport
in the sweep. Fault-injected: putting the flat 44/42 back reproduces the original
failure exactly, including seat 3 at 49px.

Faster, too, because the measuring got hoisted out of the per-seat loops while I
was in there:

  renderSeats   7.3ms -> 1.8ms
  renderAll    12.4ms -> 6.8ms

619 tests, 0 failures. 65 layout audits across 13 viewports x 5 table sizes.
Offline re-verified: a whole hand with the network cut, $800 conserved.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-03 11:55:51 -06:00
..
2026-09-01 12:34:53 -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
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, and that no text is painted outside its box. 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.

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 a second, 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 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.