polish/holdem-seats-one-size
117
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
29188d6049 |
Say ALL IN on the felt
A player with nothing behind had a blank where their stack goes. So did a seat whose stack nobody had entered. On a table with side pots that is the one distinction the dealer is working from -- who can still be bet at, and who is already in for everything they have -- and the felt drew the two states identically. Ann $40 $160 has chips Ben $40 all-in for 40 Cal $40 $160 has chips Dee $12 all-in for 12 The action bar knew: it had already built "Main pot $48, Ann/Ben/Cal/Dee" and "Side pot 1 $84, Ann/Ben/Cal". The table it sits under didn't say a word. Betting.isAllIn is the same stack test nextToAct uses to pass over a player, which is the point: the seat the felt marks is exactly the seat the dealer will never be asked to act on again. There's a test asserting that equivalence rather than restating the condition. The second half of the condition matters. A seat with an empty stack that has put nothing in is a busted player who shouldn't have been dealt in, not a player who moved all-in, so it isn't marked. "ALL IN" in gold where the stack was, and a warmer border on the seat -- deliberately quieter than the acting-seat ring, because all-in is a standing fact about a player rather than a request for attention. 9 tests (172 total). They fail on a naive condition: dropping the committed-chips and folded checks fails 4 of them. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
4c1ac222a8 |
Give Enter and Space back to the control that has focus
Found while checking the keyboard could still reach the dealer button: it could reach it and then couldn't press it. Enter and Space were bound to "advance" for the whole document, with an exception only for text fields. Those two keys are how a keyboard presses the control it is sitting on, so every button on the page lost them -- and not to nothing. Measured: Tab to Resolve Pot, press Enter -> the table dealt the next hand Tab to Resolve Pot, press Space -> Resolve Pot, correctly Tab to a dealer button, Enter -> nothing at all The inconsistency is the shortcut handler's return value. It calls preventDefault only when it handled the key, so when "advance" applied it ran instead of the button, and when it didn't apply the browser's own activation got through. A control that does something else every other press is worse than one that does nothing. Enter and Space now go to a focused button, [role=button], link or summary, and the shortcut only claims them when nothing pressable has focus. The letters stay live with a button focused -- a button does nothing with "f", and a dealer whose hand is on the keys shouldn't lose them to a stray focus ring. Space still advances the street: dealt, f, f, c, c, Space -> flop. The action bar rebuilds itself after every press, so focus falls back to the body and the dealer's Space is never quietly aimed at the Fold they just clicked. 11 tests (163 total), and they fail on the old handler -- 4 of them, exactly the Enter/Space-on-a-control cases. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
784315f2ea |
Put one dealer button on the table
A poker table has one dealer button. This one drew nine.
Every seat carried a puck: the real one in gold, and eight ghosts at 0.32 opacity
that existed to be pressed. Measured on a nine-seat table -- eight pale discs, all
eight showing the letter D, one of them the button. Finding it meant comparing nine
identical shapes for the one that's gold, when it should be the only thing of its
kind on the felt.
The ghosts also sat on their neighbours. Every remaining "something important is
covered" offender on a crowded table was a live seat covered by the seat-next-door's
ghost puck: 844x390, 915x412, 360x640 and 640x360 each reported one, and they were
all the same thing.
And a screen reader heard nine buttons named "D".
So a seat without the button doesn't draw one. Hovering a seat fades its slot in --
a preview of where the button would go, one click as before -- and on touch, where
there is no hover, "Give the button" has been in the seat's long-press menu all
along. Keyboard reaches it by focus, where the slot appears too.
pointer-events matters as much as the opacity: an invisible puck that still took
taps would be worse than a visible one.
visible pucks 9 -> 1 at every size
layout offenders 915x412 1->0, 360x640 1->0, 844x390 3->2, 640x360 3->2
clean viewports 4 of 9 -> 6 of 9
labels nine "D" -> "Ann has the dealer button" and
"Give the dealer button to Ben"
The audit now checks it, because this is a rule about the table rather than about
CSS: one puck drawn, and no two announcing themselves the same way. Verified against
the old behaviour put back -- it reports 9 and 9.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
e01d8e7c49 |
Tapping a seat gives you that seat
Went to audit the seat editor and couldn't open it: Playwright refused to click the middle of seat 1 because seat 9's dealer chip was sitting there. On a nine-seat table, tapping the centre of one player handed the button to another. I shipped that. The enlarged chip hit area grew outward from a chip already hung over its seat's corner at -10px/-10px -- and on an oval, a seat's corner is the seat next to it. Measured on the nine-seat table: seat 1 10 of 49 sampled points taken by seat 9's chip, including its centre seat 4 9 of 49 taken by seat 5's chip seat 5 3 of 49 taken by seat 6's chip I claimed at the time that no seat tap was stolen, and I had checked -- with an audit that exempted anything inside a `.seat` as harmless crowding. The thief was inside a seat. The check walked straight past it. Two fixes, and the second matters more The chip sits inside its seat now rather than on the rim, and its hit area grows inward over its own seat, which is a big target that can spare a corner. Padding inward alone wasn't enough -- I tried it and the numbers didn't move, because the chip's own box was the problem, not just the pad. Isolating that took disabling the pad and then the overhang separately: pad off gave 2 seats affected, overhang off gave zero. And the audit now asks whether a seat's centre belongs to that seat, as its own check. The occlusion check couldn't answer it: it was built for overlays on top of the table, and this was two pieces of the table fighting over a tap. It exempts a neighbour's body -- seats do overlap by design -- but not a neighbour's controls, which was the exact hole. Seven of eight sizes now have no seat whose centre goes elsewhere, and all four seats I tap open their own editor. What's left, stated rather than buried At 360x640 and 375x667 one seat still loses a single sample point of its rim to a neighbour's chip. Centres are correct; this is the rim of a crowded oval. At 844x390 two seats have centres belonging to the seat next door -- and those are seat bodies, not chips. Nine seats on a 163px-tall landscape table overlap almost completely. That's the landscape felt sizing, it predates all of this, and it wants fixing on its own terms rather than being folded in here. Both are now reported by the audit instead of hidden by it, which is the part I actually trust. 152 tests still passing; contrast clean at every size. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
c77161c3e9 |
Stop the last two things the table did behind the dealer's back
Your bug report was about a category, not one button, so I went looking for the
rest of the guessing rather than picking a new topic. Two left, both about the
button -- and one of them was in the fix I'd just shipped.
The deal preview lied
The prompt added last time promises who will post. With the button parked on an
empty chair it promised the wrong pair: it read the button where it currently sat,
while dealing relocates the button onto a player first and only then works out the
blinds.
three players in seats 2-4, button parked on empty seat 1
preview said: s2 posts $1, s3 posts $2
dealing gave: s3 posts $1, s4 posts $2
Exactly the fault you reported -- the display and the thing it describes computed
from different moments -- inside the change meant to remove it. buttonSeatForDeal
answers where the button will be without moving it, so the preview asks the same
question the deal will answer. And the move is now stated before it happens:
"Button moves to Ben - seat 1 is empty."
Moving the button after the deal moved the blinds silently
Once the blinds are posted, moving the button takes them back and re-posts them,
because otherwise the badges and the money disagree. That's the right behaviour
and it was completely silent. The toast says so now: "Button on Dee - blinds taken
back and re-posted."
It also only does it when there is actually something posted to move. Before the
deal there is nothing committed, so passing the button is just moving a marker --
which is the whole point of the setup step.
Checked and deliberately left alone
Streets still advance one press at a time during an all-in run-out, which is right
-- the dealer is physically dealing the board between them. A player who joins
mid-hand is still marked folded for that hand and plays the next one. The
base-pot-amount shorthand still stands in for "everyone antes the same". Nothing
mutates state during a render.
4 tests (152 total): the preview matches what dealing produces with the button on
an empty chair, asking where the button will be doesn't move it, a button already
on a player is left alone, and an empty table doesn't break the question.
Driven through the UI: the prompt warns about the move and then the deal matches
it exactly; moving the button after the deal announces the re-post; and the blinds
survive being passed around three seats with the chip count fixed at 400. Layout
and contrast audits clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
843ef9cd42 |
Wait to be told to deal, instead of guessing
Dealing and betting were one button. "Next Hand" moved the button and posted the
blinds in the same breath, so the moment between hands -- the one where people are
still sitting down, standing up and arguing about whose turn it is -- was spent
with money already committed.
That's where the guessing came from. The blind badges recompute from `inHand`
every render, but the posted chips are frozen from whenever beginHand ran, so the
two drift apart the instant the table changes:
seat a fourth player after Next Hand
badges said sb=s3 bb=s4, money on the table was s1=$2 s3=$1
s4 was shown as the big blind having posted nothing
take out the big blind after Next Hand
badges moved to bb=s1, s4's $2 stayed in the pot, s1 had posted nothing
press Next Hand twice
burned a hand, moved the button twice, chips went 300 -> 297
Now the dealer says when. Next Hand advances the button and clears the table, and
stops. The bar shows who will post -- "Ben posts $1, Cal posts $2" -- and waits on
"Post blinds & deal". Enter does it too, so a dealer can carry a hand from setup
through the streets to showdown without reaching for the trackpad.
Until that press nothing is committed, so setting the table is free: seat someone,
take someone out, pass the button around, name the blinds by hand. The preview
follows every change and the chip count never moves. Settling a pot ends the hand
rather than starting the next one, for the same reason -- people cash out between
hands.
The preview is honest about what it can't do: fewer than two players and there's
no button at all, just what's missing.
A table saved before this existed has no flag, and guessing "not dealt" would
offer to deal over a live hand. Anything already committed, or anyone already on
the clock, means the betting was open when the tab was closed.
11 tests (148 total): the table waits, no money moves until the deal, seating and
unseating and passing the button all cost nothing, what the preview promises is
what gets posted, and a mid-hand save comes back mid-hand.
Driven through the real UI: seat a player and move the button while waiting, watch
the preview follow with the chip count fixed at 400, deal, play it out, settle, and
land back in setup. Triple-tapping Next Hand now costs nothing but button position
-- it used to cost a hand and three chips. Layout and contrast audits clean in both
the new setup state and after dealing.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
d66abda05a |
Make the dealer puck hittable without making it bigger
Ran the touch-target check that had been sitting in the audit unused. The dealer puck is 22x22 — half what a fingertip needs — in ten places on the table, and tapping it hands the button to that seat and re-posts the blinds. Not a cosmetic control. It stays 22x22. A puck should look like a puck sitting on the rim of its seat, and growing it would take area from the seat itself, which a dealer taps far more often. Instead an invisible pad around it catches the tap: 43x44 hittable on every phone size, close to four times the area, with the pad pushed down and left away from the seat the chip juts towards. Verified that no seat tap is stolen — the occlusion audit is clean at every size. Three things the check itself got wrong, all found before acting on them It read element boxes, and boxes lie in both directions. A control padded out with a transparent ::after responds well past its painted size, and a checkbox wrapped in a label is hit by tapping anywhere on the label — so it reported the dedicated dealer toggle as a 24px fault when it sits inside a 320x44 label that already forwards the tap. It probes outward with elementFromPoint now, which is what a finger actually experiences. The probe walked a fixed 40px each way, so everything wider than 80px reported exactly 80. Every header button came back a suspiciously round "80x42". And it enforced 44px on desktop, where there is no fingertip. Those same header buttons are 64px tall on a phone and 42px with a mouse, which is not a defect. Keyed off hover:none now, the same signal used for the tablet field sizing. Also removed a ::after pad on the dedicated-dealer checkbox that was both inert -- pseudo-elements don't render on replaced elements -- and unnecessary, since the label already does the job. The puck is the one documented exemption: it is deliberately small and "Give the button" is in the seat menu at full size, so it's a shortcut rather than the only way through. The reason sits in the code next to the exemption. Phone sizes: dealer chip 22x22 drawn / 43x44 hittable, zero undersized controls even with the exemption and the pointer rule forced off. Layout and contrast audits clean at all seven sizes. 137 tests passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6fa4532781 |
Fix the contrast the audit could finally measure
Contrast is the one check I've got wrong twice, both times by being clever about backgrounds. The first version read only background-color, so text on any of the gradient-painted sheets was measured against transparency and the dealer puck's dark "D" was reported at 1.2:1 against nothing. The second over-corrected and treated low-alpha gradient stops as opaque, scoring white-on-dark at 1.0:1. So this one is built to be incapable of false positives rather than exact. Backgrounds composite through alpha; 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. It's checked against known answers before being believed: black on white at 21:1, white on white at 1:1, #767676 on white at the 4.54:1 AA boundary, white over a dark gradient, dark text on that same gradient, white over a 6% white wash on a near-black page. Nine cases, all correct — and building them found a real bug in the check, which walked past an opaque gradient and offered the page background as a candidate. That made it too lenient, which is as bad as the old false positives. What it found: Both blind badges failed against their own gradients, in 9px bold text a dealer reads at a glance across a table. At the worst stop: small blind 3.52:1, big blind 2.15:1. The small blind keeps its blue and darkens to 4.77:1. The big blind flips to dark text on brighter gold, 5.84:1 — how a gold chip is marked anyway, and it makes the two badges far easier to tell apart at speed. Quick Split's footnote names a control the dealer is told to reach for, and that name inherited the muted grey of the surrounding aside: 3.39:1. Emphasised text carrying an instruction gets its own colour now. The felt watermark came back at 1.19:1 and is meant to. It's decoration; holding it to a reading threshold would mean making it not a watermark. It's aria-hidden now, which says the same thing where it also does some good, and the audit skips aria-hidden subtrees. 32 contrast passes over the table, all five overlays and Quick Split at five viewports: zero findings, zero unmeasured. Layout audit still clean. 137 tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
2f6d63400a |
Stop re-typing the layout audit into a console every round
Last round I found a regression I'd shipped two rounds earlier: the standings table's player-name column was zero pixels wide, at every viewport, and nobody noticed because the audit that would have caught it only ever existed in a previous session's scrollback. These checks have been re-written by hand at least six times, and each time they covered only whatever had just changed. So they're a file now. uiAudit.js runs in the page — no build step, no package.json, no dependency — and checks touch-action and tap highlights, that fields stay selectable and are big enough not to trigger iOS zoom, that nothing important is covered or stranded off-screen, that no grid column has collapsed to nothing, and that no text is painted outside its box. runUiAudit.js drives it over the table, all five overlays and Quick Split at thirteen viewports. Deliberately outside CI: the unit suite is dependency-free and runs in a second, this one needs a browser and a running site. Three real faults it found on its first run Fields were 13.3px on every iPad. The 16px minimum that stops iOS zooming the page on focus went in inside a max-width:700px block, and every iPad is wider than that — so a dealer on a tablet, which is the device this is most likely to be propped up on, got the table zoomed every time they touched the blind amount. The reason for that rule was never "narrow screen", it was "iOS". Font size applies at all widths now, and the 44px touch targets key off hover:none, which is what actually distinguishes a finger from a mouse. The seat menu couldn't be scrolled. It had no height limit and no overflow, so on a phone held sideways its first two actions — Fold and All-in — sat below the bottom of the screen with no way to reach them. A dealer in landscape could long-press a seat and then not fold it. The pot wizard sat under the action bar. Every other overlay is z-index 90-98; the wizard alone was at 50, below both the action bar at 55 and the toast at 80. In landscape one of its inputs was completely covered by the Fold button, and a toast could sit over "Confirm Winner(s)". It was the odd one out rather than a decision. Three faults in the audit itself, found by checking before acting Worth recording, because each would have had me "fix" something that wasn't broken: - an ellipsised name was reported as painting outside its seat. A Range measures laid-out text, which runs past the element while overflow:hidden quietly clips it. Only overflow that is actually visible can paint over a neighbour. - every link below the fold was reported as off-screen. Something under the fold on a page that scrolls is reachable; the check now asks whether scrolling can bring it into view, which is the thing that actually matters. - a button in the newly-scrollable seat menu came back "covered by null". elementFromPoint returns null for a point outside a scrollable ancestor's clip — scrolled out of view, not covered by anything. A toast partly over a control is a warning rather than a failure now: it dismisses itself and carries a close button, and on a desktop it necessarily lands over a table that fills the content column. Fully covered by a toast is still a failure, because that was a real bug once — a seat sat exactly under the Undo button. Final sweep: 13 viewports, 65 overlay checks, 13 full-page audits, 4 Quick Split audits, zero failures, one warning. 137 unit tests still passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
aeb02d023d |
A short all-in doesn't reopen the betting
Went looking at dealer procedure again and ended up checking the rule home games argue about longest. The tool had it wrong. Blinds 1/2. Seat 4 raises to 10 — a full raise of 8. Seat 1 calls. Seat 2 is all-in for 13: three more, where a full raise would need eight. Under TDA rule 44 that does not reopen the betting to anyone who has already acted; they owe the extra three or they fold. Seat 1 and seat 4 were both being handed another go. Two things were wrong in raiseTo. Any increase at all reset hasActed for every live player, so a short shove gave the whole table another chance to raise. And lastRaiseSize was overwritten with the short amount, so the next legal raise was computed off 3 instead of 8 and came out too small — a second, quieter error that only shows up when somebody then tries to raise. Both now key off whether the increase was actually a full raise. A player who had not yet acted keeps full rights, which falls out naturally because their hasActed is already false — that distinction is the part of the rule people get wrong in the other direction. Everyone still owes the difference: isStreetComplete already asks anyone with chips left to call, so the short all-in is answered without being re-raiseable. The rulings sheet now covers what the tool does for you It had three entries. A dealer challenged on a payout couldn't look up why the tool did what it did, even for the rules it enforces automatically. Added the short all-in rule above, uncalled chips coming back rather than being announced as a side pot, and the odd chip in a chopped pot going to the first player left of the button — all three are things this tool already decides silently, and all three are worth being able to point at when somebody objects. 10 tests (137 total): already-acted players aren't reopened but still owe the difference, a player yet to act keeps full rights, the minimum raise isn't shrunk, calling it ends the street, no chips are created, and — the other half of the rule — a full-sized all-in, an ordinary raise, a raise exactly matching the last one, and a full-sized opening bet all still reopen everyone. Validated through the real action bar: after the short shove, lastRaiseSize stays 8 and the player who had already called stays settled, while the two who hadn't acted keep their turn. The longer rulings sheet still fits a phone, scrolls, traps focus and closes on Escape. Sources: TDA rules via thelodgepokerclub.com/tda-rules/, pokernews.com/strategy/deal-with-it-tips-for-mistake-free-dealing-in-home-games-30231.htm Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
60d192a9ec |
Let the banker check the cash box against the chips on the table
Ran out of things I could find by auditing the UI, so I went and read what people
who run home games actually complain about. The recurring one isn't a dealing
mistake, it's a banking one: "the last two guys go to cash out at the end of some
all-night game and there isn't enough cash to cover the chips they have."
Every chip on the table is a promise to pay cash for it later. Nothing in here
checked that the promise could be kept, and there's no way to notice by eye —
it's always a rebuy nobody wrote down, or chips taken without paying, and it
stays silent until the box is empty and two people are still holding chips.
The books now add up, or say why not
HoldemState.bankReconciliation compares the cash taken in against every chip that
could be handed back — stacks, chips in front of players, and the pot waiting to
be won. The sidebar shows both numbers and what they mean:
Balanced — the cash covers every chip on the table.
$160 more in chips than cash taken. Someone's rebuy probably wasn't
recorded — the box will be short at cash-out.
$150 more cash than chips in play. Someone has paid for chips they
haven't been given.
Money in the middle counts as chips in play, so an open pot isn't reported as a
shortfall — otherwise it would cry wolf during almost every hand.
Players who had bought in were missing from the settle-up
sessionStandings was built from the hand log, so anybody who hadn't yet been in a
recorded hand didn't appear at all. Somebody hands over $200, sits down, folds a
few, and the banker's own record doesn't mention them — then they turn up at
cash-out holding chips the list never accounted for. Anyone with money in the
game is in the list now, whether or not they've played a logged hand.
The settle-up number was already being computed and hidden
cashPosition — chips in front of them less the cash they handed over — was
calculated for every player and written to the CSV, but never shown on screen.
It's the number you read while counting out cash at the end of the night, so it's
a column now.
15 tests (127 total): a buy-in with no hands played still appears and reads as
level rather than a loss, empty chairs stay out, a player who cashed out shows
what they took, both directions of imbalance are caught, an open pot is not a
discrepancy, six played-and-settled hands never knock the books out, a proper
rebuy balances while a chip grab doesn't, and float dust isn't an alarm.
Validated with Playwright through real play — seated, blinds posted, pot open,
hand settled, rebuy — balanced at every step, plus the three seeded verdicts and
a fresh table where the panel stays hidden.
Sources: pokerlistings.com/blog/how-to-run-a-home-poker-game,
chipsoffury.com/learn/how-to-handle-poker-buy-ins-with-friends/
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
cc6a1902bc |
Stop the tool from throwing a pot away without saying so
Two ways to lose a hand's money by accident, both found by walking the settle flow and checking the chip count afterwards. Next Hand destroyed an unresolved pot silently Starting the next hand clears every seat's contribution, so money still in the middle simply vanished. Measured at a four-handed table with $8 in the pot: the table went from 800 chips to 792, and the toast said "Next hand — button on seat 3. Blinds posted." — not a word about the eight chips that had just left. It now asks, names the amount, and says what will happen to it. Confirming still works and the toast says what went. The undo was always there, but a dealer can't undo something they were never told about. The wizard's "Close" abandoned a payout that looked finished After picking winners and confirming, the wizard shows "Final Payouts — Ann $8" above two buttons: "Start Next Hand" and "Close". Close pays nobody. Under that heading, with a name and an amount beside it, a dealer taps Close believing the pot has been pushed — and walks away with the chips still in the middle and the hand unlogged. The button now says "Cancel — don't pay yet", with a line underneath spelling out that nothing has been paid. The warning measures the right thing The first version counted the pot badge, which was wrong in a way the tests caught before it shipped: the table-wide base pot is a standing ante for dealers who don't track stacks, and it re-applies to the next hand rather than being lost. Quoting it would have put a confirmation in front of those dealers before every single hand — a nag, on the simplest way to run a table, for money that wasn't going anywhere. HoldemState.unresolvedContributions() measures exactly the fields startNextHand clears: banked contributions, chip counts, and whatever is still in front of players on this street. Verified both ways in the browser — a base-pot table showing a $100 pot is not asked at all, a stack-tracking table with $8 live is. 6 tests (115 total): the quoted number equals what actually leaves the table, mid-street chips are included, a settled hand leaves nothing to warn about, five settled hands conserve every chip, an empty table has no phantom pot, and a standing ante is never warned about. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
a488bbf832 |
Stop rebuilding the hand log after every fold, call and raise
Profiled a long night at a full table — 24 seats, 200 logged hands — because
that's the state the tool is slowest in and the one a dealer reaches at 2am.
A full re-render cost 30ms, and it runs after every single action. Broken down,
25.7ms of that was renderHandHistory: tearing down and rebuilding 200 rows of a
panel that hadn't changed and that nobody is looking at mid-hand. Everything
else on the table put together came to about 4ms.
The log only changes when a hand is settled or the history is cleared — maybe
once every thirty taps. It now carries a cheap signature (length, hand counter,
and the newest row) and is skipped when that hasn't moved. Entries are immutable
once written and only ever added at the front, so the signature catches a
recorded hand, a cleared history, and an undo that puts a hand back.
renderAll, every action 30.07ms -> 4.18ms
renderHandHistory, skipped 25.74ms -> 0ms
renderHandHistory, rebuild 25.74ms -> 9.05ms
The rebuild got faster too, for the times it does run — once per hand, where
25ms of jank lands exactly on the "next hand" tap. toLocaleTimeString was the
single most expensive call in the loop and a logged hand's timestamp never
changes, so the formatted string is kept; and the rows are built into a fragment
so the panel lays out once instead of two hundred times.
A cache like this is only worth having if it can't go stale — a dealer reading
last hand's payouts while settling this one is worse than a slow panel. 10 tests
cover the invalidation directly: unchanged through a street of betting, changed
by settling, by clearing, by an undo back to a shorter log, by two hands that
differ only in the pot, and by the counter moving on.
Validated with Playwright against the real page: the rendered rows match the
stored history through settling a hand in the wizard, undoing it, clearing the
log and recording again. Touch and layout guarantees from the earlier rounds
still hold — 0 of 87 controls on touch-action auto, 0 tap-highlighted, every
stack still inside its own puck. 109 tests passing.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
9ee4816fa4 |
Make the overlays usable from a keyboard
Measured every overlay in the tool before touching anything. For a dealer running this on a laptop, all six were broken in the same three ways. - focus never moved into the dialog: 0 of 5 measured - Tab walked straight out of the dialog into the page behind the scrim: 5 of 5 - Escape did nothing: only the rulings sheet handled it, 1 of 5 - the pot wizard — the dialog that pays people — had no role and no accessible name at all So opening the pot wizard left focus on the button behind it, tabbing wandered through a page nobody can see or click, and there was no way back out except the mouse. One implementation, not six The overlays are opened and closed by five different modules, which is why they each missed the same things. Rather than patch this into all of them, modal.js watches the `hidden` attribute they already toggle: focus moves to the first real control on open, Tab and Shift-Tab wrap inside the dialog, Escape closes it, and focus returns to whatever opened it. `aria-modal` is set while open and removed on close. An overlay added later gets all of it without anyone remembering to wire it up, which is the failure being fixed. Escape presses the dialog's own cancel button rather than just hiding the overlay, so the confirm dialog still resolves its promise — verified: Escape resolves it to false rather than leaving an await hanging forever. Shortcuts.overlayOpen() had the same shape of bug, listing the six overlay ids by hand. It asks the document now, so a sheet added later can't quietly start letting "f" fold the table behind it. Focus you can see Three focus rules existed for the whole stylesheet, so tabbing relied on the browser default ring, which on these dark custom-painted controls is close to invisible. `:focus-visible` keeps it to keyboard use so a dealer tapping Fold isn't left with a ring on it. That surfaced something small: `.holdemBtn` used `transition: all`, so the new outline faded in over 150ms and the ring lagged a step behind the Tab key. The transition names its properties now. Measured after: 16 of 16 tab stops show a ring immediately, and the press state from before still animates. Also - a mistyped negative stack read back as negative, and flowed into putIn() where Math.min(amount, -50) moves a negative amount and adds chips to the table. Clamped at zero. - accessible names for the chip colour, chip value and notes fields, which had none 11 new tests (99 total), covering negative stacks, negative and unreadable blinds, blinds nobody can cover, and a save that comes back corrupt, truncated, or from before a field existed. Playwright: all five overlays re-measured — focus moves in 5/5, Tab and Shift-Tab trapped 5/5, Escape closes 5/5, focus restored 5/5, role and aria-modal present 5/5. Shortcuts still fire when no dialog is open and are still blocked when one is. Touch guarantees from the earlier round unchanged: 0 of 49 controls on touch-action auto, 0 tap-highlighted. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
db208a1a86 |
Make big numbers readable, and make the export add up
Seeded a table that looks like a real game — "Mike from work", "Jean-Philippe", stacks from $12 to $3,200,000 — and measured what the layout does with it. ".00" was costing a third of a seat puck Every amount rendered with two forced decimal places. Measured, the trailing ".00" is ~22px, against the 64px a seat puck has to show a stack in. So "$1,250,000.00" needed 102px of a 64px box — and because a number has no spaces to break on, it neither wrapped nor ellipsised. It drew straight out over the seat next to it. Money now shows cents only when there are cents. A chip game deals in whole numbers nearly all the time, and "$12" says exactly what "$12.00" said in half the width. A quarter-blind game is untouched: $0.25, $12.50 and $0.75 pots all still read as before, checked against a 25c/50c table. That alone took "$1,250,000.00" down to "$1,250,000" — 102px to 80px, still wider than the 64px puck. The remaining overflow is handled by stepping the font down on long amounts, which keeps every digit. Abbreviating to "$1.25M" would have fit more comfortably and was the obvious move, but a dealer reading a rounded stack off the felt is a worse failure than a small one, and the exact figure is the entire point of the tool. Measured after: every stack now paints inside its own puck, nothing spills onto a neighbour. Because Money.format is the single formatter all 47 call sites go through, this lands on the seat pucks, action bar, pot breakdown, seat menu, standings and history at once — the payoff from consolidating it earlier. The CSV export couldn't be added up The file exists so a house manager or a disputed chip count has something that outlives the tab, and every money column in it was written as text: "$1,250,000.00". The commas were quoted correctly so the file always parsed — it just imported as strings, so the one thing anyone opens it to do, total a column, needed every cell cleaned first. Money columns now go in as bare numbers (1250000.00) with no symbol and no separators. The columns meant for humans stay human: names are still names, payouts still read "Alexandra: $1,250,000", and a name with a comma in it is still quoted. 12 new tests (88 total, all passing), including the case that would be ruined by dropping cents unconditionally, and a proper quote-aware CSV parser to check every money cell parses as a number. Re-ran the occlusion sweep from the previous round at 9 seats, 10 seats and tournament stacks: still clean, no regression from the font changes. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
3d63ba50ee |
Handle the messy table: people joining, leaving, and arguing about blinds
Home games don't run like a cardroom. People walk up mid-hand, storm off mid-hand, and pass the button round three times before anyone deals. Probing each of those found four problems, one of which loses chips. A player taken out mid-hand took their chips with them `computeContribution` opened with `if (!player.inHand) return 0`, so the moment a dealer switched a seat off, everything that seat had already put in vanished from the pot. Measured: four players in for $2 each, one taken out, pot drops from $8 to $6. Their $2 was still sitting on the seat record — it just stopped counting. A dealer following the tool would have paid out $6 of an $8 pot. Chips already pushed into the middle are dead money. They stay in the pot and go to whoever wins, whether or not the player is still in the hand, and it is not theirs to take back. The `inHand` check now guards only the table-wide base-pot default, which does have to stay tied to being dealt in or every empty chair would be treated as having anted. The pot wizard counts a departed seat as folded, so its chips are in the pot but it can never be picked as a winner. Passing the button before the cards come out Moving the button left the posted blinds where they were while everything positional started reading off the new spot, so the money and the blind positions silently disagreed. While nothing but the blinds are in, moving the button now refunds them and re-posts from where it lands — pass it round six times and the chip count is unchanged. Once anybody has acted the blinds are settled and it stops offering. Naming the blinds by hand Blinds were purely positional with no way to overrule them. The seat menu now offers "Make small blind" / "Make big blind" while only the blinds are in, and a badge with a ring marks a blind that was named rather than dealt. Naming one pins the other where it is. It's refused if it would put both blinds on one player or post to somebody who isn't dealt in, and it lasts one hand — carrying it forward would freeze the blinds on two people for the night. The first hand, and a table that empties out A fresh table puts the button on seat 1 whether or not anyone is sitting there, and a table can empty out around it. The button is moved onto a real player before a hand is dealt. Joining mid-hand A newcomer was added straight into the live seats and would be asked to act in a hand they hadn't been paying into. They now sit the hand out and are a full player from the next one. Nothing is covered on a phone any more Swept 13 viewport sizes with elementFromPoint, five points per control, on the table and every overlay. Before: the action bar covered the bottom seats on short screens (62px of felt at 360x640), the toast covered up to six seats at once — Undo button included, so a tap meant for a player hit "Undo" — and in landscape the table came out 765px tall in a 390px window, putting six of nine seats off-screen entirely. - the action bar's real height is published as `--actionBarHeight` and drives both the page's bottom padding and the toast, replacing a hardcoded 9rem that was wrong the moment the raise pad opened - landscape sizes the felt from the height that's left, not from width. The first attempt did nothing: `padding-bottom` is a percentage of the containing block's *width*, so narrowing the wrap left it full height, and a `max-width: 900px` rule later in the file was overriding it anyway. It's set outright now and lives at the end of the sheet. - on short screens the toast floats over the action bar's status readout rather than over the seats. Nowhere is clear of both; covering something you re-read beats covering something you tap. - headers go compact on short screens, which is where the vertical room came from 13/13 sizes clean, 360x640 through 844x390, portrait and landscape. Also fixes a marker that flickered: `classList.toggle(cls, undefined)` toggles rather than clearing, and `manual` is undefined for positional blinds, so the hand-picked ring appeared on alternate renders. 18 new tests (76 total, all passing). Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
c1a484b97a |
Pay down the Hold'em dealer's tech debt and give it a test suite
Every module in the dealer ends with a `module.exports` guard, so they were all written to be testable, and none of them were ever tested. The correctness fixes that went in recently were checked by hand in a browser console that has since been closed. This puts a real safety net under them and clears out the duplication that made those bugs possible in the first place. Tests (58, ~300ms, no dependencies) Node's built-in runner, no package.json and no install step: the modules are plain script-tag files, so `load.js` just requires them and puts them on globalThis under the names they use to find each other in the page. The randomised suite is the part that matters. Every bug found in this tool so far has been shape-dependent — blinds posted to empty chairs, the button skipping players, heads-up acting out of order — and none of them show up on the full six-handed table anyone would write a fixed example for. It deals 1000 hands per property across random seat counts, empty chairs, stack depths and dealer input, checking that chips are never created or destroyed, that the pot breakdown plus uncalled returns equals the money in, that paying out settles to the cent, and that the button and blinds always land on real players. The generator is seeded so a failure reproduces instead of flaking. One implementation each - `Math.round(x * 100) / 100` was written out 16 times across 6 files. It's `Money.round` / `toCents` / `fromCents` now, in the module both pages already load first. - The chip epsilon was declared twice with the same value under two names (`PotCalculator.EPSILON`, `Betting.EPSILON_CHIPS`). Both now point at `Money.EPSILON`. - `Betting.seatAt` and `HoldemState.nextSeatIndex` were the same modular arithmetic written twice. Walking the seats is exactly where the button, the blinds and the action drifted apart from each other before, so there is one of it now. - The undo snapshot/restore dance appeared three times in main.js, each one re-deleting every key on `state` in place. It's `snapshotState` / `restoreState` once, with the reason it can't just reassign written down. A bug the tests found `layers()` and `uncalledReturns()` both decide what money was never contested, separately, and disagreed. `uncalledReturns` measured each player against the biggest live contribution *including their own*, which makes the biggest bet its own yardstick — so the one case it exists for came back empty, and `layers()` announced the overbet as a side pot only the bettor could win. With Ann shoving 200 into Ben's 60, the dealer was told "main pot 4, side pot 3, side pot 116, side pot 140" — four pots, one of them fictional. It now reads "main pot 123, uncalled 140 back to Ann", which is what a dealer says out loud. Layers are derived from contributions with the uncalled money already removed, so the two can't disagree again. A band every payer folded out of is now merged into the pot below rather than dropped, which was quietly shrinking the announced pot in another edge case. Validated with Playwright against a real hand on both surfaces — the table's live pot readout and Quick Split's all-in mode — plus undo, with no console errors. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |