feat/casino-holdem-clock
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0e2821257f |
Run the clock on a player, instead of counting in your head
The bar has had a stopwatch on the acting seat for a while: how long this player has been thinking, so the dealer knows when it's worth a nudge. What it never had was the thing a dealer actually does about it. Calling the clock is a procedure — a player asks, the dealer grants it, the player gets a set time and then a countdown said out loud, and if they still haven't acted the hand is decided for them. Running that from this tool meant counting in your head while the table argued about whether it had been thirty seconds yet. Thirty and then ten, which is the common form and what the TDA writes down. The countdown is the part the table hears, so it is marked apart rather than being ten more seconds of the same: the readout switches to a bare number and turns red. At zero the bar says the ruling in the dealer's own words and offers it as one button. The ruling depends on what they were facing, and getting that backwards is the part worth being careful about. Facing a bet, the hand is dead. Facing nothing, it is checked — nobody gets folded for being slow when checking was free. Three things about where it sits. The button appears only once somebody has been thinking long enough for the table to start asking, which is the same threshold the stopwatch already marks, so an ordinary hand costs no room at all: measured at 390x844, the bar is 152px with or without it. It is 44px like everything else a thumb has to find — it was drawn at 36 and the audit called it at every touch viewport the moment the check ran, which is the rule doing its job on a control that kills a hand. And once the count is over the Stop button goes, because there is nothing left to stop: at 640x360 its 94px pushed the undo control onto a second line and the taller bar covered the two bottom seats. The ordinary Fold/Check/Raise stay underneath the ruling. A player who speaks up as the count ends is still acting, and whether it was in time is the dealer's call, not the tool's. 11 new tests, and both halves of the ruling were inverted on purpose to check they catch it. Watched end to end in the browser at shortened intervals: the button appears, the count runs down, the ruling lands, and the hand is killed — or checked — with the undo control naming what happened. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
c19575ab10 |
Count the house's cut instead of calling it missing chips
A game that takes a rake has chips leaving the table every hand, and the bank check had no idea. It added up the buy-ins, added up the chips on the felt, and told the dealer somebody had been paid for chips they were never given — a shortfall that grows all night and points at a miscount that never happened. Every home game that takes a drop for the beer fund hits this on hand one. So the tool takes it, rather than watching it go. Under Table Setup: a percentage, an optional cap, and "no flop, no drop" — the rule almost everyone plays, on by default, where a hand that ends before the flop pays nothing. Zero percent is a game that doesn't rake, which is most of them and the default, and nothing about that game changes. Where it shows up is the point. The pot wizard says "House takes $2 of $40 — 5%, capped at $5" above the payouts, before a chip moves, because the summary is the dealer's last chance to disagree with the arithmetic and money taken quietly is the one thing a tool like this must never do. The bank check gets a House take line and says "balanced ... once the $2 in the house's cut is counted". The hand log keeps the pot as the table saw it with the cut recorded beside it, and the export has a column for it, so the night's total is one sum in a spreadsheet. Two decisions worth stating. The rake rounds *down* to a chip the table actually has: a house that can't make 40c takes nothing and the players keep it, which is the only way to be wrong about this that nobody argues with. And on a split pot it comes off the top, then what is left is shared in proportion — with the rounding leftovers going back to the winners rather than to the house, which asked for a number and gets that number, not that number plus dust. Measured end to end through the UI: 5% capped at $5 on a $40 pot pays $38 and banks $2; four stacks plus the cut come to exactly the $800 bought in; the bank check reads balanced. With no-flop-no-drop on, a hand folded round pre-flop pays the full $15 and takes nothing; with it off the same hand pays $14 and takes $1. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
8aa90e90a4 |
Give a phone on its side the table shape it needs
The last two rounds left one thing failing and said so: a landscape phone with six or twelve seats stacked two of them 25px apart at each end of the felt, with pucks 64px tall. It has been there since seats were first spaced by distance rather than by angle. Three things were wrong at once, and it needed all three. The rim is the wrong shape when the table is very flat. An ellipse 616px wide and 96px tall has nowhere to put the seats near its ends: the curve doubles back on itself, so two seats a long walk apart come out one above the other. Past two and a half times wider than tall the rim is now a stadium — two straight rails with a half-round cap at each end, which is how a real poker table is built and exactly why it is built that way. A cap is a half-circle of the felt's own height, so an end takes one seat and the rest is rail. Only past that ratio: measured, the cut-off lands between a landscape phone at 4.4 and every other layout the tool has, 1.45 at 1280x900 and 1.0 to 1.4 upright, so no working table changes shape. The ring had less height than the felt did. The seat layer is inset by half a puck to keep the outermost seats on screen, and 26px off the top and bottom of a 96px felt left 44px to work with — less than half the table, which is why no rim shape could have fixed it alone. The inset is a percentage where the felt is short. The landscape branch never had that rule at all, because it is matched on height while the inset lives in a width query, so an 844px-wide phone on its side drew its ring against the felt's whole box; with a flat-topped stadium rail that put a dealer button over Resolve Pot in the header. And the puck was a line too tall. In the tight state the hole cards give up their row: of the four things on a puck, the cards are the one the dealer is holding in their other hand, and the seat editor still shows them. That took the puck from 64px to 55px, which is what finally cleared the ends. Measured across 36 combinations of nine viewports and 2, 6, 9 and 12 seats: the closest two seats at 640x360 go from 25px apart to 55px, and every combination is clean. Then 216 audits across thirteen viewports, three seat counts and both a mouse and a thumb: no failures, no console errors. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
dc5db16ffd |
Let the phone table use the width it has
Last round capped the felt so the ring would clear the action bar, and capped the wrong dimension. The wide layout sizes the felt from its width, so capping the width is how you shorten it there — on a phone the felt is already the full width of the column, and capping that threw away 96px of a 360px screen: a 240x240 table inside a 336px space, with nine pucks crowding it. A max-height instead leaves the width alone and lets the aspect ratio give way, which turns a short screen into a wide oval — the shape a poker table is. At 360x640: 336x240 rather than 240x240, same clearance over the bar, and the twelve-seat ring that was crowded at 240 comes out clean, which closes the one combination left failing last round. Where the room isn't the binding constraint, 390x844 and up, nothing changes and the table stays round. The landscape branch now says max-height: none, because it already spends the same measured room on its own height and applying it twice took the felt to the floor value. The measurement that decides whether pucks shrink was also wrong, in a way that only showed up once it was asked a hard question. Rim length per seat measures the walk between two seats, not the gap between them, and on a long oval the rim doubles back at the ends: twelve seats at 640x360 had 193px of rim each and sat 60px apart. It now measures the closest two neighbours directly, off the ring itself — and off the seat layer rather than the felt, because the layer is inset by half a puck on a phone and the felt's box put the answer out by 30px. That makes the rule geometric rather than about screen size, so it is no longer scoped to a phone media query: a wide landscape felt crowds at its ends while its rim claims there is room, and now it is told so. Pucks still touch at nine and twelve seats on a small screen. Tried 46px against 58px: on a roomy phone 46 cleared every overlap where 58 left two, but in a hand with a side pot, where the bar has grown and the felt has shrunk to suit, 46 overlapped four times and 58 twice. That is the felt being small, not the puck being wide, and paying for it with an ellipsis on every long name buys nothing. What has to hold is that each seat keeps its own centre, and that is measured at every size. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
b9b2237dbe |
Run the two checks the audit had and never used
runAll() did not include the contrast check — it simply wasn't in the list — and
touch targets sat behind an opt-in flag nothing passed. So "the audit is clean"
has meant nine checks rather than eleven for a long run of rounds, and the two
left out are the two whose faults a dealer meets in a dim room with their thumb.
Both were written, tested, and then never called; the contrast checker was built
across two rounds and every sweep since has been silent about colour.
The standalone runner had a second version of the same hole: the 44px rule
stands itself down where `hover: hover` says there's a pointer, and a headless
desktop browser is a pointer, so a single pass had never measured a touch target
at any viewport. It now runs every viewport twice, once with a mouse and once
with a thumb.
A check that decides it doesn't apply now says so, rather than reporting an empty
offenders list — which reads exactly like a pass.
Faded text is measured instead of filed as unchecked. `opacity` on a box fades
the box and its text together onto whatever is behind it, so a dimmed seat is not
grey text on a dark puck: it is a faded puck and faded text both landing on the
page. Both halves are composited that way now. Two exemptions remain and both say
so out loud: a disabled control, which WCAG itself excuses, and a link inside a
sentence, whose size is set by the line it sits in.
What running them found, and this round fixes:
- the chair number on every seat: #8a8a8a at 9px, 3.39:1 once the empty seat's
fade is composited, on the label that answers "which seat is that?";
- Quick Split's amount field at 37px tall and its delete button at 30px across,
on a page whose targets had never been measured;
- and on a 360x640 phone with the bar grown by a side pot, the bottom seat
sitting 38px inside it.
That last one brings back a cap this project measured and rejected once: capping
the felt to the room left fixed the six-seat ring and broke the nine-seat one.
That trade was real then and isn't now — seats are spaced by distance around the
rim rather than by angle, so a shorter felt no longer piles them up. Re-measured
across 30 combinations of five phone sizes and 2 to 12 seats: every one clean
except twelve seats at 360x640, where a 240px felt and a 64px puck cannot both be
true. Where the rim can't give each seat a puck's width, the pucks come down a
size, measured from the rim itself rather than from a media query.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
fc866597eb |
Deal the straddle, instead of leaving the dealer to fake it
A straddle is a blind raise put out before the cards by the player left of the
big blind, for twice it. Home games run them constantly, and the tool had no
idea they existed — so a dealer's only way through was to deal the hand and have
the straddler "raise" to $4, which gets the money right and the other two rules
wrong.
There are three at once:
- the amount doubles as it goes left, and re-straddles carry on doubling;
- first to act is left of the *straddler*, not left of the big blind, and the
straddler keeps the option when it comes back round even if nobody raised;
- the minimum raise is twice the straddle. Measured off the big blind
underneath it, a $2/$4 game prices the next raise at $6, and every player at
the table will argue — correctly.
Tap Straddle on the deal prompt and the tool posts it, puts the right player on
the clock and prices the raise. The seat wears an STR badge for the hand, read
off what the deal actually posted rather than off the count, so the badge can't
outlive the chips it stands for. It's a one-hand decision, like posting in, and
it clears when the button moves.
The control sits on the deal row rather than a line of its own: measured at
390x844, its own line cost 44px of felt on every deal prompt, straddle or no
straddle, and this bar is docked over the seats. Three buttons across a 360px
phone want to wrap, which made the bar 64px taller and put the bottom seat three
fifths behind it — this layout deliberately doesn't shrink the felt to suit, so
the deal button is given a 7rem floor instead, which keeps the row to one line
and the cost to nothing.
20 new tests, checked by breaking each of the three rules on purpose first: the
wrong first-to-act, the minimum measured off the big blind, and a flat rather
than doubling chain each fail a different one.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
b97453fba1 |
Seat the table properly on a phone held sideways
Landscape is the one shape this layout has never really fitted, and the audit was calling two separate faults there. The felt worked its own height out as `100vh - bar - 7rem`, guessing that everything above it came to 7rem. That is 108px at 640x360, which the guess covers, and 119px at 844x390, which it doesn't — so on an iPhone on its side the bottom row of seats sat up to 26px inside the action bar. The measurement it needed was already being published for the wide layout: the bar's real height, the wrap's real offset and the bottom seat's overhang, all taken off the window. It uses that now. The floor under it drops from 220px to 96px, because 220 was not a size a 390px-tall window has to give — holding out for it was itself how seats ended up behind the Fold button, which is the one thing the floor exists to prevent. That still left the header taking two rows, and the second row is 44px of the 390. The title block and the five buttons came to 829px inside a 765px header, missing one line by 64px. The page title goes — the one thing on this screen a dealer already knows — hidden from the eye rather than removed, so the heading is still there for a screen reader. The buttons tighten across but never down: they stay a 44px thumb target. The seats themselves were spaced by angle, which is only even spacing on a circle. On a 601x126 felt, ten seats at 36° apart put four of them down each steep end, 20px apart, with pucks 72px tall — a seat's centre belonged to its neighbour, so tapping a player got the wrong one. They now walk the rim by distance instead, which puts them along the two long rails the way a real table seats people. Closest neighbours go from 33px apart to 59px. Measured at 640x360, 667x375 and 844x390 with 9-, 10- and 12-seat tables: every combination the audit used to fail is clean, bar a 10-seat table at 640x360, where two seats at the end of a rim that flat still overlap. Portrait, tablet and desktop are unchanged — the floor never bound there, and on a rounder felt walking the rim gives back the same angles it always did. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
419852a1f4 |
Let a dealer take the table off the device it's stuck on
The night lives in one browser's localStorage and nowhere else. A dealer whose phone dies, or who wants to carry on from the tablet after the break, loses the stacks, the buy-ins, where the button is and the whole hand log. The CSV export can't bring any of it back: it records what happened, not where the table stands. The table can now be written to a file and read back. Round-tripped through the real UI, a hand in progress comes back mid-hand — blinds posted, the same seat on the clock, the button where it was. Loading replaces everything, so it asks first and names both sides rather than saying "are you sure": how many players are in the file and when it was saved, against how many players and how many logged hands are about to go. It goes through the same undoable path as the header buttons, so the toast offers the way back, and the confirm says so instead of overstating it. Reading a file goes through exactly the same migration a save from storage does — load() had that logic inline and it is now a function both call. A second, looser path for imports is how a format grows two meanings of the same field. What it refuses, each with something a dealer can act on and never a half-loaded table: anything that isn't JSON, JSON with no seats in it, a save from some other tool that happens to carry a `kind`, and a file from a newer version — that last one because loading it risks reading a field whose meaning has changed, and the dealer would find out from the money. What it accepts anyway: a bare table with no envelope, for somebody who copied the storage value out by hand. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
8a2fc3e540 |
Give undo and the card picker's buttons a fingertip to land on
The first thing the repaired audit found, run on a real touch context at 44px: three
controls a dealer taps on a phone that are too small to tap.
.actionUndo 145x30 the "I recorded that wrong" pill
#cardPickerClearBtn 50x40 wipes a player's cards
#cardPickerCloseBtn 50x40 closes the picker
Undo is the worst of them. It is the control a dealer reaches for having just entered
the wrong action with five people waiting, and it was fourteen pixels short of a
fingertip. The picker's two are short because the touch rule that fixes every other
small button is scoped to .panelBody, and the picker is a sheet — it never reached
them. One of them is destructive.
All three now sit at 44px on touch. Nothing changes on a pointer device.
I tried an invisible hit-area pad on undo first, to avoid spending bar height, and the
measurement said it did nothing: the pill clips its overflow to keep a long label on
one line, and giving it a positioning context made it clip the pad too. Growing the
box is the honest fix, and it turned out to cost nothing anyway — the bar goes from
138px to 152px and the felt stays at 366px, because the felt had slack against its
220px floor rather than sitting on it as I had assumed.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
b64da7a787 |
Make the layout audit usable inside modals, and stop it passing what it can't measure
Last round turned up that Quick Split had never been audited. Chasing that, the reason
the dealer table's modals had never been audited either turns out to be the harness,
not neglect: it reported the pot wizard covering the seats as ten faults and the
rulings sheet as seven. A modal covering the page is what a modal is for, and an
instrument that calls that a fault is one nobody can use in those states.
It now finds whatever is open on top and scopes the reachability checks to it — the
overlays, plus the phone seat sheet, which is a body class rather than an element. The
seat-ownership check is skipped outright while a sheet is open: seats behind it aren't
fighting each other for taps, nothing is meant to reach them at all.
Two worse faults came out of proving the fix rather than assuming it. Injecting a 20x14
button into the open wizard, the contrast check caught it and the touch-target check
did not:
* effectiveTarget returns null when elementFromPoint can't find the control, and the
check treated that as a pass. Silent skips are how faults hide — the same lesson as
the aria-hidden and faded-text exemptions before this. Unmeasurable controls are
now reported as warnings.
* The probe failed on anything scrolled out of its container. That was hiding a real
control: "Confirm Winner(s)", the button that pays the pot out, had gone unmeasured
for thirty-six rounds. It is scrolled into view before probing, and passes.
A checkbox inside a label is now measured by its label, since tapping the label is what
activates it. Without that, the dedicated-dealer toggle read as an unprobeable 24px
target when it has a 335x44 label around it.
The tool itself came out clean: no failures and no warnings across the table, the pot
wizard, the rulings sheet and the seat editor, on a real touch context at 44px. The
value here is that those states can be checked at all from now on — verified by
injection, which the fixed check catches and the old one did not.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3be93cdadf |
Chop a side pot on the screen that worked it out, and fix Quick Split's contrast
Quick Split lists the pots when somebody is all-in — main, sides, uncalled — and then
leaves the dealer there. Two players tying for a side pot is ordinary, and working out
each one's share meant switching to the other mode and retyping an amount the screen
had just calculated. Retyping a number with money on it is a transcription error
waiting to happen, and the number was already on screen.
Each pot with two or more people who can win it now carries a Chop, opening underneath
its own row: as many ways as there are contenders by default, a stepper for a chop
between fewer of them, and the same odd-chip and dust wording the main split uses,
because it is the same rule and the dealer shouldn't have to notice they are reading a
different screen. Per-row state, so opening the main pot's chop doesn't disturb what
the side pot below it says. Not offered on the uncalled row, which goes back to one
player, nor on the total.
Separately, this page had never been through the layout audit the dealer table gets
every round, and it was carrying nine contrast failures:
.unevenCurrency 4.11:1 the $ beside every amount typed
.unevenRemoveBtn 3.66:1 the ✕ that deletes a player's entry
.splitFootnote 3.94:1 the instruction for what to do with the pot list
All under the 4.5:1 small-text minimum, the worst of them on a destructive control.
Lifted to pass. The large $ on the even-mode pot input is left alone: at 1.6rem it is
large text, where 3:1 is the bar and 4.11:1 clears it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
40fa16920b |
Offer bet sizes the table's chips can actually make
Half the pot lands on a half chip constantly. Pre-flop at 1/2 the honest half-pot is $4.50, and the smallest chip in the default legend is a dollar — the tool's own chip breakdown calls it "4×$1, $0.50 no chip covers". The button offered it anyway, so the dealer rounded it themselves every single time. The pot-derived quick amounts now round up to something the table can build. Up, not down or nearest: these are bet sizes, a pot-sized bet that has been shaved is no longer a pot-sized bet, and rounding up cannot drop an amount below a legal minimum the way rounding the other way can. Only when the legend describes this game. A smallest chip larger than the big blind means the denominations were never set for these stakes, and rounding a 5c/10c table up to whole dollars would be the tool doing real damage on the strength of a field somebody forgot to fill in. Those tables keep exact amounts, and the wrong legend stays visible rather than being quietly applied. A table with no legend at all is told nothing about chips and so has nothing applied either. Verified across five tables: default dollar chips at 1/2 give $4/$5/$7, cent chips at 5c/10c keep $0.20/$0.23/$0.35, five-dollar chips at 5/10 give $20/$25/$35, and both the stale-legend and no-legend tables are left exact. The minimum raise is deliberately not rounded. It is what the rules require, and moving it to suit the chip tray would change what is legal rather than what is convenient. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
09eac536b5 |
Make the pot button offer a pot-sized raise
The raise pad worked its quick amounts out as "the current bet plus the pot as it
stands". That leaves out the raiser's own call, which is in the pot before the raise
is measured off it, so every amount came out short. Measured pre-flop at 1/2:
offered: Min $4 ½ pot $4 Pot $5
correct: Min $4 ½ pot $4.50 Pot $7
A pot raise there is: call the $2, which makes the pot $5, then raise $5. And the
half-pot was so short it clamped up to the minimum and sat next to the Min button
showing the same $4 twice, which is how it went unnoticed — it looked like a table
where those two happened to coincide.
"Pot" is not a suggestion. It is a definite number players say out loud, and a dealer
tapping the button that claims to be it should be recording what was actually said.
The rule moves into Betting.potRaiseTo, next to the other rules and testable on its
own, rather than living as arithmetic inside a DOM renderer:
raise to = the current bet + (pot + what they owe) × fraction
Facing no bet it comes out as a plain pot-sized bet, because both the bet and the
amount owed are zero. Facing a bet it comes out as three times the bet plus the dead
money, which is the shape a player checks it against.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
8efc14f61f |
Show the banker the amount to count out, not just whether the player is up
The standings had a column headed "Cash out" and it did not hold an amount to pay.
It held the difference against the player's buy-in. Measured on a table where Ann
bought in for $200 and sits behind $340:
PLAYER NET CASH OUT
Ann $0 +$140
She is handed $340. The column called "Cash out" says $140, under a heading that
invites reading it as the payout, at the exact moment somebody is counting money onto
the table. The number a banker's hands actually follow — what the player is holding —
was computed all along and written to the CSV, and never shown on screen.
Three columns now, and they say what they are: Player, Chips, Up/down. Chips is a
plain amount and never signed, because it is a quantity of money to hand over. Up/down
keeps the sign and the colour, because it is a result. The two differ by exactly the
buy-in, which is asserted rather than assumed.
Chips counts what is out in the middle for the hand in progress as still theirs, for
the same reason the up/down figure does: the panel is read mid-hand constantly and a
stack that drops by the size of a bet looks like money going missing.
Net — won less paid in, from the hand log — comes off the screen to make room. It is
the weakest of the three: it only moves when a pot is resolved through the wizard, so
at a table where the dealer settles pots by hand it reads $0 all night beside two
columns that don't, and up/down already answers "how are they doing". It stays in the
CSV export, which loses nothing.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
c70cb150db |
Stop telling a brand-new table its players are broke, and give it a way out
Following on from seating the table: a dealer who names six people and hasn't yet
said what a buy-in is gets told
Nobody at the table has chips left. Add a buy-in to deal.
with no buttons on the bar at all. Both halves are wrong for that table. "Left" says
the chips were had and lost, which sends the dealer looking for a rebuy — and a
blocked prompt with nothing to press is a dead end, when the fix is one field in the
sidebar the message never pointed at.
A table where nobody has ever bought in is now told what is actually true, and given
the way out:
Nobody has bought in yet. Set what everyone starts with, or give each seat
their chips. [ Set starting stack ]
The button opens Table Setup, scrolls the starting stack into view and selects it, so
the next thing typed is the answer. A seat with no stack of its own falls back to the
table default, which is why one number is enough to seat everybody's chips at once.
A table that genuinely busted out is a different situation and still says so, with no
starting-stack button — there the fix is a rebuy, not a default.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
e9bc9afcdb |
Seat a player when the dealer names one, and stop hiding the bar on a full table
The first thing anybody does with this tool is seat the table, and it didn't work. Measured on a fresh table: click a seat, type a name, repeat six times — six names on the felt, six seats still not in the hand, and no action bar at all. No "post blinds and deal", no explanation, nothing to press. Six people at the table and a blank screen with no way forward. Three things were wrong and each hid the next. Naming a chair and dealing that chair in were separate acts. Putting a name on an empty chair is somebody sitting down, so it now deals them in. Only a chair that was empty — no name and nobody in it: renaming someone who is sitting out is a correction, not an arrival, and dealing them back in over a typo would be the tool overruling the dealer. The action bar hid itself whenever no seat was live, which was written for an empty felt and caught a named one too. A table with names on it is not an empty table; it's a table the dealer needs to be told something about. And the seat editor never redrew the bar. It refreshes narrowly on purpose — a full render on every keystroke would take the name field out from under the dealer mid-word — but the bar answers "is there a table here at all", and seating somebody is exactly what changes that answer. Even ticking "In hand" by hand left the bar hidden until something else happened to redraw. Verified the fix keeps focus and the caret where they were, typing a character at a time. For the states that can still reach it — a table where everyone was sat out, a save from before naming seated people — the prompt names them and offers "Deal in all 6" rather than a hunt through six seat editors. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
9f3d0dce4b |
Call who shows first at the showdown, instead of leaving the dealer to remember
Every showdown that isn't a fold needs somebody to turn over first, and it is the one
call at the table nobody volunteers for — everyone would rather see a hand than show
one. The tool held both facts the rule needs, who last raised and who sits left of
the button, and said neither. The dealer worked it out from memory while five people
waited, every hand.
The action bar now names the seat and the reason above the resolve button:
Dee shows first — last to bet or raise.
Ben shows first — checked round, first left of the button.
Dee shows first — last to put money in; nobody could bet the river.
The reason is there so the call can be repeated to whoever argues with it, which is
half of what makes it stick.
Three cases, in the order the rule asks them. Somebody bet or raised on the final
round: they show. Nobody did: first live player left of the button. Nobody *could*,
because the chips were already in: it falls to whoever last put money in, on whichever
street that was — "no bet on the final round" isn't a choice anybody made when there
was no round to bet in, so the seat left of the button hasn't earned it.
Aggression is tracked per street and per hand, because those two answers differ
exactly when it matters. A short all-in counts: not reopening the betting is a rule
about who may raise, and says nothing about who shows. Posting a blind doesn't — a
hand limped and checked all the way down has no aggressor at all, which is the case
that goes to the button.
A matching ruling goes in Common rulings, since the dealer will be asked.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
01f5139740 |
Let the dealer find one hand in the log instead of scrolling the night
The hand log exists for one moment: somebody at the rail says they won a pot an hour ago and wants it looked up. A night runs to sixty hands or more and the panel showed five of them at a time, in a box fixed at 260px whether the dealer was on a phone or a 27" screen with room for twenty. There was no way to ask for a hand — only to scroll and read every row. A filter box over the list now takes a name, an amount or a hand number. Matching is loose on purpose: "fay" whatever case it's typed in, "190" for a $190 pot or a $190 payout, "$1,90" for the same typed badly, "#12" when the dealer knows the hand and wants that exact one. Beside it, how much of the night is hidden — "8 of 60" — since a filter matching one hand otherwise looks like a table that only played one. The list also takes the room it has: clamp(200px, 42vh, 560px) rather than a flat 260, which is seven or eight rows instead of five on a desktop and no worse on a phone. Export stays the whole night, always. A CSV quietly missing the hands scrolled past would be a record with holes in it and nothing on the file to say so — if a filter is up when it's exported, the toast says how many went in. Matching deliberately covers only what the row displays. Searching the full ledger as well was the first attempt, so the hand that broke somebody could be found under the loser's name — but the ledger holds everyone who put a chip in, and measured on a seven-handed night where nobody folded, "Fay" returned 60 of 60. A filter that searches text the panel doesn't show looks broken even when it's right. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
c76ebf4d18 |
Say when somebody is being dealt in for free, and give the dealer the post
Sitting down between the button and the big blind means the blinds have gone past you. The tool dealt those players straight in, silently. Measured: Eve takes a seat, and the next hand she is dealt cards in late position having put nothing in — a free hand, out of everyone else's night a couple of dollars at a time, with nothing anywhere on screen to tell the dealer it had happened. At a table there are two answers and the player picks one: post the big blind and come in now, or wait for the blind to reach them. The tool did neither and named neither. The deal prompt now names anyone about to be dealt in who has not paid for it, and carries the post as one tap. Nothing is forced — a home game that doesn't care can deal as before, and the sentence is still worth having on its own, because the dealer at least knows. The post goes in as a big blind out of that player's own stack, is live like any blind, and is spent at the end of the hand rather than becoming a standing charge. Somebody back from sitting out is owed one the same as a newcomer. The big blind of the hand isn't — they're already paying — and neither is anyone at a table that has not dealt a hand yet, where the whole table is new and nobody has been skipped. Deliberately one line for however many have walked up, not one each: the bar is docked over the felt and the felt has already shrunk as far as it goes on a phone, so every line costs seat area. A line apiece put the bar over two fifths of the bottom seat at 390x844. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
cc060b29b8 |
Keep the big blind moving one player at a time when somebody leaves out of the blinds
Passing the button to the next player is right until a chair between the button and
the blinds empties. Then it pulls the whole blind order forward a place. Measured,
four-handed, button on Ben, blinds Cal and Dee, with Cal cashing out:
tool: button Dee, small blind Ann, big blind Ben
correct: dead button on Cal's chair, small blind Dee, big blind Ann
Ann is never asked for the big blind she owes, and Ben pays one a full orbit early.
That is a real big blind out of a real stack, and it happens every time somebody
stands up out of the blinds — which in a cash game is constantly. It is also the
kind of thing the dealer gets blamed for, because the tool is the thing everyone at
the table is reading position off.
The dead button rule holds the rotation to the seats for one hand instead of to the
players. The button follows the small blind round, which at a table with nobody
missing is the same next-player-along it always was; when that player has gone the
button rests on their empty chair for a hand, and when the big blind has gone no
small blind is posted. Either way the big blind advances exactly one player, which
is the whole point of it.
Worked out from who actually posted last hand rather than from the seating as it
stands, because by the time the question is asked the people it turns on are the
ones who have left. Heads-up is exempt — the button is the small blind there and
there is no position to leave dead — as is a table too sparse to have a rotation to
hold to, and a dealer who moves the button or names a blind by hand overrules it.
Both dead positions are said out loud rather than left to look like a fault: the
deal prompt explains them in a sentence, and the dead button goes hollow and dashed
so the felt agrees with the words.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3149b48c14 |
Remember the pots the dealer throws away, so the bank check stops blaming them
Starting the next hand with money still in the middle destroys it. The tool asks
first, and says afterwards how much went. Nothing wrote it down, though, so when
the bank check came round at cash-out it saw only that chips were gone:
$5 more cash than chips in play. Someone has paid for chips they haven't
been given.
and settle-up told the dealer to fix it before anyone cashed out. Both were
accusing the dealer of a miscount for money the tool itself had removed, at the
dealer's instruction, minutes earlier — the one kind of shortfall that isn't a
mistake, reported as the one kind that is.
Discards are now recorded on the table as they happen (undoably, with the rest
of the Next Hand step), the reconciliation counts them alongside the chips, and
the panel shows a "Taken off the table" line so the number is visible rather
than merely subtracted. A genuine shortfall on top of a discard still reports,
at its true size — the discard accounts for its own $5 and not a cent more.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
cb057b5159 |
Stop the think-clock claiming things it can't know
The clock beside the acting player told two lies, in opposite directions. Refresh mid-hand and it restarts at 0:00. The clock lives in memory, so a reload wipes it, and the bar then says the player has just started thinking when they might have been in the tank for two minutes. It is the one number in the bar whose whole job is to tell a dealer when to nudge somebody, and a refresh reset it to "no need". It cannot know that time -- the action moved before this session existed -- so it does not guess. It shows a dash until the action next moves, and says why if you hover: "The action was already on this seat when the page loaded, so there's nothing to time from yet. The clock starts when the action next moves." The moment the action moves it is measuring something it saw, and it counts from zero again. And past an hour it read "65:03". A table left open on a hand really does get there -- the state survives a refresh by design -- and sixty-five minutes written as though it were minutes and seconds is a number the eye gets wrong. It reads 1:05:03 now. Checked in the browser rather than reasoned about: dealt in this session it counts 0:00 then 0:02; refreshed mid-hand it shows the dash and keeps showing it rather than drifting up from an invented zero; one action later it is back to a real 0:00; and with the clock wound forward it reads 1:05:04 with the slow-tank styling still applied. 5 tests (283 total) on the formatting, including that minutes are padded once there are hours and not before, and that a day reads 25:00:00 rather than wrapping. Also checked and correct already: a backgrounded tab does not lose time -- four minutes away reads 4:01 on return, because the clock is a timestamp difference rather than a count of ticks -- and the timer disappears entirely once the hand is over. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
ab92259e3e |
Put the dealer's own notes in the file they hand over
The CSV export carries the hands, the standings and the settle-up. It carried neither of the two places a dealer types context. Each seat can hold custom fields -- "IOU: $20 to Ann", "Seat rent: paid to 2am", "Venmo: @ann" -- and the table has a notes box for the rest: side bets, a high-hand promo, who owes the box from last week. Both are typed precisely because they matter to settling up, and both were visible only inside the tool. The one place the context was written down was the one place it could not leave. Now the standings rows end with a Notes column carrying that seat's fields, and the table's notes go in a section of their own at the end. Player,Paid In,Won,Net,Bought In,Chips Now,Up/Down,Notes Ann,20.00,60.00,40.00,200.00,150.00,-50.00,Seat rent: paid to 2am; Venmo: @ann Ben,20.00,0.00,-20.00,200.00,200.00,0.00,IOU: $20 to Ann Cal,20.00,0.00,-20.00,200.00,250.00,50.00, Table Notes "High-hand promo until midnight. Mo owes the box $15, from last week." Notes go last so the money columns keep the shape a spreadsheet expects, and the multi-line note is quoted as one cell rather than becoming rows that read as more players. An existing test caught this, correctly "every money cell parses as a number" asserted that every column after the name was numeric, and a Notes column is text by definition. The invariant it protects is real -- the file exists so somebody can total a column -- so it now names the six money columns and checks those, rather than "everything after the first". That is the assertion it always meant. 9 tests (278 total): a seat's fields ride along, the money columns keep their order, a seat with nothing written gets an empty cell rather than the word undefined, a blank field does not leave a stray colon, a multi-line note survives as one cell, no notes means no section rather than an empty heading, and asking for the standings does not change the seats. Driven through the real Export CSV button, not just the function: the downloaded file carries both seats' notes and the table's. Audit and contrast clean, no console errors. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
65f201cdfd |
Show each stack as the chips that make it
The chip legend has only ever read chips *into* an amount -- counting what is in front of somebody. Colouring up at the end of a level is the same question backwards, and it is the one the dealer answers with a rack in one hand: $205 is two hundreds and a five. So is counting a stack out for a player who is leaving. The arithmetic already existed. Payouts have been shown in chips since #73; this points the same function at the stacks. Stacks in chips Ann — $205 2×$100, 1×$5 Ben — $1,337 2×$500, 3×$100, 1×$25, 2×$5, 2×$1 Cal — $40 1×$25, 3×$5 Eve — $88 3×$25, 2×$5, 3×$1 Fay — $500 1×$500 Sits under the chip legend, because that is where the denominations it works from are set, and it only appears when there is both a legend to work from and somebody with chips. A table that never set its chip colours up gets nothing, which is what it had before. A seat on zero is left out rather than listed as nothing. Driven at five sizes across touch and pointer contexts, with a 26-character name and a $1,337,000 stack: no row spills, no page overflow, contrast clean, layout audit clean but for the landscape phone already known from #63, no console errors. Every breakdown checked by hand against its total -- 500+200+75+2 = 777 and so on. The breakdown itself is covered by the twelve tests from #73; what is new here is which seats are listed, which the browser run covers: with a legend and stacks it lists five of six seats, with the legend emptied it disappears, and with every stack zeroed it disappears. 269 tests still passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
d297c1a2c9 |
Close two blind spots in the contrast check
Last change found a folded player's stack sitting at 2.22:1, hidden for twenty rounds
behind an exemption. So I went looking for the other exemptions rather than waiting for
the next one to surface. There were two, and neither is hiding a live fault today --
which is worth saying plainly, because it means this is a fix to the instrument and not
to the tool.
Leaf elements only
if (el.children.length > 0) return; // leaf text only
An element's own text stopped being measured the moment a child was added to it. The
undo control went that way two changes ago, the day a <kbd> was appended to it: its
label became invisible to this check with no sign that anything had changed. Now it
measures an element's own text nodes and leaves the descendants to their own turn,
against their own colours.
aria-hidden as a contrast exemption
The bets layer is aria-hidden because the amounts on the chips are already announced by
the seat and the action bar, and repeating them would be noise. That is a good reason
to hide them from a screen reader and no reason at all to stop checking whether a
sighted dealer can read them -- and the chips are exactly what the felt is read for.
Two different questions were being answered by one attribute.
Nothing is exempt now. If something genuinely decorative turns up, it should say so in
its own words rather than borrowing an attribute that means something else. The
watermark that the exemption was written for is gone as of the change before last.
What the widened check finds
Nothing. Zero offenders at six viewports across touch and pointer contexts, and with
the sidebar panels and the rulings sheet open. Measured from rendered pixels, the two
things that had been invisible are comfortable: the bet chip amounts read 12.65 to
13.74:1 and the undo label 8.06:1.
So the fix is proved by injection instead. Colouring the bet chip amounts #3a3a3a now
reports three offenders at 1.80:1, and colouring the undo control #2f2f2f reports it at
1.13:1. Neither was reported before.
269 tests still passing, no console errors.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
f0cfd24647 |
Make the seats that are out of the hand readable again
A folded player's stack measured 2.22:1.
The contrast audit has been reporting clean for twenty rounds, and it never looked at
these seats: the check skipped anything with opacity below 0.99 as "faded on purpose",
and folded and sat-out seats faded their whole box -- 0.5 and 0.58. Opacity on a box
fades the text you still have to read along with the box you wanted to push back.
Measured off rendered pixels, 11.5px text, where AA asks 4.5:1:
name stack
live 13.28 4.75
folded 4.75 2.22
sitting out 7.00 3.22
A folded player's stack is not decoration. It is what they will have next hand and the
answer to "how much has Ben got?", and at 2.22 it was barely there.
So the box recedes and the text does not: a flatter, darker background and a muted
border, with name and stack colours that measure.
name stack
live 13.28 4.75 (unchanged)
folded 7.60 4.55
sitting out 7.60 4.71
Taking the fade away took the signal with it, which the screenshot showed and the
numbers did not: a sat-out seat became almost indistinguishable from a live one. A
folded seat still says so twice, with a red border and a line through the name, but
sitting out had only faintness. It wears a dashed edge now -- the same thing an empty
chair wears, which is right, because neither is in this hand, and the name and stack
tell the two apart.
The same fault, found once the audit stopped hiding it
The key hints on the action bar are 9.6px and were measuring 2.97 to 3.33 against
their buttons -- a fade chosen for looks pushing text a dealer is meant to read under
the threshold, exactly like the seats. They are 4.93 to 7.63 now, still quieter than
the labels they sit beside.
And the audit no longer passes over faded text in silence. Something invisible is still
exempt, since there is nothing to read at zero opacity, but anything in between is
reported as unmeasured -- a number nobody checked is not the same as a number that is
fine. It now lists five: the four key caps and the disabled Export CSV button, which is
disabled and therefore genuinely exempt.
269 tests still passing. Contrast offenders zero, layout audit clean, no console
errors; the folded seat's red line-through and the sat-out seat's dashed edge both
confirmed in cropped screenshots rather than assumed.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
95fd46ee99 |
Take the broken wordmark off the felt
The felt carried "TEXAS HOLD'EM" across its middle, and the pot badge sat on top of
it. Measured at every seat count from two to ten, the middle 20% of the word was
covered, so the table read
TEXAS LD'EM
That is not a watermark, it is a rendering fault, and it looked like one.
I tried to move it before removing it. Four placements, measured across six viewports
and two seat counts each, as the share of the word covered by a seat, a bet chip or
the pot badge:
desktop tablet phone phone landscape
1280x900 768x1024 390x844 360x640 844x390
centred (before) 20% 20% 20% 20% 20%
30% down, full size 0-7% 0-12% 43-60% 48-70% 20-23%
20% down, smaller 0% 0% 40-67% 53-70% 40%
78% down, smaller 0% 0% 77-97% 90-100%* 27-37%
* and off the oval entirely at 360px
Desktop and tablet can be made clean. A phone cannot: below 900px the felt is square,
and a square felt with six to nine seats has no clear horizontal band wide enough for
a word. The best phone result of any placement was 40% covered.
So it is gone rather than special-cased per device for a decoration. The felt still has
its gradient, its rim, the inner gold line and its shadow to be a table with, and the
page header says "Texas Hold'em Dealer" a few inches above it.
The reasoning and the numbers are left in the CSS where the element used to be, so
whoever thinks of adding one back finds out what happened first.
269 tests still passing. Seven viewports: contrast clean, layout audit clean everywhere
but the landscape phone already known from #63, no console errors.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
4cc8faf8cb |
Notice when the same table is open twice
Opening the tool in a second tab -- tapping the bookmark again rather than hunting for
the tab you already had -- silently throws away whatever the first one is doing.
Each tab holds its own copy in memory and each save writes the whole table, so the
second one to act wins. Driven with two tabs on one profile:
tab A: deal, and Ann calls stored -> bettingOpen true, committed [0,1,2,2]
tab B: type a name in seat 5 stored -> bettingOpen false, committed [0,0,0,0]
The deal, the blinds and the call are gone, and neither tab said anything. That is the
same silent shape as the save and load failures either side of this change.
The browser already tells us: a storage event fires in every other tab when the key
changes, and never in the one that wrote. So a tab that sees its table move underneath
it now says so, once, with the way out attached:
toast: "This table is open in another tab. Whichever acts last wins." [Reload]
panel: "This table is open in another tab, and both are writing to it — whichever
acts last wins, so work done in the other one can vanish. Close one, then
reload this tab to pick up the table as it stands."
Watched rather than merged, deliberately. Replacing the table under a dealer's hands
in the middle of a hand would be its own hazard, and reloading is the only resolution
that does not throw somebody's work away -- so it is offered rather than taken.
6 tests (269 total): a write from elsewhere is noticed, another app's key and our own
recovery key are not our business, ten writes produce one warning, watching twice
listens once, no listener is not an error, and no window at all is not an error.
Driven with two live tabs: both quiet at first, A deals and B warns with the reload
offer, A stays quiet about its own writes, two more actions in A add no further toasts
in B, and reloading B picks up A's table -- bettingOpen true, committed [2,1,2,2] --
with the warning cleared. Audit and contrast clean, no console errors.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
cfb83a2ba6 |
Don't quietly write over a save that couldn't be read
Last change made a failing save say so. This is the other direction, and it turned out to be worse. load() is well defended -- try/catch, a shape check, sensible fallbacks -- and every one of those paths ends in `return this.createDefault()`. So an unreadable save becomes an empty table. Driven with a save truncated to 70% of its length, the way an interrupted write leaves one: 1762 bytes stored, 1233 after truncating loaded: 0 named seats, hand #1 instead of #34 toast: none. warning: none. anything on the page: no. A dealer mid-session sees an empty felt and cannot tell whether they mis-tapped Reset, whether the browser lost the night, or what. And then the first edit destroys the evidence. I typed a name into a seat and storage went from the 1233 unreadable bytes to 2549 bytes of fresh default state. Whatever a person could have picked out of that JSON -- the names, the buy-ins, the standings -- was gone one tap after the tool decided it could not read it. Now the unreadable value is moved aside to its own key before the fallback, and the table says what happened: toast: "The saved table couldn't be read, so this one started empty." panel: "The saved table couldn't be read, so this one started empty. The unreadable copy has been kept rather than written over." It is deliberately not offered as a restore. Half a JSON document is not a table, and pretending otherwise would be worse than an empty one. It just stops being destroyed. The two warnings share the Session panel line and both can show at once -- a browser having trouble with storage tends to have it in both directions -- with the load message first, because "why is my table empty" is the question the dealer is standing there asking. 8 tests (263 total): nothing stored is a new table rather than a failure, a good save loads clean, a truncated one is reported, valid JSON of the wrong shape counts too, the copy is kept, a later save does not touch it, the listener fires once across three reads, and storage with no room to keep the copy is not a crash -- which is exactly the storage most likely to hand back half a value in the first place. Driven through the UI: warning and toast on a corrupted save, the set-aside copy still 1233 bytes intact after editing a seat, and nothing shown at all on a healthy table. One audit correction rides along. The collapsed-column check measured inside hidden elements, so the standings header -- hidden with its table until somebody buys in -- was reported as three collapsed columns on every empty table. It skips hidden subtrees now, and still catches a real one: squeezing a visible column reports the header and all three rows. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
4071be5329 |
Say so when the browser refuses to save
Every save goes through one try/catch, and the catch was empty:
} catch {
// Storage full/unavailable (private browsing, etc.) — the table still works
// for the rest of the session, it just won't survive a refresh.
}
The comment is accurate and the behaviour is right -- the chips have already moved in
memory and refusing to continue would be worse. What was missing is anybody being
told. Driven with setItem throwing: two saves failed, no toast, nothing on the page,
and a refresh came back to before the hand was dealt with the dealer none the wiser.
That is the same shape of fault the bank reconciliation exists for. Silent until it is
too late to act on, and the way you find out is the last two players cashing out.
Now it says so twice, in the two places that suit the two timescales.
A toast the first time, and only the first time -- a dealer taps this bar thirty times
a hand and does not need thirty warnings. Measured: four failed saves, one toast.
"This table isn't being saved — refreshing would lose it. Keep this tab open."
And a standing line at the top of the Session panel, because a toast is a moment and
this is a fact about the rest of the night. The raise pad dismisses toasts, and the one
warning that must not be missed is not the one to leave to a toast.
"This table isn't being saved — the browser refused. It'll keep working, but
closing or refreshing this tab loses the session. Export CSV below keeps a copy."
Amber, like the bank check's disagreement, because both mean the same thing: your
record of the night is wrong and you can still do something about it. The export is
named because it is the thing that helps.
6 tests (255 total): a good save reports success and leaves no mark, a failing one
reports failure rather than throwing, the listener fires exactly once across twenty
failures, it is handed the error so the reason can be reported later, a failing save
still does not stop the table, and no listener at all is not an error.
Driven through the UI with setItem throwing: hidden while saving works, toast on the
first failure, zero further toasts across three more actions, the panel line present
and readable. Contrast clean, layout audit clean, no console errors.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
8a7d7b09bf |
Print the two shortcuts a dealer had no way to find
Seven keys drive this tool. Five of them are printed on the control they press -- F on Fold, C on Call, R on Raise, the return arrow on Deal and on Raise to, esc on Cancel. Two were printed on nothing at all: A opens the amount pad on All-in U takes back the last action A dealer who learned F, C and R from the buttons had no way to discover the other two existed. They are the two worth knowing: A is the fastest action in the tool and U is the one that saves a misclick. Both are on their controls now. The key lookup matches any of a button's classes, so the raise pad's four quick amounts can share one class for their looks while only All-in carries a key. And a legend in the sidebar, listing all seven with what they do, built from the same list the handlers are keyed off. There is a test that every key in the map has a line in the legend and that every line names a key that really is bound -- a shortcut with no line is one nobody can find, which is the whole bug this fixes. The panel is hidden on a touch device. A phone has no keys to press, and it is the same pointer question the touch-target rules ask rather than a guess about width. One thing I broke and fixed on the way. .actionKey pins itself to the top-right corner of a big action button. The undo control is a 30px pill with overflow hidden and no positioning context, so the U escaped it and floated above the action bar -- visible in the screenshot before I looked properly. It has its own inline class now, checked at six sizes: the cap sits inside the pill and inside the bar, and the pill is not clipped. 5 tests (249 total). Driven at six sizes across touch and pointer contexts: the undo pill shows U, the raise pad shows A on All-in and nothing on Min, half pot and Pot, the legend lists all seven, and it is display:none on a phone. Contrast clean, layout audit clean but for the landscape phone already known from #63, no console errors. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
ebf22e3153 |
Shorten the rulings index to the words a dealer would say
Last change added an index to the rulings sheet and I wrote down what it cost: 342px at the top of a 390px-wide sheet, so a dealer reading straight through scrolls past the whole thing first. That was an honest trade at the time. It is also fixable. The index was labelled with the section headings, which are sentences -- "A short all-in doesn't reopen the betting" -- so nine of them stacked one per row. A dealer hunting for that ruling is looking for the words "short all-in", not the sentence. Misdeal Board card early Out of turn Short all-in Uncalled bet Oversized chip Minimum raise Odd chip String bet phone 342px -> 143px 9 rows -> 3 desktop ~200 -> 70px -> 2 landscape -> 94px -> 2 The first ruling now starts 309px into the sheet on a phone rather than 440. Each short label lives on its own section as data-short, next to the heading it shortens, and the index falls back to the heading when there is not one -- so a ruling added later still appears, still without anyone having to write it twice. The full sentence stays as the button's aria-label, so the short label never costs a screen reader the heading. Checked at four sizes across touch and pointer contexts: nine labels, nine aria-labels, every chip 44px on touch, jumping still lands and focuses the right heading, contrast clean, no console errors. 244 tests still passing. Also looked at, and left alone: the card picker refuses a card already dealt -- disabled, titled "Already dealt to Ann" -- and every one of its 52 buttons clears 44px. A messy nine-seat table with 25/50 blinds, two folded players, a busted seat, accented and hyphenated names, a $125,000 stack and a filled notes field audits clean at every size but the landscape phone already known from #63. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
f0994a8aed |
Get to the right ruling in one tap, and cover the string bet
The rulings sheet is opened mid-argument, with players waiting. It was eight rulings of prose in a row with no way in. Reaching the last one meant scrolling: 1.6 screens on a desktop 2.1 on a phone 3.5 on a phone held sideways A dealer looking for the odd-chip rule had to read past seven others to find it. There is an index across the top now, one chip per ruling, and tapping one scrolls its section into view and puts the keyboard and a screen reader on its heading. Built from the sections rather than written out beside them, so a ruling added later turns up in the index without anyone having to remember to add it twice. And the missing ruling I went looking for what home games actually argue about rather than guessing, and the answer that kept coming back alongside misdeals -- which were already here -- was the string bet. It was the one common dispute the sheet had nothing to say about. Betting in one motion - the string bet A bet is whatever goes in on the first trip to the stack, unless the amount was said out loud first. Reaching back for more chips without having said "raise" is a string bet, and only the first motion stands. Saying the number is always enough on its own: "raise to sixty" binds whatever order the chips arrive in. It's the rule home games invoke most and enforce least, and both are fine -- the point is that a player can't watch the table react and then decide how much to bet. Say it, or push it all at once. That second paragraph is deliberate. The sheet's whole stance is that house rules win, so a ruling that tells a home game it must enforce something would be the wrong voice. What it costs: the index is 342px at the top of a 390px-wide sheet, so a dealer who wants to read straight through now scrolls past it first. That is the trade -- it is a jump table you use once and then read, and the alternative was scrolling blind. Driven through at four sizes across touch and pointer contexts: nine rulings, nine index entries, every one of them 44px on touch, tapping the last one lands it on screen and focused. Focus stays inside the sheet across fourteen tabs, Enter on a chip jumps, Escape closes, and opening it three times still builds nine entries rather than twenty-seven. Contrast clean, no console errors, 244 tests still passing. Sources: cardplayerlifestyle.com and pokernews on string bets; the sheet still follows the TDA rules it cites. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
ddb3907ace |
Say the payout in chips, not just in money
"Ann - $135" is the answer to the arithmetic. The dealer's actual job at that moment
is working out that $135 is a black, a green and two reds, while four people watch
them do it.
The chip legend has known the denominations all along. It was only being used to read
chips *into* an amount -- counting a seat's stack -- and never to turn an amount back
into chips.
Final Payouts
Ann - $135
1x$100, 1x$25, 2x$5
Largest first, which is greedy and so not optimal for an arbitrary set of values. Poker
chips are not arbitrary: every real set is canonical -- each denomination divides the
next, 1/5/25/100/500 -- and greedy is exactly optimal on those. A dealer who invents a
3-and-7 chip set still gets a valid answer, possibly one chip longer than it needed to
be, which is a fair trade for not solving a knapsack in a modal.
Two cases where saying nothing is the right answer, and one where it is not.
A table with no chip legend gets the amount on its own -- it has not told us its
denominations, so there is no advice to give. But a legend that exists and cannot reach
the amount does say so: "$3 no chip covers" when the smallest chip is $5. Half a dollar
in a dollar-chip game is the dealer's problem to solve and they can only solve it if
they are told, so it is never rounded away.
12 tests (244 total): the breakdown adds up to the amount across eight amounts, cent
chips make cent amounts exactly, a jumbled legend still pays largest first, the colour
comes along for a future caller, and the two no-advice cases are told apart from the
cannot-reach one.
Driven through a real all-in hand at four sizes: $135 reads "1x$100, 1x$25, 2x$5" and
a tournament-sized $1,350 reads "2x$500, 3x$100, 2x$25", fitting without spill down to
360px. With the legend emptied the line disappears and the payout stands alone.
Contrast clean, no console errors.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3fe16fa7f2 |
Let a phone move a player to another seat
The hint under the table has said this on every screen since the gestures went in:
Tap a seat to edit - double-tap to fold - press and hold for quick actions -
drag to swap seats.
Three of those work on a phone. The fourth never has. Swapping seats is HTML5
drag-and-drop -- draggable="true", dragstart, drop -- and a touch screen does not
generate drag events at all. So the one clause written in the same breath as "tap"
and "press and hold" was the one thing a thumb could never do.
I found it by dragging seat 1 onto seat 4 with real touch pointer events and watching
nothing happen: no swap, no menu, no error.
Moving a seat is in the long-press menu now, which is the route a thumb already takes
for everything else. "Move to..." replaces the action list with the other chairs, by
number and name, and picking one makes the move:
Move Ben to...
Their chips, cards and bet go with them. The button and the blinds stay on the chair.
Seat 1 - Ann Seat 3 - Cal Seat 4 - Dee Seat 5 - empty ...
That subtitle is the rule, and it is the one a card room uses: the whole seat travels,
while everything naming a position stays put. Somebody who sits down in the big
blind's chair is the big blind.
Driven through on a phone: Ben, small blind, $109 behind with $1 posted, moves to
seat 5 with both numbers; seat 2 goes empty; the button stays on seat 1; the toast
reads "Ben moved to seat 5" and undo puts it all back. Reopening the menu on another
seat shows actions again rather than the chair list it closed on.
The hint tells the truth per device now -- dragging is only mentioned where a mouse
exists. Dragging still works there, checked.
8 tests (232 total): the whole seat moves, the bet moves with it, chips are conserved,
the button and the blinds stay on their chairs, swapping twice is a no-op, and a chair
that is not there is refused rather than swallowed.
One audit fix rides along. The "one dealer button" check counted the empty slot that a
hovered seat deliberately fades in, so it reported two buttons whenever a mouse
happened to rest on the table -- which is most of the time a mouse exists. It ignores
the hovered seat now, and still catches the real fault: with the pre-#62 behaviour put
back it reports six, or five while hovering.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
f199148046 |
A pre-filled amount is a suggestion, so reaching for the box replaces it
Every amount box in this tool opens pre-filled: the raise pad offers the minimum, Add chips offers a buy-in, the seat editor offers the stack a player already has. A dealer taps one because they want a different number -- and a tap puts the caret after the suggestion rather than over it. Driven on an emulated phone: raise pad shows 4 tap, type "60" -> 460 add chips shows 200 tap, type "50" -> 20050 seat stack shows 200 tap, type "175" -> 200175 The raise pad is the most time-critical control in the tool, and 460 is a number the dealer never said. The all-in warning happens to catch that one against a $200 stack; against a deeper stack it is simply accepted, and the pot is wrong from then on. Number boxes now select what is in them when they take focus, so the first digit typed replaces the suggestion. raise pad 4 -> "60" add chips 200 -> "50" seat stack 200 -> "175" a name Cal -> "CalX" That last line is the point of the rule. Only number boxes: type=number, or the type=text/inputmode=decimal pair the raise pad uses to get a keypad without spinner arrows. A name or a note is a thing you edit, and selecting all of it on focus would be the same mistake pointing the other way. Delegated from the document rather than wired per field. The raise pad, the add-chips sheet and the seat editor all rebuild their inputs on every render, and a box added later should not have to remember to ask for this. The select is deferred a tick. Doing it inside the focus handler loses the race with the browser placing its own caret, and the selection collapses again the moment the tap completes. 10 tests (224 total) on the predicate, including the ones that must stay false -- name, note, colour swatch, checkbox, select, and an input with no type stated. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
38536a5c0c |
Ask about the thumb, not the window width
There is a careful touch-target pass in this stylesheet -- 44px fields, 44px delete buttons, a padded hit area for the rulings link. Every rule in it sits behind `max-width: 700px`. A phone held sideways is 844px wide. So none of that work applied to the device it was written for, half the time. Measured on an emulated touch device with the sidebar open -- twelve controls under 44px at 844x390 and again at 915x412: the rulings link 91x14 the dedicated-dealer box 13x13 chip-colour delete 28px wide blinds-direction select 32px tall five panel headers 38px The rules are restated against `pointer: coarse`. The width-scoped block keeps its layout changes -- one column, stacked dialogs -- because those really are about how much room there is. This is only about what a thumb can hit, so it asks about the thumb. 844x390 touch 12 controls under 44px -> 1 915x412 touch 12 -> 1 390x844 touch 6 -> 1 360x640 touch 6 -> 1 The one left is the bare 24x24 checkbox, whose row is 44px tall -- the row is the target, which is the existing design and the only thing that works for a checkbox. Two things were too small on every device, not just touch, and are fixed outright. The panel headers -- the sidebar's entire navigation -- were 38px everywhere, phones included; the pass below never reached them. And the rulings link was 91x14, which misses the 24px minimum with a mouse as well as with a thumb. Under 24px anywhere is now zero, at every size measured. Two traps worth recording. A <summary> only draws its disclosure triangle while it is display: list-item, so the headers get there on padding rather than a flex box. And a <summary> is content-box, so `min-height: 44px` was *added* to the padding and gave 71px -- the same box-model trap as the header buttons one PR ago, in a different tag. Both are stated in the CSS where the next person will hit them. 214 tests still passing; layout audit and contrast clean at six sizes across touch and pointer contexts; no console errors; panels still toggle and the triangle is still there. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
286a924fe4 |
Give buttons and links the same box
The header on a portrait phone was 227px -- a quarter of the screen -- before the
table started, and at 360x640 the bottom of the table sat 16px behind the action bar
with the gesture hint off screen entirely.
Chasing where the pixels went turned up something better than a tight header.
.holdemBtn is worn by three <button>s and two <a>s. The class never stated a box
model, and the browser does not give those two tags the same one: Chromium makes a
button border-box and leaves a link content-box. So `min-height: 46px` on the header
row came out as 46px on the buttons and 46 + 16 padding + 2 border = 64px on the
links -- and a flex row is as tall as its tallest item, so all five became 64px.
A 46px touch target was costing 64px of screen, twice over, because the header wraps
to two rows on a phone.
390x844 header 227 -> 191 buttons 64 -> 46
412x915 header 227 -> 191
430x932 header 227 -> 191
360x640 header 173 -> 137 table clearance -16px -> +20px, so the felt now
clears the action bar instead of running under it
The other half is the one I did not go looking for. The 46px floor only exists under
the 700px breakpoint, and the accidental stretch was the only reason anything above
it was big enough to press. Once the boxes agree, a landscape phone -- wider than the
breakpoint, still a thumb -- would have been left with 42px buttons. So the 44px
floor is stated on the class itself. Measured across the page:
844x390 13 .holdemBtn under 44px -> 0
915x412 13 -> 0
1280x900 13 -> 0
That is every button on the page, not just the header: the panel buttons, the pot
wizard, the confirm dialogs.
What it costs: 844x390 loses 2px of table clearance, -5 to -7, because its header
grows with the buttons. That size already has the bottom seat behind the bar (#63),
and 13 unpressable controls was the worse of the two problems.
214 tests still passing; layout audit and contrast clean at 9 sizes; no console
errors; every header button still hit-testable at 46x121 and 46x112.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3e9e2e899d |
Stop the big blind's own chip from covering the BB badge
On a phone the badge marking the big blind was not hard to read. It was not there.
The chip showing their $2 sat on top of it.
Both live on the same spoke. Bets sit between a seat and the pot, so on the bottom
half of the ring the chip comes down onto the seat's top edge, which is exactly where
the badge was -- and the chip is a layer above it.
390x844, six seats: badge 24x12 = 288px², chip covered 196px² of it
badge z-index 2, chip z-index 3
the badge's own centre hit-tested as the chip
So seats on the bottom of the ring wear their badge underneath instead, on the edge
the pot is not on. And badges now outrank chips regardless, so a layout this does not
anticipate cannot hide one again: which blind a seat is posting is the one thing on
that puck nothing else on the felt repeats, while a bet chip's amount is on the seat's
own total line as well.
Measured across 20 table sizes -- badge area lost to bet chips:
430x932, 390x844, 360x640 at 2 and 6 seats 35% -> 0%
8 seats 4-12%, unchanged: the blinds land
on the top arc there, where the
badge was already out of the way
desktop and tablet 0% either way, more room
Two other placements were measured and rejected. Tucking the badge inside the seat's
bottom edge put 38-42% of it on the seat's own stack line at every size including
desktop -- trading a chip covering the badge for the badge covering the money. Leaving
it on top and only raising its z-index worked everywhere, but leaves the badge sitting
on the chip rather than clear of it; the z-index is kept as a backstop, the move is
what does the work.
What it costs
At 360x640 heads-up the bottom seat's badge moves down into the action bar, where it
was visible before. That seat is already substantially behind the bar at that size --
the narrow layout reserves scroll room rather than capping the felt, which #63
measured and settled on -- but this change is what put the badge there, so I am
saying so rather than leaving it to be found. Every other size and seat count is
clean, including 360x640 at six and nine seats.
An instrument note, because it nearly became a finding. Bet chips are
pointer-events: none above 900px and auto below it, so elementFromPoint reports them
as covering things on a phone and as invisible on a desktop. The 35% figures above
are geometric overlap and a cropped screenshot of the rendered pixels, not hit tests.
Three "chip covered by DIV" results I saw earlier were that quirk and are not real.
214 tests still passing; layout audit and contrast clean at 8 sizes x 3 seat counts;
no console errors.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
96cd7684f0 |
Buying chips puts you back in the game
Handing over cash and being dealt in are one event at a real table. Here they were two, and the second one was easy to miss. Last change deals a busted player around until they rebuy. So the dealer takes their money, taps Add chips, and: Ben: inHand false -> false stack 0 -> 100 buyIns 100 -> 200 toast: "Ben added $100." He has chips, he is not in the game, and nothing on screen says so. He gets skipped hand after hand until somebody notices and finds "Deal in" in the seat menu. Now the buy-in does both, and says which: mid-hand: "Ben added $100 - in from the next hand." inHand true, folded true between hands: "Ben added $100 - dealt in." inHand true, folded false Folding him for the hand in progress is the rule "Deal in" already follows -- you can't buy into a pot you have not been paying for -- so the two doors into the game behave the same way. It is one undoable step: undo takes back the chips, the cash record and the seat. Not for a seat that was sat out while holding chips. That is a dealer who sat someone out on purpose and is topping them up, and dealing them back in would be the tool guessing at something it was not told. The test reads the seat's own recorded stack rather than stackOf(), which substitutes the table's starting stack and would make a seat with nothing recorded look like it was holding chips. busted, sat out -> back in new chair, no stack yet -> in sat out holding $100 -> stays out already in the hand -> nothing to do, including an all-in at zero chips 8 tests (214 total). Driven through the UI: the rebuy, the toast, the next hand actually dealing him in (4 players, live 4), and undo putting all three fields back. Found on the way, not fixed here Buying in on a seat with no stack of its own credits the table's starting stack *plus* the buy-in. A new player handed $100 on a table with a $100 default ends up with $200 of chips against $100 of recorded cash. I am leaving it because the right answer is genuinely unclear and it is not mine to pick. A blank stack means "no chips recorded" to the bank check, which counts it as zero, and "the table's starting stack" to the betting code and the felt, which show it as $100. Both are defensible for their own job, and which one a buy-in should follow decides whether a casual dealer who never types stacks gets $200 or $100 after a rebuy. Worth saying out loud rather than quietly picking one. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
1af82dfddf |
Call a bet a bet and a raise a raise
The bar decided which word to use from what the acting player still owes. That gets
the big blind's option backwards -- they owe nothing, so it offered "Bet", but their
own blind is a live bet and putting more in is a raise. And it gets the other half
wrong too: open the pad to make a first bet on the flop and the confirm button said
"Raise to", then wrote "Ann's raise to $10" into the undo control for it.
preflop, action back to the big blind
was: Fold | Check | Bet pad: "Raise to"
now: Fold | Check | Raise pad: "Raise to"
flop, nobody has bet
was: Fold | Check | Bet pad: "Raise to" undo: "Ann's raise to $10"
now: Fold | Check | Bet pad: "Bet" undo: "Ann's bet of $10"
The arithmetic was always right -- minRaiseTo returns two big blinds for the big
blind's option and one for an opening bet, and I checked both before touching
anything. This is only what it is called. For a tool a dealer announces the action
from, that is not a small thing: a dealer who says "bet" when the big blind puts more
in gets corrected by the table.
The test is whether there is a live bet to raise, not whether this player owes
anything, and it is one helper rather than a condition written out five times.
The warnings follow the same word. Under-minimum with a bet in front of you leaves
you calling it; with an empty pot there is nothing to call:
"$3 is under the $4 minimum. A legal raise is $4 or more -- otherwise it's a
call, or all-in."
"$1 is under the $2 minimum. A legal bet is $2 or more -- otherwise it's a
check, or all-in."
Also the field's screen-reader label, which announced "Raise to amount" for a bet.
8 tests (206 total), including a table playing without blinds -- which opens with a
bet, and is the one preflop case where the old condition happened to be right.
Driven through the UI at seven viewports in both spots: every label correct, contrast
clean, no console errors.
One thing found and deliberately not touched. Fuzzing 40 hands with random actions
and rebuys -- chips stayed conserved to the cent throughout, no negative stacks, and
the acting seat was always a seat that could act -- turned up one disagreement:
isStreetComplete returns false during an all-in run-out where one player still has
chips and hasn't acted, while actingSeatIndex is correctly null. It has no symptom,
because the bar picks its layout from actingSeatIndex and shows "Deal Turn" as it
should. Changing a betting-round predicate with no observable fault behind it is a
worse trade than leaving it written down.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
2cc2c66976 |
Deal around a player who busted and hasn't rebought
The most ordinary thing that happens in a home game: somebody stacks off, and they
don't buy back in that second. The table dealt them in anyway, and the result was a
hand nobody could finish.
Three players, two of them busted:
bar before dealing:
"Pot $0 | 3 players | All-in - running out the board (3 players)
| Cal posts $1, Ann posts $2 | Post blinds & deal"
after dealing:
Cal posted nothing -- no chips to post with
Ann posted $2 and was asked to check or bet against her own money
liveSeats 3, isStreetComplete false, isHandOver false
Every line of that is wrong. It announced a board being run out over a felt with no
cards on it, promised a small blind from a seat that couldn't post one, took it from
nobody, and then sat on a hand with no way forward.
So a seat with no chips and nothing in the pot is dealt around. Their seat, name and
buy-in record are untouched -- sat out, not folded, because they didn't muck anything
-- and adding chips puts them straight back in the next hand.
four players, Ben busted, button on Ann:
"3 players | Cal posts $1, Dee posts $2
| Ben has no chips - sitting out this hand. | Post blinds & deal"
-> Ben inHand false, folded false; blinds exactly as promised; Ann to act
Said before it happens, for the same reason the button move is: a dealer shouldn't
find out from the felt. And the preview computes it on a view of the table rather
than by changing it, so the promise and the deal ask the same question -- the fault
these two lines exist to prevent.
When it can't be dealt at all, it says who has to buy in rather than pretending it's
a seating problem:
"Only Ann has chips - Ben and Cal need to buy in."
"Nobody at the table has chips left. Add a buy-in to deal."
Two smaller things fall out. The run-out banner is now gated on the betting actually
being open -- before the deal, a table of busted players is indistinguishable from a
table of all-ins to that test. And the bar's player count reads the table as it will
be dealt, so someone waiting on a rebuy isn't announced as one of the players.
Not the same as all-in, and the code says so: an all-in player has money riding on
the hand. A seat with no stack recorded isn't broke either -- those fall back to the
table's starting stack.
13 tests (198 total), and 5 of them fail with the sit-out and the run-out guard taken
back out.
Driven through the UI at seven viewports in both states: layout and contrast clean
everywhere but the landscape phone already known from #63, and no console errors.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
a92a389245 |
Work out who pays whom
I went looking for what home-game dealers actually struggle with rather than reading
the code again, and the answer that kept coming up was the end of the night: hosts
reach for a separate app to finish a game "knowing exactly who owes whom in seconds
rather than minutes".
This tool already had every number that answer needs. The standings panel knows what
each player is up or down -- chips in front of them less the cash they handed over --
and then stops. Which leaves the dealer with six signed numbers at 2am and the actual
job still to do. That last step is the part people get wrong, not the poker.
Ann +$75 Dee +$110 Fay -> Dee $70
Ben -$10 Eve -$40 Cal -> Dee $40
Cal -$65 Fay -$70 Cal -> Ann $25
Eve -> Ann $40
Ben -> Ann $10
Largest debt against largest win, repeatedly. Greedy rather than optimal, which is
the right trade here: the true minimum number of transfers is NP-hard, while this
needs at most one payment fewer than there are people and each one it emits clears
somebody out of the list entirely. A dealer wants a short list to read down, not a
proof. Payers are the outer loop so everything one person owes prints together --
the list gets read out one player at a time.
All of it in integer cents. A settlement that has to come out exactly right is the
last place to let a float land on 41.99999999.
When the chips on the table don't match the cash taken
Then the payments can't all be made, and that is reported rather than quietly spread
across them. It is never rounding -- the arithmetic is integer cents -- so it is
always what the bank check sitting right above it is already warning about: a rebuy
nobody wrote down, or a miscount. Inventing a transfer to make the columns agree is
how a real person ends up short.
$100 of winnings has nothing to pay it -- there are more chips on the table than
cash was taken for. Fix that above before anyone cashes out.
The payment list goes into the CSV export too, under the standings it came from,
with the caveat if there is one. Whoever opens that file wants the answer, not to
redo the arithmetic in a spreadsheet.
13 tests (185 total): everyone ends up square, at most n-1 payments, a payer's lines
stay together, cents survive, nobody pays themselves, no payment is ever for zero,
both directions of an unbalanced bank, and one that settles a real table's standings
rather than hand-made numbers.
Driven through the UI at six viewports, balanced and not: clean layout audit and
contrast everywhere but the landscape phone already known from #63. Checked with
26-character names and seven-figure stacks on a 360px screen -- names wrap, amounts
stay whole and right-aligned, nothing overflows.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3d1ce1f45c |
Keep the table above the action bar on wide screens
The bar publishes its height so the layout can stay clear of it. Only the phone and landscape rules ever read it. The wide layout sized the felt from its own width -- a box 72% as tall as it is wide -- and so had no idea what was underneath it. That was survivable while the bar was a constant. It isn't: it grows from 117px to 172px the moment a hand needs a side pot, which is exactly the hand where the dealer most needs to see who is still in. Measured with two side pots, six seats, the bottom seat sat this far behind the bar: 1280x900 22px 1600x1000 54px 768x1024 23px 1024x768 clear 360x640 39px 390x844 clear The first attempt at this didn't work and the failure is worth recording: the wrap was a padding-bottom: 72% box, and percentage padding resolves against the containing block's width rather than the element's own. Capping the width narrowed the felt from 822px to 746px and left it exactly 617px tall, so the seat stayed where it was. It is an aspect-ratio box now, and the shape is stated once as --feltAspect so the height and the cap both derive from it. 1280x900 felt 857x617 -> 746x537 seat 22px behind the bar -> 51px clear 1600x1000 ...760 -> 885x637 54px behind -> 59px clear 768x1024 felt 689x689 -> 603x603 23px behind -> 56px clear Six-seat tables at 1280x900, 1600x1000, 1024x768 and 768x1024 all go from one covered seat to a clean audit. Not applied below 700px. The narrow layout already had an answer -- it reserves the bar's height as scroll room under the table, and a phone is a thing you scroll -- and capping there was measured and rejected: at 360x640 it took the felt from 321px to 226px, which fixed the six-seat ring and broke the nine-seat one, two covered seats traded for two seats whose centre landed on a neighbour. Landscape phones opt out too. They set their own height from the bar already and are sitting on their 7rem floor; 844x390 and 667x375 still have the bottom seat in the bar, and nothing about a 112px-tall table with 73px seats can fix that. The seat overhang is a constant rather than something measured off the layout it is about to change -- a seat is ~73px tall at every width -- so this settles in one pass instead of asking a different question each time it shrinks the felt. 172 tests still passing; no console errors; contrast clean. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
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
|
||
|
|
0c6cd55951 |
Let the landscape table use the width it has
Last round I left a residual and said it wanted its own fix: on a phone held sideways, seats overlapped so badly that a seat's centre belonged to the seat next door. Here's the cause. The landscape rule capped the felt's width by its height to hold the portrait aspect ratio. At 640x360 that produced a table 135px wide inside a 601px column -- 22% of the screen -- with 366px of rim for nine seats needing 662px. Adjacent seat centres ended up 13px apart for seats rendering 74px wide. Landscape has width and no height, so the table now takes the width it has and is only as tall as the room left over. A wide ellipse, which is what a real poker table is anyway. 640x360 felt 135x97 -> 601x121 rim 366 -> 1263 centres 13px -> 33px 844x390 felt 176x127 -> 765x151 rim 480 -> 1605 centres 37px -> 59px 915x412 felt 207x149 -> 492x173 rim 563 -> 1106 centres 44px -> 55px Seat ownership went from 8 seats wrong at 640x360 and 2 at 844x390 to zero at 844x390, zero at 915x412, and two at the very smallest. Nine of eleven sizes clean, portrait and desktop untouched. Two things I tried and threw away Spacing seats by arc length instead of by angle. It sounds obviously right -- even distance around the rim rather than even angle -- and it made things worse: 640x360 went from 2 wrong to 3, closest centres 33px to 21px, and even the desktop table dropped from 180px to 155px. Even arc spacing packs seats through the tight turns at the ends of an ellipse, where a fixed distance along the rim covers much less straight-line distance. Equal angles happen to separate them better. Reverted. Shrinking seats on the tightest landscape phones. Took the rendered seat from 74px to 64px and moved closest centres not at all -- still 33px -- while the covered count at 844x390 went from 3 to 4. A change that measures as nothing but costs CSS and legibility isn't worth keeping. Reverted. So this is one CSS rule, which is all the measurements supported. What's left 640x360 and 667x375 still have two seats whose centre lands on a neighbour. Nine seats on a ring 121px tall is a genuine physical constraint rather than a layout mistake, and both remaining fixes I tried made it worse rather than better. It is reported by the audit rather than hidden, and a dealer on a 360px-tall screen has the seat menu and the action bar for everything that matters. 152 tests passing; contrast clean at every size; dealing still works in landscape. 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
|
||
|
|
e5d5b515ba |
Merge pull request 'Hold'em dealer: nine polish rounds released together' (#57) from feature/casino-holdem-acting-seat into master
Build and Deploy / build-deploy (push) Successful in 1m22s
Releases #49-#57 in one go: pot safety, the banker's cash reconciliation, the short all-in rule, the standings name column, the layout and contrast audits, dealer puck hit area, illegal-raise warnings, and the acting-seat highlight. |
||
|
|
a14087fa7d |
Merge pull request 'Stop rebuilding the hand log after every fold, call and raise' (#48) from feature/casino-holdem-render-perf into master
Build and Deploy / build-deploy (push) Successful in 1m27s
|
||
|
|
76f1952d61 |
Merge pull request 'Make the overlays usable from a keyboard' (#47) from feature/casino-holdem-keyboard-focus into master
Build and Deploy / build-deploy (push) Canceled after 10s
|
||
|
|
f464e6f2e2 |
Merge pull request 'Make big numbers readable, and make the export add up' (#46) from feature/casino-holdem-resilience into master
Build and Deploy / build-deploy (push) Canceled after 5s
|
||
|
|
f5108de288 |
Merge pull request 'Handle the messy table: joining, leaving, and arguing about blinds' (#45) from feature/casino-holdem-messy-tables into master
Build and Deploy / build-deploy (push) Canceled after 0s
|
||
|
|
ee37cf7bd9 |
Merge pull request 'Make every tap in the dealer land instantly on a phone' (#44) from feature/casino-holdem-touch-mobile into master
Build and Deploy / build-deploy (push) Canceled after 10s
|
||
|
|
0d1218fa53 |
Merge pull request 'Hold'em dealer: a test suite, one implementation each, and a pot-announcement fix' (#43) from chore/casino-holdem-tech-debt into master
Build and Deploy / build-deploy (push) Canceled after 0s
|
||
|
|
a72f199139 |
Light up the seat that's holding the game up
Asked what actually slows a home game down and the answer from people who deal them is consistent: not how fast the cards come out, but players not noticing the action is on them. "Waiting for shuffling and players not realizing it's their action is going to cost much more time than how quickly you spread the flop." So I measured how obvious the acting seat is, and it was distinguished by a rim colour and nothing else -- a gold border and a 2px ring where a live seat has a green border and a 1px one. Same fill, same size, nothing moving. That is a subtle thing to ask a dealer to spot across a table on a phone, for the one state the whole pace of the game hangs on. It reads as lit now rather than merely outlined: a solid gold ring at full strength, a warm glow separating it from the felt, a warmer fill, and it sits above its neighbours so the ring is never clipped by the seat next to it. There's also a slow two-and-a-half second breath, which stops entirely under prefers-reduced-motion -- the ring carries the whole job on its own for anyone who would rather nothing moved, and that was checked rather than assumed. The lift is box-shadow, not transform. A seat's transform is what positions it around the felt, so setting one here would throw it off its spot -- the same trap that nearly shipped a 38px seat displacement a few rounds ago. One measurement I'm not claiming: the acting and live fills differ by only 1.1:1, because the change is a hue shift rather than a lightness one. The ring and the glow do the work; the fill just stops it looking like the same chip. Checked at eight viewports: layout and contrast audits clean, exactly one acting seat everywhere, and the lit seat is the same size as its neighbours at every size -- the ring adds no layout. renderAll is 1.8ms with the pulse running, so it costs nothing. Text on the warmer fill still passes contrast at every size. 137 tests passing. Sources: pokerchipforum.com/threads/as-a-dealer-how-do-increase-more-hands-per-hour.114914/, forumserver.twoplustwo.com/27/casino-amp-cardroom-poker/tips-advice-faster-better-dealer-241644/ Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
e0eb0d2079 |
Say something when the raise isn't a legal raise
Web search turned up the oversized-chip rule described as the single most common
cause of floor disputes, and specifically a home-game problem. Chasing that led to
checking what the bar does with an illegal raise, and the answer was: nothing.
With the minimum at $18 and a player holding $200, typing 13 was accepted without
a word. The engine then treated it as a short raise and correctly declined to
reopen the betting -- right for an all-in, wrong for somebody who just misheard
the bet, and either way the dealer was never told.
The raise entry now says what's wrong and still lets it through. This tool records
what the humans at the table actually did, and a dealer overruling the arithmetic
is right more often than the arithmetic is, so Confirm stays enabled in every case:
$13 is under the $18 minimum. A legal raise is $18 or more -- otherwise
it's a call, or all-in.
All-in for less than a full raise. Legal, but it doesn't reopen the
betting -- anyone who has already acted owes the difference or folds.
More than they have. This goes in as all-in for $200.
$10 is not above the $10 already bet -- that's a call, not a raise.
The second one surfaces the rule fixed two rounds ago at the moment it decides
something, rather than only in the rulings sheet.
Two minimums, which is where the first attempt went wrong
The pad already had a minimum, clamped to what the player can actually put in --
correct for the Min button, wrong for the warnings. For a short stack the clamped
minimum equals their all-in, so neither warning could ever fire for exactly the
player the rule exists for. Kept separate now: one for the button, the rules one
for the warnings.
A regression the audit caught, in the same round that introduced it
The note makes the raise pad taller, --actionBarHeight grows, and the toast --
which floats just above the bar since the occlusion round -- got pushed up over
the table. Measured at 360x640: bar 122px closed, 178px with the pad, 217px with a
note, and the toast climbing from y506 to y411, covering seats outright.
A toast is transient news about the action just taken; the pad is the dealer
deciding the next one. The older one gets out of the way when the pad opens --
nothing is lost, its Undo is also the bar's own undo control. The note is tighter
on short screens, and the pot breakdown hides during entry, since which side pots
exist isn't what's being read while typing an amount. Net: 210px rather than 217px,
and no seat covered that the pad wasn't already covering at 178px.
Two rulings entries, from the same search
The oversized chip -- a single big chip pushed out silently is a call, not a raise,
however big it is; to raise you say so before it lands. And what a full raise is,
which the pad now enforces in words.
137 tests passing. Raise warnings verified across four amounts on a deep stack and
three on a short one, at eight viewports: note shows, pad stays reachable, contrast
clean, Confirm never disabled. Toast still behaves normally when the pad is closed.
Sources: cardplayer.com/.../contracts-and-poker-the-oversized-chip-rule,
pokernews.com/strategy/be-careful-when-betting-essential-wsop-rules-to-remember
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 |
||
|
|
0d6e7e0b5a |
Give the standings table back its player names
Went looking for more rules bugs and didn't find any — the blind rotation is fair
over an orbit, nobody is double-charged or skipped when players come and go, and
the big blind's option is handled correctly including heads-up and when the blind
puts them all-in. Those probes are written up below.
What I did find was a regression I shipped two rounds ago.
The name column was zero pixels wide
Adding the "Cash out" column in the bank-audit round made five columns. Four of
them are numbers with a 4.5rem minimum, which with gaps needs 320px — and the
sidebar is about 290px. The name column is the only flexible one, so it absorbed
the shortfall and collapsed to nothing. Player names were not truncated, they
were entirely invisible, and the row quietly overflowed.
Measured at 360, 390, 1024, 1280 and 1600 alike, because the sidebar is narrow at
all of them — this was not a phone problem, it was everywhere except the tablet
layout where the sidebar goes full width. Names were already down to 50px with
four columns, so this had been getting worse before I finished it off.
The table earns its columns now instead of assuming them
Narrow: player, net, cash out — the two numbers a banker settles up from. Wide
enough: the per-hand In and Won as well. A container query rather than a viewport
one, because the sidebar is 290px at both 360px and 1600px viewports and the
thing that matters is the space the table actually has. Names get a real minimum
and ellipsis rather than being crushed. Everything stays in the CSV either way.
360x640 name 0px -> 152px Player | Net | Cash out
390x844 name 0px -> 182px Player | Net | Cash out
768x1024 name -> 389px Player | In | Won | Net | Cash out
1280x900 name 0px -> 151px Player | Net | Cash out
Rules probes that came back clean, so nothing changed:
- blind rotation over 12 hands with 4 players: exactly 3 big blinds each
- a player leaving and rejoining mid-session never causes a double or a skip
- the big blind gets their option when everyone limps, can raise with it,
and checking it ends the street
- a big blind left all-in by the blind itself gets no phantom option
- heads-up, the button acts first pre-flop and the big blind still has the option
- post-flop the minimum bet falls back to the big blind
Re-checked the guarantees from earlier rounds with the sidebar open: 0 of 50
controls on touch-action auto, 0 tap-highlighted, 0 occluded, stacks still inside
their pucks, no sideways scroll, bank panel still fits and reads correctly.
137 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 |
||
|
|
073ba9a252 |
Make every tap in the dealer land instantly on a phone
Four measured problems, all of which only show up under a thumb. Every tap waited on the browser (53 of 53 controls) Not one interactive element set `touch-action`, so a mobile browser held every tap for ~300ms to see whether a second one was coming that would mean "zoom in". That is on the fold button, the call button, every seat, every card slot. Worse, this tool uses double-tap on a seat as the fold shortcut — which *is* the browser's zoom gesture. The dealer's fastest shortcut was fighting the page zooming under their thumb. A single seat tap cost ~300ms of browser hesitation before the app's own 220ms double-tap window even started. `touch-action: manipulation` everywhere keeps panning and pinch-zoom and drops only double-tap-to-zoom, which is precisely the one that was in the way. Press feedback was the OS grey box (1 `:active` rule in 2385 lines) Only `.actionBtn` had a pressed state. Everything else — seats, header buttons, card slots, picker cards, menu rows, toasts, wizard choices — relied on the translucent grey rectangle the browser paints on tap. So the tap highlight couldn't just be switched off; the controls needed real pressed states first. They press down 1px and brighten, in 0.04s, matching the action bar that already did. Then the grey box goes. Hover stuck after every tap (21 unguarded rules) `:hover` has no meaning on touch, but it still applies — and stays applied until you tap elsewhere. Tapping "Next hand" left it lifted and lightened as though still under a cursor, and it would have fought the new pressed states directly. All of them are behind `@media (hover: hover)` now. The one rule that shared a block with `:focus-visible` was split so keyboard focus survives. The notes field zoomed the page (iOS) `#tableNotes` was 13.3px. iOS zooms the viewport when a field under 16px takes focus, and the dealer is then pinching back out to see the table. Every other input here was already 16px; this was the straggler. Seats deliberately have no `:active` rule Worth writing down because it nearly went in. A seat's `transform` is what positions it around the felt, so a pressed state setting `transform` drops the `translate(-50%, -50%)` and throws every pressed seat ~38px off its spot. Validation caught it: seats already dip via `.seatPressing`, which `pointerdown` adds in render.js — earlier than `:active` resolves, and it restates the translate correctly. Validated with Playwright at 390x844: 53/53 controls off `touch-action: auto` and 0 tap-highlighted across the table, all four overlays and Quick Split; text fields still selectable and buttons not; 14/14 hover rules gated; presses measured live via mouse-down (fold reads translateY(1px)/brightness(1.3), a seat holds its translate with 2px of drift from the shrink alone). Hand played through the UI including a double-tap fold. No console errors. Unit tests 58/58. 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 |
||
|
|
6117886e70 |
Merge pull request 'Hold'em dealer: five correctness fixes and four polish passes (replaces #33–#41)' (#42) from feature/casino-holdem-dealer-batch into master
Build and Deploy / build-deploy (push) Successful in 1m12s
Reviewed-on: #42 |
||
|
|
ae8744672f | Merge remote-tracking branch 'origin/feature/casino-quicksplit-side-pots' into feature/casino-holdem-dealer-batch | ||
|
|
328ef56af5 | Merge remote-tracking branch 'origin/feature/casino-holdem-contrast' into feature/casino-holdem-dealer-batch | ||
|
|
7714e16547 | Merge remote-tracking branch 'origin/feature/casino-holdem-touch-audit' into feature/casino-holdem-dealer-batch | ||
|
|
cc31cd033b |
Merge remote-tracking branch 'origin/feature/casino-holdem-wizard-polish' into feature/casino-holdem-dealer-batch
# Conflicts: # JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js |
||
|
|
f324793732 | Merge remote-tracking branch 'origin/feature/casino-holdem-seat-count-safety' into feature/casino-holdem-dealer-batch | ||
|
|
56d9d740b5 | Merge remote-tracking branch 'origin/feature/casino-holdem-odd-chip' into feature/casino-holdem-dealer-batch | ||
|
|
e5b02b00e4 | Merge remote-tracking branch 'origin/feature/casino-holdem-button-moves' into feature/casino-holdem-dealer-batch | ||
|
|
34cd7dbc59 |
Merge remote-tracking branch 'origin/feature/casino-holdem-allin-runout' into feature/casino-holdem-dealer-batch
# Conflicts: # JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js |
||
|
|
4f72dda6fa |
Ask before a smaller table deletes the players sitting in it
Typing a smaller number into "Seats at table" silently threw away every seat past the new count. Nine players down to six removed three of them — names, stacks and buy-ins gone, no confirmation, no way back. It also quietly falsified the settle-up. Those players keep their rows in the session standings, because the hand log remembers who won what, but the seat they were read from no longer exists — so a player who had just won a pot showed as having bought in for nothing and holding nothing. Shrinking now says who it is about to remove and asks, and either way the change is undoable like the rest of the table's actions. Growing the table, and shrinking onto chairs nobody is sitting in, still happen without a word. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
cc1b5011a1 |
Give the odd chip to the player the rule says gets it
Quick Split tells the dealer a leftover chip goes "to the first player left of the button". The pot wizard, on the same tool, handed it to whoever the dealer happened to tick first. Two screens, same split, different answer. The wizard now orders the winners around the table from the button before the calculator shares the pot out, so the leftover lands where the instruction says it does. The calculator itself is unchanged — it already gave leftovers to the front of the list, it was just being handed the list in the order the checkboxes were clicked. It's pennies, but it's pennies in front of the person who didn't get them, and a dealer who has just read the other screen's instruction out loud looks wrong. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
18bb23ad6d |
Pass the button to the next player, not the next chair
The button moved one seat per hand regardless of whether anyone was sitting there. At a ten-seat table with four players — an ordinary home game — it spent five hands out of eight on an empty chair. That reads as untidy, but the real problem is fairness. Position and the blinds are meant to come round to each player at the same rate. Rotating past empty chairs breaks that: a player with three empty seats to their right takes the button far less often than one sitting in a cluster, while both pay blinds every time the rotation reaches them. The button now skips to the next seat that's actually dealt in, so with four players it visits those four in turn and each takes it once every four hands. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
ae591e4212 |
Stop asking all-in players to act
A player who is all-in is still in the hand and still wins pots, but has nothing left to decide. The engine didn't know that: it picked the next player to act by walking to whoever was still in the hand, chips or no chips. So a dealer with three players all-in pre-flop was asked to check for each of them on the flop, the turn and the river — nine taps to say nothing — and the action could land mid-street on someone who'd already pushed their last chip in. Now the action passes over players who are all-in, and once fewer than two people have chips left the betting is simply over: the bar runs Deal Flop, Turn, River, Showdown straight through, and says "All-in — running out the board" so the absence of a question reads as deliberate rather than as a stuck screen. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
89d79a0f7d |
Post the blinds to the players rather than to the chairs
Blinds were assigned by seat offset — always button+1 and button+2 — which is only right when everyone is dealt in and there are three or more of them. Heads-up it had the blinds backwards. The button posted the big blind and acted second before the flop; the rule is the opposite on both counts. That's the end of every home tournament, dealt wrong. Worse, the offsets don't care whether anyone is sitting in those chairs. With two players at a nine-handed table, or with the seats left of the button sitting out, the blinds landed on empty seats and simply weren't posted — no money in the middle, and SB/BB badges pointing at nobody. Blinds now walk the players who are actually dealt in, starting from the button, with heads-up as its own case: the button is the small blind, acts first pre-flop, and the big blind acts first afterwards. The badges come from the same function as the money, so what the table shows and what gets posted can't disagree. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
d0f75d0c14 |
Lift the faint greys to something readable in a dim room
This is a dark interface used in poker rooms and kitchens with the lights down, and I'd been generous with grey-on-black. Measured every piece of text against what's actually painted behind it: thirteen declarations came in under 4.5:1. The worst was the empty card slot's "+" at 2.63:1 — an affordance telling the dealer the slot is tappable, and the least legible thing on the screen. Then a run of #777 labels around 3.9 to 4.1: the standings header, the time on a logged hand, the currency mark in the add-chips field. The rulings source line sat at 3.62 and the rulings link itself at 4.39. All of them move up just far enough to clear 4.5 rather than being flattened to white, so the hierarchy between a heading, a value and a hint survives. Worth recording what the measurement can't see: it reads background-color, so text on a gradient — the pot badge, the dealer button, the blind badges — comes back as a false result in both directions, and those were checked by eye instead. The felt watermark is deliberately at 6% and stays that way. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6b3a7dc09d |
Bring every phone target up to size, not just the ones I happened to check
Last round I measured one screen and found the smallest targets in the tool were on it. So I measured all of them: sixty-four controls at phone width, flagging anything under 44px. Twenty-three came back. The sidebar had never had the touch pass the seat form got — its inputs, selects and small buttons sat at 26 to 30px, and the buttons that delete a chip colour or a custom field were 28px wide, which is a bad size for something destructive. The rulings link was a 14px-tall underlined word. The seat form itself had drifted: its mobile sizing rule named five fields, and the stack field added two rounds later wasn't one of them, so it sat at 28px beside its 44px neighbours. Both that rule and the new sidebar one are now written against the form and the panel rather than a list of field names, so the next field added inherits the size instead of waiting to be noticed. The dealer button keeps its 22px disc — it's a chip sitting on a seat, and drawing it thumb-sized would swallow the puck — but takes taps through an invisible 44px pad. Verified the seat underneath still gets its own taps. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
38488f2978 |
Give the pot wizard the same tap targets as everything else
Measured the screen where the money actually gets decided and found the smallest targets in the tool: 37px winner rows, 13px checkboxes, a 36px confirm. Every other surface has been held to 44px and up for several rounds — the one place a mis-tap pays the wrong player was the one place that hadn't been. Winner rows are now 56px and up, with a 22px box and a green fill when chosen, because a dealer confirms this with players watching and a small checkbox is easy to misread across a table. The row already was the tap target; now it looks like one. Each row also splits into name, what they can win, and their cards as separate pieces rather than one run-on sentence, so the hand can be read at a glance. The confirm takes the full width, and on a phone the summary's buttons stack with the one that ends the hand furthest from a stray thumb. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
7c64749267 |
Let Quick Split handle the pot it used to send people away for
Quick Split answered the easy half of the question and printed a footnote for the hard half: if somebody was all-in for less, go set up the full table. That's the moment a dealer least wants to be setting up a table. A second mode takes what each player put in and lists the main pot and each side pot with who can win it, plus anything uncalled that goes back. Rows can be marked folded, so dead money lands in the right pot without its owner being able to win it. The running total is shown so the breakdown can be checked against the chips actually on the table. It reuses the same pot maths as the full table rather than growing a second implementation, which means the two screens can't drift apart and Quick Split inherits the uncalled-bet and everyone-folded handling that was fixed there. Also fixes hiding: these sections carry a `display` declaration, which outranks the browser's own [hidden] rule, so toggling `hidden` on them did nothing and both modes' controls showed at once. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
e8d6098d39 |
Merge pull request 'Add chips to a seat without leaving the table' (#32) from feature/casino-holdem-rebuys into master
Build and Deploy / build-deploy (push) Successful in 1m7s
Reviewed-on: #32 |
||
|
|
5d1a6b7399 | Merge branch 'master' into feature/casino-holdem-rebuys | ||
|
|
a816b32426 |
Merge pull request 'A rulings reference for the calls that come up mid-hand' (#30) from feature/casino-holdem-rulings into master
Build and Deploy / build-deploy (push) Canceled after 31s
Reviewed-on: #30 |
||
|
|
5271d5fbad | Merge branch 'master' into feature/casino-holdem-rulings | ||
|
|
552ec1a542 |
Add chips to a seat without leaving the table
Somebody rebuying is the most common interruption in a cash game, and the only way to record it was to open the seat editor and retype a stack from memory. Press and hold a seat and there's now "Add chips…": quick amounts off the table's starting stack — half a buy-in, a buy-in, two — with a keypad for the times it isn't one of those. It's undoable like every other seat action, and it records cash handed over as well as chips added, because those answer different questions. The stack moves every hand; what a player has paid in only moves when they buy. The export gains bought in, chips now, and up/down, so settling up at the end of the night is reading a row rather than reconstructing it. Getting that last column right needed care. Chips in front of a player disagree with the hand log for as long as a hand is open — and since blinds post the instant the previous hand settles, that window is almost always. Counting what a player already has in the current pot as still theirs makes the cash position agree with the hand log at any moment, including across a rebuy. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
5c807d1581 |
Merge pull request 'Feature/casino holdem sidebar polish' (#31) from feature/casino-holdem-sidebar-polish into master
Build and Deploy / build-deploy (push) Successful in 1m5s
Reviewed-on: #31 |
||
|
|
a5b9c26902 | Merge branch 'master' into feature/casino-holdem-sidebar-polish | ||
|
|
9a5b974fda |
Add a rulings reference for the calls that come up mid-hand
The tool handles the money; the other half of dealing is the calls. A home-game dealer who isn't a pro has to rule on a misdeal or a prematurely turned board card with a table watching, and the usual outcome is an argument settled by whoever is loudest. A "Common rulings" link under the table opens a reference for exactly the situations that come up: what counts as a misdeal, what to do when the fourth board card gets turned before the betting is finished, and how acting out of turn is normally treated. It follows the Tournament Directors Association rules, and says plainly that house rules win and that a dispute gets settled between hands rather than during one. It's a sheet rather than a sixth sidebar panel — a dealer consults this rarely and needs it immediately, which is the opposite of what a panel is for. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
b563a56563 |
Merge pull request 'Put bets on the felt as chips instead of numbers in a box' (#29) from feature/casino-holdem-bet-chips into feature/casino-holdem-sidebar-polish
Reviewed-on: #29 |