e90bcccada626d21ccd992c2b56d54b7ef687031
312
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e90bcccada |
Spend the accent on the one thing to do next, not on two at once
The green is a promise: this is the step. Measured over a whole hand — seventeen steps — it was on two controls at thirteen of them, the bar's Call and the header's Resolve Pot painting the identical gradient at opposite ends of a phone, and on the header alone at the other four while the bar held the only button there was to press. At showdown the most consequential tap in the hand, "Showdown — resolve pot", was blue-grey while the corner shortcut was green. So the accent moves to .actionAdvance, which is always the next step by definition, and comes off the header shortcut, which never is. Twenty-two measured steps of a real hand now show exactly one. The sweep checks it: oneNextStep() counts the accent-coloured controls that are actually on screen, scoped to an open sheet when there is one. Six broken versions were run at it, and the two that took three goes to catch were both the same mistake in the injection rather than a hole in the check — a button painted green with the panel shut isn't rendered, and one painted green below the fold is deliberately ignored. Scrolled into view it is named alongside the bar's Call, and goes clean again when the green comes off. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
14fb68dc04 |
Merge pull request 'Hold'em dealer: making the table and the bar fit the screen they are on' (#119) from polish/holdem-round-next into master
Build and Deploy / build-deploy (push) Successful in 1m40s
Reviewed-on: #119 |
||
|
|
4a1cb842cb |
Say on each shut sidebar panel the one thing you'd open it for
The sidebar is eight <details>, and every one measured exactly 60px shut with nothing on it but its own label — 480px of a phone's screen, and 480px of a 660px desktop column, spent saying "Standings", "Hands", "Waiting list". Whether the box balances, whether anyone is waiting for a seat, how many hands have been played: the page had all three worked out already, and charged a tap for each of them, on rows two thirds of which were empty. That is the disclosure pattern's named failure — a header you have to open to find out whether it was worth opening — and the answer the design systems give is the same one: put the status on the header. The panel keeps the sentence. "Off by $300" is the alarm; which seat is holding chips nobody wrote down is a paragraph, and that is the half that belongs behind the tap. Two rules, and panelGlance.test.js checks both. A glance appears only when it has something a dealer would act on — eight rows that always say something are eight rows that say nothing again, and a row reading "Nobody waiting" all night is the clutter this went in to remove. And a glance never says something the panel under it would contradict: it is read off the same state, never off the panel's DOM, which for a shut <details> isn't rendered at all. Absolutely positioned rather than floated. A <summary> only draws its disclosure triangle while it is display: list-item, so it cannot be a flex row; the float that replaced it kept the row at 60px but aligned to the label's line box, and a 12px string in a box sized for 15px text rode 2.5px low on all six panels. Out of flow is also the smaller promise — an absolutely positioned box cannot make its row taller at all. Three paths deliberately skip renderAll, because it would take the field out from under whoever is typing in it, and all three move something a glance reports. The notes textarea, the chip legend's value fields and commitSeatChange each call renderPanelGlances on their own instead; without that the note you had just typed was summarised by the note before it until the next tap landed. A space between the label and the glance, too: the summary is announced as one button, and without it a screen reader was given "Table Setup9 seats". Costs 0.08ms of a 4.79ms renderAll on a twelve-handed phone. Four injected faults in the wording are caught one test each. glancesFitTheirRows took three goes. The first asked only whether a row had grown taller than a bare one, and a glance floated back into flow and allowed to wrap sailed straight through it, because a float does not expand the box it sits in. The second was caught by the sweep on its first clean run — 94 failing configs, 91 of the offenders this check's own: it takes the shortest row as its idea of a bare one, and the Keyboard panel is display: none under pointer: coarse, so on every viewport visited with a thumb the bar was a row that wasn't drawn, and all seven real rows read as grown. Rows that aren't rendered aren't rows. The four broken layouts are re-run with that panel hidden and each is still caught. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
e7ddff2fed |
Let a large felt have larger seats, and the type that goes with them
The felt grows with the window and the pucks did not: 96px was the only ceiling there was. Nine-handed, the seats covered 19% of the felt on a phone, 11% on a 1280px desktop and 8% at 1440px and beyond — and the stack figure a dealer reads sat at 10px on a 2560px monitor, the same 10px as on a 360px phone, well under every published floor for text meant to be read. Two things stop this costing more than it buys. Every candidate width is a forced layout. Simply raising the ceiling took the layout pass from 24ms to 73ms twelve-handed on a desktop, so a screen that cannot use the wider widths must not measure them: the extra pass is offered only when the arithmetic already says the seats have room to spare, and a phone measures exactly the ten widths, in the same order, that it measured before. Timed with the order alternated so neither carries the warm-up: 10.7ms against 10.4ms on a phone, and +3 to +6ms where the widths are actually used. And a wide puck on a narrow felt is worse than a small one. The first version of this let phones through, and a 144px puck on an iPhone's 278px ring is half the width of the table: six-handed, all six seats sat over the rail by 15px against 5px before, reading as a stack of cards rather than a table with people around it. So the ceiling is a fifth of the felt's shorter span, capped at 144. Five is measured rather than chosen — the shorter span is 233-303px on every phone in the sweep including sideways and 617-714px on tablets and desktops, so any divisor between 3.2 and 6.4 separates them — and the test pins both ends. Above the ordinary ceiling the puck takes the larger type as well, which is the point of the extra width. Nine-handed: an iPad goes 96px/12px to 134px/15px, a 1440px desktop 96px/12px to 140px/15px, and the seats go from 8% of the felt to 12%. Every phone, at every seat count, is untouched. The sweep is byte-identical to before this change: same six configurations, same 63 offenders, empty diff. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
f8ed4abc85 |
Stop two empty rows in every seat puck setting the size of the table
A seat puck carries the name, the hole cards, the amount that player has put in, and their stack. Two of those are empty most of the time and both held their space anyway. The amount pill was worse than empty space. It has a background, a pill radius and side padding, so with nothing in it, it painted an 8x11px green lozenge — measured on seven of nine seats at an ordinary table. One of those is a component holding its place; nine is a rash, and a dealer has to learn to ignore a colour that on the other two seats means "this player has money out". The space was not free either. seatRingRadii sizes the ring from the tallest puck on it, so two rows nobody had filled were setting how far apart the whole table had to sit. Across 21 tables — seven viewports, six, nine and twelve handed — every single puck comes back shorter and six come back wider: an iPhone 14 nine-handed 78x88 -> 78x63, twelve-handed 44x65 -> 66x55, which is the one name in the set that was clipped and now is not. The collapse is only safe because the layout cache moved with it. resolveSeatLayout measures ten candidate widths, each a forced layout, against seatShapeSignature() — which listed the names and the dealer seat, everything that could wrap a name onto a second line. It did not mention these two rows, because while they always held their space they could never change a puck's height. Now they can, so the signature names them: booleans over the whole table, because measuredSeatBox takes the tallest puck on the ring, and because a per-seat key would be correct and would throw the cache away on every tap. The ring settles by 24-26px when the blinds go in — and it settled by 23-25px there before this round too, since the action bar changes height at that moment and the felt is measured against it. The one place it moves is a place it already moved. uiAudit gains emptyDecoration(): nothing on the felt may paint a background or a border with no content in it. Its one exemption is on the list because the check reported it rather than because it was predicted — .betChipDisc is a circle filled with the denomination colour of the chip beside it, so the colour is the content. Three others were written from memory first and matched no element on the page at all. The sweep is unchanged at 6 failing configurations and 63 offenders. All four table failures are the all-in shape, where every pill is filled and the collapse buys nothing; the ordinary tables this helps were already passing, so what moved is room a name has rather than whether anything was covered. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
942919f3f7 |
Stop the side-pot list growing the action bar over the felt
The bar is fixed over the table, so its height is the felt's budget — and it carried a pot breakdown that drew one row per all-in with no ceiling. On a 360x640 phone: five players idle put the bar at 151px, six all-in at 303px, twelve all-in at 411px. That is 64% of the screen, leaving 96px of felt to draw twelve seats in, and at that size the seats stop being a table: five pairs collapse onto each other's centres, ten hang past the rail and a dozen labels sit completely covered. Past four layers the bar now draws the main pot, one line for everything between, and the newest side pot. The two ends are the live ones for the same reason — side pots are paid from the newest back down to the main pot, so the top layer is the next thing that happens and the main pot is the last, and those are the two a dealer says out loud while the chips are stacked. The middle is the rest of that count-down, wanted at showdown, which is the pot wizard: same builder, no cap, in a dialog that scrolls. The elided line carries the count and the money, so the visible rows still add to the announced pot. Nothing in the sweep could see any of this, and the reason is worth keeping. Every check asks whether an element is covered or clipped; none asked how much room the covering element had taken. But the deeper miss was the sweep itself — all 294 audits drove their table with Call and Check, so no table it ever measured had a player all-in, and the one thing on the bar whose height depends on the table was empty in every measurement. There is now a fourth table shape, twelve players shoved in for twelve different amounts, and furnitureBudget() measures the fixed band against the window: failing over half of it, warning over the 30% that persistent chrome is usually held to. Galaxy S8, twelve all-in: bar 411px -> 267px, felt 96px -> 192px, 91 layout offenders -> 25. iPhone SE: 73 -> 10. The rest is the seat ring on a short phone, which the sweep can now see. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
02c2a034cb |
Stop the fixed bar and the toast sitting on the keyboard's focus
WCAG 2.2 SC 2.4.11 asks that a focused control not be completely hidden
by something the page has put on top of it. The bar is fixed over the
bottom and is not in the flow, so the browser does not know it is there:
Tab to a control near the bottom and it is scrolled flush against the
edge of the window, under the bar, and the browser considers it done.
Tabbing the whole page measured 16 stops resting completely covered on a
390x844 phone and 27 more partly, plus four under a toast on the desktop.
It is now 0 and 0 on desktop, phone and a phone on its side — clean
against 2.4.12 (AAA) as well, which allows no overlap at all.
Three things were wrong and they needed different answers.
scroll-padding-bottom on the scroll container, sized from the bar's own
published height and the toast's, handles the case where the browser has
to scroll to reach the control. That is half of it, and the half that
looks like all of it: a control the browser already considers visible is
one it will not scroll to, and a fixed overlay on top does not enter into
that decision. Measured — tabbing to Common rulings with a toast up left
it 100% covered and scrollIntoView({block:"nearest"}) moved nothing. So a
focusin handler measures the overlap and scrolls by it, and when the page
has no room left (26px against 94px wanted) the toast gives way instead.
Sizing the page padding for the toast would have fixed that by making the
page grow and shrink 64px whenever anything was announced.
The page's room to scroll past the bar was 9rem on the sidebar alone,
which left the table's column — most of a desktop — with none. It is now
on the whole app and taken from the bar's real height, so it is also 0 in
the layout where the bar is a rail down the side.
And the seat editor's bottom sheet was put away with a transform, which
moves a thing without putting it away: its Done button sat at y=850 on an
844px screen, focusable, announced, and impossible to scroll to, because
a fixed element does not move when the page does. visibility:hidden puts
it away, transitioned so the slide-out still runs.
uiAudit.js gained focusHasRoom() and focusablesReachable() for the two
static halves. Audit unchanged at 2 failing configurations and 3
offenders, both pre-existing. 805 unit tests pass.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
cd2db6346e |
Stop a chip colour taking the table's money with it, quietly
Deleting a chip colour was one click of an unlabelled 24px button, and it was never a change to a legend. computeContribution() prices a seat by running the legend over that seat's chip counts, so a colour that leaves takes every chip counted in it off the table. Measured in a browser on a three-handed table: deleting the $500 row took $22,500 of $26,874 with it. No confirmation, nothing said afterwards, and no way back — a colour added again is a new colour with a new id, while the counts stay keyed to the one that left. The counts were left behind too, so the saved file carried money nothing could price and nothing could show. Now: a colour nobody has counted still goes on one click, and one that is holding money asks first, naming the amount, the chips and the seats, and saying that Undo will put it back while adding the colour again will not. Conditional on purpose — a confirmation that fires every time is one nobody reads by the third night, and four others already compete for that attention here. Removal goes through runUndoable, and the counts go with the colour so the file matches the screen. The number in that warning is the difference between pricing every seat with the colour and without it, not chips times value. The obvious arithmetic is wrong both ways: a seat with a typed amount is never priced from its counts, and a seat whose chips all leave falls through to the table's standing ante rather than to zero. A randomised check over 2,000 tables asserts the warning equals what the table then does, and it found a third case — fourteen $1 chips against a $200 ante, where removing the colour puts the table UP $186. The wording says so in that direction too. The × also had no accessible name, so uiAudit.js gained controlNames(): every control needs a name containing a letter or a digit, resolved the way the accessibility tree resolves it. A symbol is not a name. Taking the new aria-label off again makes it report all five buttons. Audit unchanged at 2 failing configurations and 3 offenders, both pre-existing. 805 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6eb97c8a35 |
Give the sidebar eight names instead of five and a cupboard
"Session" was a container word rather than a name. It held the standings, a waiting list, the hand log and the save/load/reset buttons — a topic, a task, a log and three utilities under one label, which is a navigation layer with more than one logic in it. It measured 1206px and 103 visible elements, against 555 and 29 for the next biggest, and "The table itself" sat 1052px down inside it. Reaching Reset table meant opening a panel called Session and scrolling past the money and the log to get to it. It is now Standings, Waiting list, Hands and Save & reset, each named for the thing a dealer came looking for, in the order of a night: set the table up, watch the money, fill the seats, look back at a hand — and, last and apart, the two buttons that end it. Reset table goes from 1052px inside a panel called Session to 62px inside one whose label says what it does. The biggest panel drops from 1206px to 607px. The four subheads that used to carry those names are gone, because the summary says it now. Every panel name is also a heading. A <summary> is announced as a button, so the sidebar's names — its whole navigation — were unreachable by heading, and splitting Session would have removed the only four headings down there. The outline is now h1 page, h2 per panel, h3 for the two subheads that remain. Visually identical: the h2 gives up everything a heading brings with it. The cost is 216px more collapsed summaries at rest, three more one-line labels. Audit unchanged at 2 failing configurations and 3 offenders, both pre-existing. 795 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
bd8ee11f77 |
Say who can win each pot in the fewest names that answer it
The pot breakdown listed every eligible player for every pot. Pot eligibility is nested — the main pot is everyone still live and each side pot above it drops whoever was all-in below — so the main pot's line was every name at the table and each side pot repeated it minus one. The lines too long to fit were exactly the ones nobody needed spelled out, and the short ones at the bottom were the side pots a dealer actually announces by name. Measured on an iPhone 14: a single all-in five-handed already clipped the main pot, and twelve-handed the first row wanted 521px of a 254px slot. There was no count and no title, so a dealer reading "Alexandra…" could not tell whether three people or eleven were eligible — the number the showdown turns on. Each line now names whichever of "who is in" and "who is out" is shorter, which nesting guarantees is never more than half the table, and leads with the count because the line is right-aligned and it is the tail that gets cut off. "all 9", "8 in, not Mike from work". On a phone this takes one, two and three all-ins from every main-pot line clipped to nothing clipped at all; twelve-handed goes 6 clipped rows to 5, and even a cut row still opens with "9 in". The wizard's copy of the same announcement wraps instead — it is a dialog that scrolls, and it is what the dealer reads out before anybody shows a hand — so nothing there is ever cut off. The full list is also on the title for anyone with a pointer, which is the extra rather than the answer, since a tooltip is not a mechanism a keyboard reaches. potCalculator.test.js now proves the nesting across 20,000 random tables and asserts on its own generator, so the loop cannot go vacuous. Audit unchanged at 2 failing configurations and 3 offenders, both pre-existing. 787 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
13d704301d |
Stand the felt up on a phone, and check which rule actually wins
A portrait phone got a square felt, so the vertical room the layout had already measured was published and then dropped: 522px of room under a 366px table on an iPhone 14, 638 under 406 on a Pro Max. The cost was seat names — 44px pucks with five of twelve clipped. --feltAspect: 1.5 spends it. Twelve seats at 390x844 go from a 44px puck with five names clipped to 66px with one; five seats go from three clipped to none; the Pixel 7's twelve-seat ring goes to zero. 1.6 and 1.7 measure byte-identical, so 1.5 is where the sweep stops paying. The three smaller phones are room-bound at any cap and measured identical from 1.15 to 1.7. The first version of this was a no-op. The rule went into @media (max-width: 700px), and @media (max-width: 900px) below it set the same property back to 1 — both match a phone, same specificity, later one wins. It measured as 1 on every phone while reading perfectly. The 900px rule is now a 701-900px range, and feltShape.test.js resolves --feltAspect through the media queries the way the cascade does so the next one of these is caught. Also tried and removed: a floor under the max-height, so a tall bar couldn't press the table flat. Twelve all in with nine side pots takes the bar to 341px, leaving 142px of room at 360x640; floored to 242 the felt keeps its shape by putting five of twelve seats behind the bar, unfloored it is flat with all twelve visible. The slit it defended against came from forcing the custom property by hand. A test asserts its absence with the numbers. Audit: 4 failing configurations and 5 offenders down to 2 and 3, both remaining ones pre-existing. 776 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
7bd205a91a |
Line the bar up with the table, and stop the button squeezing a name
Two things that were wrong at every desktop width, both of them the same shape: a number that was right about one thing being used for another. The action bar was centred on the window. The table is centred on its own column, and the sidebar takes 320px out of the right of that — so the two things a dealer is actually looking at sat 172px apart, at 960px wide and at 1600px and everywhere in between, because the offset is half the sidebar and the sidebar is a constant. The bar also finished 80px inside the sidebar's column at 1280, painting over whichever panel happened to be at that height. It leans away from the sidebar now, by the sidebar's own width, which comes out exact above and below the 1400px cap — measured on the running page at four widths, 0px off at all of them. No 100vw in it, which is the point: the bar's own centring comment warns that on a page with a scrollbar 100vw is wider than the viewport, and the offsets are exact without it. The dealer button's chip sits in the puck's top-right corner and the name kept out from under it with `padding-right: 22px`. That takes the chip's width off *every* line of the name, and the chip is on one of them. Measured at 1280x900: the name box is 78px, "Christopher" is 66 and fits, and on the seat holding the button it was cut to 56 and broke as "Christop / her" — mid-word, no hyphen. The button moves every hand, so that is a player whose name reads properly for four hands and comes apart on the fifth. A float is what indents only the lines it sits beside. Line one gets 56px and the rest get 78, so a name too long to fit beside the chip drops below it whole instead of being broken to fit next to something that isn't there. Not on a crowded ring, though, and finding that out cost a sweep. A tight puck gives the name one line on purpose, and one line has nowhere to move to: with two lines granted the name dropped onto a second line that the chip's lower half was still sitting on, and the audit called it at every viewport in the list. So the float is for the ring with room for two lines and the padding for the one without — two rules, matching two regimes the puck already had. The sweep also learned two things from this: - Text is measured where it was painted, not where its box is. The seat-label check modelled "text kept out of somewhere" as padding, which is one way of doing it and not the only one; a float does it per line, and the check called every seat in the sweep an offender while the text sat exactly where it was meant to. Range rects ask the question directly, so padding, floats, centring, wrapping and a short label in a wide box all come out right without any of them being modelled. - Offenders say how much is covered, not that something is. "Is under a badge" reads the same for a stack a dealer cannot read and for a badge's rounded corner grazing the last digit. Probing the whole line and counting turns the residual this sweep has carried for three rounds into "1/7 under SB" — one probe of seven, one configuration, thumb only. 294 audits, 4 failing configurations, 5 offenders: twelve players on a 360px phone with the raise pad open (3/7 and 2/7, pucks genuinely overlapping — the search reports it rather than papering over it) and the 1/7 hairline above. 770 unit tests. Fault-injected, all restored and re-verified: writing the sidebar's width out again instead of the token, growing the float to cover the chip's whole height, and taking the padding off the crowded ring are each caught by a test; and with both the float and the padding removed the rewritten label check still reports the name 6px under the chip, which is the fault it exists for. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
f07144acd8 |
Give the table the screen back when the phone is on its side
A phone held sideways is 640x360: short of height and swimming in width. The layout spent the whole of that shortage on one column — 70px of header, 85px of felt, 205px of action bar. Two rows of seat pucks 55px tall have to fit inside 85px of felt, which they cannot, so seven of nine finished past the rail with up to 17px of puck floating on the page. That single fault was 96 of the 98 offenders the layout audit could find anywhere. No amount of shrinking fixes it, because the shortage was never in the table. The bar is 205px tall and 529 wide to hold three buttons and a status line, and it was taking that height from the one element that needed height. So in landscape it moves: the bar becomes a rail down the right-hand side, the table takes the column beside it, and the felt goes from 529x85 to 409x205 — an actual oval, with room for two rows of pucks and a gap between them. It also puts Fold, Call and Raise under the right thumb rather than along the bottom edge, where a landscape grip has the palm. Which layout the bar is in is declared by the stylesheet in a custom property and read back by actionBar.js, because the one thing that must not exist is a second copy of that media query written in JavaScript. Then the ring stopped predicting itself. seatRingRadii works out the widest ring whose outermost puck still finishes inside the rail, and it is right about a puck it measured before the width search settled and before a name wrapped or was shortened — each worth up to 15px of height arriving after the answer. Four viewports finished with the top seat 5-6px over. render.js now checks the drawn result the way the audit judges it: if the worst puck hangs 6px past the rail, the ring comes in by 6px and they are placed again. A layout that predicts itself has to be right about the future; one that measures itself only has to be right about the present. Also in this round: - Names shorten at a word instead of being chopped mid-one. "Mike from work" on a nine-handed phone was "Mike f…"; he is Mike. A name never ends on a joining word, because "Mike from" looks cut off rather than shortened, and the whole name is on the tooltip either way. - The blind badge is counted in what a seat needs. It always points away from the table, which was taken to mean it costs nothing between two seats — true of a straight rail and false of a ring, where neighbours sit at different heights. Three tables had a player's stack painted under the next player's SB or BB, and that badge outranks everything else on the felt. It also got shorter, 15px to 12: clearing its own name at 5px of overhang instead of 9 hands every chair in the ring four pixels back, which at 640x360 is the difference between a 66px puck and a 48px one. - Border-box, inherited, for the whole app rather than nine elements opting in one at a time. Both elements that hadn't went wrong in the same afternoon: the rail overran the page's padding by 25px and put Resolve Pot underneath itself, and the raise pad's amount field pushed a horizontal scrollbar across a 640px phone. - The raise amount field is thumb-sized. The rule that sizes fields for a finger names the settings panels and the seat form, so the one field on the one surface built for thumbs was 36px on every phone in the list. The sweep never saw it because it had no step that opened the raise pad; it has one now, and that step also caught the rail squeezing its own status line under the pad. - mobile-web-app-capable alongside Apple's older name, which Chrome has been calling deprecated in this page's console on every load. The audit goes from 19 failing configurations and 98 offenders to 2 and 3, across 294 audits — thirteen viewports, three table shapes, a mouse and a thumb. What is left is twelve players on a 360px phone with the raise pad open, where twelve pucks at the 44px minimum genuinely do not fit on the felt; the search reports that rather than papering over it. Each new mechanism was checked by breaking it on purpose: removing the settle pass puts 26 offenders back, dropping the badge from the footprint 5, and un-sizing the raise field is caught by the step that now opens it. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
fbc4bd9a3e |
Merge pull request 'Hold'em dealer: a felt that fits, and a table that reads its own showdown' (#118) from polish/holdem-seat-collisions into master
Build and Deploy / build-deploy (push) Successful in 1m11s
Reviewed-on: #118 |
||
|
|
cd57c16050 |
Let the table tell the dealer who won
An option — off by default, and off means the tool is exactly what it was.
Turn it on and the community cards go on the felt. Each street asks for the cards as
it is dealt: the picker opens on the flop the moment the flop button is pressed, and
again on the turn and the river, because the moment a dealer is holding those cards is
the only moment they know what they are without turning the board round. Three taps,
then one, then one. The picker is the same sheet as the hole cards, which now knows
about the middle of the table too — the seven of clubs cannot be on the flop and in
somebody's hand, and until now it could.
Turning the players' cards over is unchanged. That already worked the way it should.
What is new is what happens next. Resolve Pot stops asking who won and says so:
Alexandra wins $50 with three of a kind, nines.
with every hand at the table read out under its player's name, the winner already
ticked, and the button reading "Pay Alexandra". A chop comes out as "Christopher and
Mike from work split it, $22.50 each, with a pair of aces", both ticked. The dealer
still confirms — the tool reads the cards, a person still says the pot is good, and a
hand entered wrong is corrected by unticking rather than by starting over.
It declines rather than guesses. A board that isn't five cards, a player who hasn't
turned over, the same card entered in two places: each says which of those it is and
hands the question back, and the wizard is the one it always was. Who is *ahead* and
who *won* are different claims and only the second one can pay somebody.
Side pots are read per round, not once. The best hand takes the main pot and is then
out of the question; the side pot goes to the best of who is left, which is a different
answer and the one the tool has to ask for again.
handRank.js is the only module here that can pay the wrong person without anybody
noticing, so it is built to be checkable rather than clever: rank five cards, then take
the best of the twenty-one five-card hands in seven. Beside the written cases — every
category against every other, the wheel and the steel wheel, three kickers deep, the
counterfeited two pair, the board that plays — it is checked against a second evaluator
written a different way, on twenty thousand random seven-card hands and four thousand
random pairs of them. An ace that keeps its value in the wheel, a dropped two-pair
kicker and a flush read only by its top card were each introduced on purpose and each
caught.
The board takes the middle of the felt, sized from the room the seat ring actually
leaves — measured against every seat rather than the ring's widest point, which is only
the answer for a seat sitting exactly on the horizontal axis and had two pucks on top
of the board while claiming 77px of clearance either side. Where there is no middle to
take, it moves to a strip above the table: a phone on its side has a felt 528x83 with
55px pucks, so the ring closes over the centre entirely and a board in it is a board
with players painted on top.
That strip found an older bug. It costs the felt height, and a felt that changes size
had only ever been re-measured, never laid out again: the seats follow it on their own,
being placed in percentages, but a bet chip anchored inside a seat is placed in pixels
off that seat's centre. Measured at 640x360, the chips were positioned for a felt 83px
tall and painted on one of 60 — seat 3's $5 came to rest on seat 4's puck, and two
pairs of seat pucks were overlapping. The bar growing mid-hand does the same thing: a
side pot appearing is 55px off the felt. Both fixed.
The sweep grew a third table — the same five players in the same nine chairs as the
second, with the board on, so the only thing that differs between those two rows is the
board itself. It earned its place on the first run.
728 unit tests. 216 audits: what is left is the seat ring hanging past the rail on a
landscape phone, the known residual, plus five or six pixels more of it on the layouts
where the strip takes the felt's height, and a blind badge over a stack on a packed
twelve-seat portrait table.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
2a080eab6a |
Give every bet on the felt somewhere to be
The chips were the last thing on the table nobody had measured, and the sweep had never seen one: it audited a table that had never had a bet placed on it, so a third of what is on the felt during a hand went unlooked at for the tool's whole life. It posts blinds, deals and calls three times now before it measures anything. What that found, and what this fixes: - Chips accumulated. Seats are kept and updated in place rather than rebuilt, so clearing the bets layer stopped clearing the ones anchored inside a seat and nothing else ever did. An iPhone 14 had one chip per seat after the deal and five by the third render; a dealer's every tap left another disc on the felt. - The ring-or-cling decision asked how wide the window was. A phone held sideways is 844px across and gives the felt 117px: it passed the width test, took the ring, and every chip on it was painted under a puck — the seats carry z-index 1 and the bets layer carries none, so a chip that lands under one is gone, and with it the only number saying what that player has in. Measured now, and only measured. - Getting clear of a box was `edgeReach(puck) + edgeReach(chip)`, which is right only straight up, down or sideways. Two axis-aligned boxes are clear when they are apart on one axis, so the travel is the reach of the box you get by adding their widths and their heights. At 768x1024 a $10 leaving a seat on the diagonal wanted 88px and got 77, and came to rest 11px inside its own player's puck — every seat off the four compass points, which is most of them. - Every chip was laid out at the first chip's size. A chip is as wide as the number on it: "$5" is 21px and "$10" is 27px on a phone, so each $10 was placed as if it were 6px narrower than it is. The widest is no better: at 768x1024 one 47px chip made the whole table give up a ring the other four had room for. - Anchored chips settled where they landed. At 640x360 the felt is 528x83 and a puck is 55 of those pixels tall: clearing it takes 41px, the players facing each other are 70px apart, and both chips set off 41px into a gap that is 70 wide. Three overlapping pairs on a twelve-handed table. Each chip now gives ground only to something it is actually standing on, a pixel at a time — a midline rule or a single scale factor both punish the seats at the ends of a rail, which were never going to hit anything. - Nothing knew the pot total was there. Seat 1's $10 was sitting on "Pot $65". Two things off the felt, from the same sweep: - The toast outlived its news. "Next hand — button on seat 2. [Undo]" stayed up while the hand was dealt and three players called: an offer to move the button, wired to a snapshot that predates the hand, so taking it would have thrown the hand away without saying so. It was also in the way of thirty small screens' seats and controls. The bar drops it as it takes the next action; the bar's own Undo names what it would actually take back. - The pot dialog cut a name off. Three players in the pot on a Galaxy S8, two rows visible, the third below a hairline that reads as the end of the list — a dealer ticking the two they could see would have paid two thirds of a three-way pot. The 85vh budget was still subtracting the action bar's height from a dialog that has sat above the bar since its z-index was fixed, and a player's ceiling moves beside their name on a short screen. All three fit now, and where a list does run on, it runs on under a fade rather than stopping dead on a row boundary. The pot badge goes on a phone held sideways: 86x66 of a 692x113 felt, for a number the action bar prints three inches below it, standing in the only band a bet chip has. The audit stopped calling one thing another. An element clipped away by a panel that scrolls is not covered by whatever is painted at that height — the wizard's third winner came back for four viewports as "fully covered by button Confirm Winner(s)". 190 audits: 25 failing configurations to 7, and every one of those seven is the seat ring hanging past the rail on a landscape phone, which is the known residual from the last round. 663 unit tests, 44 of them new. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
f93e0935ef |
Give every seat its own pixels
Six players at a nine-handed table is the most ordinary table this tool draws, and on every phone in the sweep one of them had their stack painted under the player next to them. Measured across nine viewports and six table sizes: seventeen of fifty-four tables hid a name or a stack behind something belonging to another seat. Two causes, and the layout only knew about a third of one of them. The felt decided how big a puck could be from a single number — the straight-line distance between the closest two *neighbouring* seats, judged against a fraction of a puck's width. Distance has one number in it and a puck is half again as wide as it is tall, so the same 84px was generous side by side and nowhere near enough on the diagonal. And neighbours are the wrong pairs to ask about: on a phone held sideways the felt is 528x87, the ring is very nearly a straight line, and the seat that lands on top of you is the one across the table. Ten of the tables in the sweep had a collision that a neighbours-only check cannot see — seats 1 and 7, overlapping by 68px, while every adjacent pair had room. seatsCollide() asks the real question instead: do any two of these boxes want the same pixels. Two equal axis-aligned boxes overlap exactly when their centres are closer than one box in both axes, so it is an answer rather than a proxy, and it looks at every pair. The second cause was the small blind badge, sitting at `left: -6px` — hanging off its own puck's left corner onto whatever was beside it. On an iPhone Pro Max at six seats, with the pucks not touching at all, "SB" was painted across the next player's $1,137. All three badges now sit inside the puck's width, and in the same place as each other: a seat only ever wears one, so two positions bought nothing except making the dealer's eye find which of two spots this seat was using before it could read which blind it is. Then the layout had to be able to act on the answer, and it couldn't. It chose between two widths — 96px or 58px — and if neither fitted it widened the ring and let the seats overlap, on the reasoning that a dealer can see an overhang but cannot see a tap landing on the wrong player. That trade is real; it was also a false choice, because the tables that overlapped did not want either number. At nine seats a Galaxy S8 had room for a 61px puck, an iPhone 14 for 68, a Pixel 7 for 74, an iPhone Pro Max for 79. Three media queries declared three more of these numbers — 78px, 64px, 62px — at three breakpoints, guessing at something a media query cannot see: the same 390px phone has room for a 90px puck at four seats and 61 at nine. So the width is searched for now, not chosen. render.js walks candidates from the top, applies each one, measures what the browser actually drew, and takes the widest that leaves every seat its own box. There was a width that worked on every table in the sweep; the code was never asking for it. Ten candidates is ten forced layouts, so the result is cached against the felt's size, the seat count and the names — measured 12.5ms cold, 0.8ms warm, and renderSeats runs after every tap. Three things this got wrong on the way, all caught by measuring rather than reading: The badge overhang was folded into the space between two seats. That collapsed the ring on a phone held sideways — 87px of height, and 18px of badge is most of the room the ring has to move in — until a six-handed landscape table was unsatisfiable at any width. It didn't belong there: the badge points *away* from the table at both ends, so two seats facing each other across the felt wear them back to back. box-sizing: border-box, which the puck needed so the searched number means the width it is drawn at, quietly turned every min-height from a content height into a drawn one. An empty chair went from a 52px tap target to a 42px one on every phone in the sweep. The audit found it; the four min-heights have each gained the padding and border they used to be quoted without. The new audit check reported a name under a neighbour on a table whose seats were 9px apart. A puck's name is a `-webkit-box` with `line-clamp`, and getClientRects() hands back a rect for every line the text *would* have taken, including the ones the clamp throws away — so the probe was hit-testing 30px below the puck, where nothing is painted. It clips to what is painted now. uiAudit gains "no seat covers another seat's writing", the complement of the check already there for a puck's own decoration on its own name — which is why nothing caught this: seatLabelsClear() only ever looked inside one seat. Fault-injected against the previous version: eighteen configurations, naming both causes. And the sweep itself now runs two table shapes. It had run one for its whole life — five players at a nine-handed table — and how crowded the felt is depends on how many people are sitting down far more than on anything a viewport can report. A full table was never once looked at. 632 tests, 0 failures. 190 audits across thirteen viewports at two table shapes, with a mouse and with a thumb. Every table from two seats to twelve now keeps its own text on every viewport in the sweep. One configuration still reports: a dozen players on a 640x360 phone, where the felt is 87px tall and two rows of 55px pucks cannot both fit and stay on the rail — the ring grows 8px past the edge to keep the seats off each other, which is the trade the old code named and got right. Left reported rather than tuned away. Names on a crowded phone table truncate harder than they did, because the puck is now sized to fit instead of allowed to overlap: "Christopher" is "Christ…" at nine seats on an iPhone 14. The full name is in the DOM, in the seat's form and in the sidebar. The next lever on that is the seat layer's inset, which is still a fixed 44px of "half a puck" for a puck that is no longer 96px wide — worth 54px to 60px where it was measured. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
190ccebf6c |
Merge pull request 'Hold'em dealer: a reachable Confirm, a table that fits, offline, and a quieter first screen' (#117) from polish/holdem-offline-and-render into master
Build and Deploy / build-deploy (push) Successful in 1m42s
Reviewed-on: #117 |
||
|
|
c052ee32c8 |
Put the Confirm button back on screen
Opened the pot wizard — the one dialog in this tool that divides money — and its Confirm button was below a scroll. Not on a tall table, not on one screen: at every size I measured, with six players and a side pot. iPhone 14 390x844 244px of the dialog below the fold Galaxy S8 360x640 417px 844x390 landscape 583px desktop 1280x900 172px Nothing on screen said there was anything under the list of names, so a dealer with the table waiting had to scroll a list of players to find the only control that finishes the hand. Two goes at this, and the first was wrong. A sticky footer is the obvious answer and it cannot work here: the actions were the last child of the scrolling content, so their containing block ended exactly where they did and `bottom` had no room to lift them. Sticky on the *header* did work, for the mirror-image reason — its container extends below it — which is what made the failure confusing to read. Capping the player list wasn't enough either; the pot breakdown and the log still overflowed on a short screen. So the dialog is a proper column now: a title that doesn't move, a body that scrolls, and a footer outside the scroll. potWizard.js appends its buttons to the footer instead of into the body, and clears both together so a step can't inherit the previous step's buttons. Measured after: Confirm on screen and hit-testable at all six configurations, twelve-handed on a Galaxy S8 included, and the dialog itself no longer scrolls at all. The checkboxes are painted now, too. `accent-color` only ever coloured the tick — the box around it stayed the platform's own bright white square, which on a near-black dialog was the loudest thing on screen and the last raw control in a tool where everything else had been drawn. Same surface ramp, same corner radius, and a tick drawn with clip-path so it can't pick up a substituted font. uiAudit gains "an open dialog's buttons are on screen", which hit-tests rather than trusting geometry — a button can be inside the viewport and still be under something. Fault-injected: putting the footer back inside the scrolling body reports Confirm 105px below the fold. Two things checked and left alone, because they turned out to be right: the standings table hides three columns on narrow screens through a container query keyed to the sidebar rather than the viewport, which is why a desktop shows fewer columns than an iPad — the sidebar is the narrower box there. And In/Won/ Net read $0 for hands settled outside the wizard, which the comment above them already explains. Caught by my own guard on the way past: the token test read `scroll-padding` as `padding` and failed the spacing scale. Correct by its rules, wrong in substance — those values are a sticky bar's measured height, not a step in the rhythm — so the lookbehind now rejects a hyphen as well as a word character. Re-fault- injected to confirm it still catches a real ad-hoc gap. 619 tests, 0 failures. 65 layout audits across 13 viewports x 5 table sizes and 35 across the five overlays. The whole wizard walked end to end: side pot resolved, hand logged, $1,560 on the table before and after. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
1f3dda180f |
Make the table big enough for the game around it
Looked at the felt at three times scale and the seats were falling off it. Not
sitting at a rail — hanging past the outer edge of the table into the page,
half a puck on green and half on nothing. Measured:
1280x900, nine-handed five of nine seats past the rail, worst 49px
844x390, six-handed all six, worst 20px
430x932, six-handed two, worst 12px
360x640, nine-handed none
The ring sat at a flat 44% by 42% of the seat layer at every size, two numbers
picked against a phone. A fixed pair can only be right for one ratio of puck to
felt, and the felt's ratio changes with every viewport — so it was a different
amount of wrong on each one, which is exactly what makes something look like a
mistake rather than a choice.
The radius is derived now: the widest ring whose outermost seat still finishes
inside the table, worked out from the measured table, seat layer and puck. It
can only ever pull seats in — the old pair is kept as the ceiling — and it falls
back to that pair whenever there is nothing to measure, which is every call from
the unit tests and the first render before the felt is laid out.
Three things this turned up on the way, each caught by the sweep:
* The seat layer is not centred on the table. Its insets differ top from
bottom, so the first version — which assumed symmetry and worked from half
widths — left a phone with 12px still over the rail. It works in absolute
pixels against the nearer edge now.
* The radius and the puck size depend on each other: the ring is limited by
how big a puck is, the puck is sized by the `seatsTight` class, and that
class was decided from the gaps between seats, measured at a radius that
hadn't been worked out yet. Fixing the gap measurement then made a
nine-handed phone overhang that had been fine, because the two were chasing
each other. It is settled by trying the candidates in preference order
instead: the roomy layout if everything fits, the tight one if not, and if
neither fits, tight pucks on a ring widened only as far as it takes to stop
seats colliding — because two seats fighting over a tap is a worse failure
than a puck over the rail. A dealer can see an overhang; they cannot see
that the seat they pressed wasn't the one that answered.
* The spacing threshold was a flat 84px, the figure a 96px puck was judged
against. Asking a 68px tight puck to keep a 96px puck's distance is a test
it can never pass, so every landscape table fell through to the fallback and
took an overhang it didn't need. It is a ratio now, not a number.
Result: worst overhang anywhere goes 49px to 9px, and 59 of 65 configurations
are completely clean. Desktop, tablet and every phone held upright: nothing over
the rail at all.
Residual, reported rather than tuned away: six configurations still overhang by
1-9px, all of them landscape at 640x360 and 667x375, where the felt is about
110-125px tall and no ring puts readable pucks inside that. The audit check is
left strict so they stay visible.
uiAudit gains "seats and chips stay on the table", which runs at every viewport
in the sweep. Fault-injected: putting the flat 44/42 back reproduces the original
failure exactly, including seat 3 at 49px.
Faster, too, because the measuring got hoisted out of the per-seat loops while I
was in there:
renderSeats 7.3ms -> 1.8ms
renderAll 12.4ms -> 6.8ms
619 tests, 0 failures. 65 layout audits across 13 viewports x 5 table sizes.
Offline re-verified: a whole hand with the network cut, $800 conserved.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3ffce9d7c1 |
Stop the empty table shouting, and point at the next step
The first screen a dealer ever sees was the one nobody had looked at. Opened
fresh, with nothing saved, the tool showed:
* the word EMPTY, in capitals, ten times across the felt
* Resolve Pot as the accent-green primary — on a table with no players and no
pot, so the loudest control on screen was the one that could do least
* Next Hand, with no button to move
* no indication anywhere of what to do first, except a grey caption under the
felt saying "tap a seat to edit"
An empty state is the one screen a new user is guaranteed to meet, and this one
described itself ten times without once saying what to do.
**The felt stops repeating itself.** An open chair is already dashed instead of
solid, faded, and has no stack under it — that reads at a glance, which is how a
dealer looks at a felt, and it reads the same on the one open chair at a full
table, which is the case the label was actually for. The word is gone from all
ten, and the seat numbers underneath it are now the thing you see. What the word
carried for anyone reading by ear moves to an aria-label on the seat, which now
says more than the sighted version ever did: "Seat 3, empty", "Seat 1, Ann".
**The bar says what the table is.** It used to hide itself when nobody was
seated — silent at exactly the moment a dealer has never used this before. It
now says "Nobody's at the table yet." with one full-width button, Seat a player,
which opens the seat editor on the first free chair. That is the same panel
tapping a seat gives them, so the button teaches the gesture rather than
replacing it.
**Two dead controls stop pretending.** Resolve Pot and Next Hand are disabled
until somebody is at the table, which takes the accent green off the button that
cannot work and leaves the only green on screen on the one that can. Disabled
rather than hidden: hiding them would move the header the moment the first
player is named, and a control that vanishes teaches nothing about what it was
for. Next Hand's own explanatory title is kept and handed back when it
re-enables.
One bug of my own, found by walking the flow rather than by reading it: after
typing a name both header buttons stayed greyed out, still captioned "nobody is
at the table yet". commitSeatChange refreshes narrowly on purpose — a full
render on every keystroke takes the name field out from under the dealer
mid-word — and the comment directly above it already warns about this exact trap
for the action bar. The header needed the same line.
609 tests, 0 failures. 65 layout audits across 13 viewports x 5 table sizes,
0 failures — the empty table is now one of those sizes, and had never been
audited before. Re-verified with the network cut: the first-run screen, the CTA
and seating a player all work offline.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
5d5093f3cc |
Make the dealer tool work with no signal, and stop it shouting
Three findings this round, all measured on the running page.
**1. The tool needed the internet to exist.**
The night already survived anything — it lives in localStorage — but the page
did not. Reload in somebody's back room with one bar of signal and a dealer
holding a phone in one hand and a deck in the other got a browser error page
with nine players waiting. Verified: with no worker, an offline reload is
net::ERR_INTERNET_DISCONNECTED.
There is now a service worker (wwwroot/casino/sw.js) and a manifest, so the
tool installs to a home screen and runs with the network switched off. Measured
end to end: 19 assets cached from one ordinary visit, then with the context
offline the page loads, draws six seats, applies its CSS, and plays a whole
hand — blinds posted, four calls, $800 still on the table.
Three decisions worth writing down:
* The precache list is read off the document rather than kept in the worker.
Every stylesheet and script is already in the DOM; a hand-written file list
stays right until somebody adds a script tag, and then the tool is quietly
offline-broken until a dealer finds out at a table.
* No skipWaiting. A new worker taking over a live page can hand it assets
from a different release than the ones it started with, and this page loads
seventeen scripts that expect to agree with each other.
* Stale-while-revalidate for assets, not cache-first — see finding 3.
Scope is /casino/ rather than /casino/texasholdem/, because a scope has to be a
prefix of the page it controls and "/casino/texasholdem" does not start with
"/casino/texasholdem/". Anything in scope the worker wasn't told about goes
straight to the network.
Program.cs needed .webmanifest added to the MIME map: an unknown extension is
not served with the wrong type, it is not served at all, and a 404 manifest is
exactly the silent failure that stops a browser ever offering "Add to Home
Screen".
**2. A screen reader read out the whole action bar after every tap.**
The bar carried aria-live on the element whose entire contents render() rebuilds
each action. Measured, after one tap:
"PRE-FLOP Pot $15 9 players Bob · $250 behind 0:00 ↶ dealing the hand U
Fold Call $10 Raise"
Nothing in it is wrong; it is unusable at the speed a hand is dealt, and it is
the documented anti-pattern — a live region has to be a stable element whose
text changes, not a container that gets replaced. There is now a small
role="status" element that exists from page load and says one sentence:
"Dee to act, $10 to call. Pot $15." Guarded on the sentence rather than on the
state, because render() runs for many reasons that don't change whose turn it
is, and a live region handed the same words twice reads them twice.
**3. Every tap rebuilt ninety DOM nodes.**
renderSeats did `innerHTML = ""` and then cloned a template, attached three sets
of listeners and set a dozen attributes per chair — on every render, and
renderAll runs after every action. Profiled on a nine-handed table:
renderSeats 7.3ms -> 4.0ms
renderAll 12.4ms -> 9.5ms
The pucks are built once and updated in place, which is what the rest of that
function was already doing to their contents. A seat's node belongs to its index
for as long as the table is that size, so the listeners still close over the
index; only a change in the number of chairs rebuilds the row. Verified that
listeners don't stack: one click after six renders fires the hook exactly once,
and resizing 9 -> 4 -> 10 renumbers correctly.
Found along the way and deliberately left alone: ViewData["cssVersion"] renders
empty on every page of the site, so the ?v= cache-busting has never worked. It
is inert — Program.cs sends Cache-Control: no-store on all static files — and
the service worker was the only reason to care, which is why it uses
stale-while-revalidate instead. Noted in TexasHoldem.cshtml.
608 tests, 0 failures, including a new announcing.test.js. 52 layout audits
across 13 viewports x 4 table sizes: 0 failures, and the action bar still
doesn't change height mid-hand. Gestures re-verified after the seat refactor:
double-tap fold, long-press menu, give-the-button, and the active dealer chip.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
aaf96543a8 |
Merge pull request 'Hold'em dealer: blind fix, design tokens, and the decrowd' (#116) from fix/naming-a-blind-deals-the-hand into master
Build and Deploy / build-deploy (push) Successful in 1m36s
Reviewed-on: #116 |
||
|
|
2e9c55af21 |
Give the felt back the screen the chrome was using
Measured on a 390x844 phone, six-handed, same table before and after:
before after
header 193px 120px
felt starts at 225px 152px
controls on screen 27 18
bordered elements 37 22
text elements 61 49
A quarter of the screen went on chrome before the dealer saw the table.
**The header.** Six equally weighted buttons in two rows, of which two are
per-hand controls. The other four have gone where they belong rather than into
a "more" menu — that relocates crowding instead of removing it. Reset Table is
in the Session panel now, beside Clear history and the save/load pair: it is
the control that throws the night away and it was sitting in the same weight as
Resolve Pot, the most-pressed button in a session. Quick Split is down with
Common rulings, where the other "open something else" link already lived. The
way back to the site is a quiet link at the top, where a back link goes.
The subtitle is gone. It was one sentence of orientation on a tool used
nightly, and the landscape and short-phone rules already hid it — the layout
had decided it was expendable on every screen this is actually used on.
**Teaching copy, once.** Table Setup shipped open, so the first screen was the
most crowded one; it now opens only on a table nobody has set up yet, decided
once at load so it cannot reopen itself under a dealer who just shut it. The
gesture hint under the felt goes after the first deal of the session — keyed to
the hand number, not to bettingOpen, which was the obvious test and the wrong
one: it would have appeared and disappeared twice a hand, shunting the panel
stack up and down all night.
**Borders.** Panels had a border, a lighter background and a shadow — three
ways of drawing one edge. The standings had a rule under every row, which is
what makes a list read as ruled paper. Both drop the line and keep the
grouping; the standings header keeps its rule, because that one marks a change
of kind. Fields and buttons keep theirs: those say "interactive", which is not
decoration.
**The felt itself.** Names wrap to two lines instead of ellipsising, so
"Christopher" and "Mike from work" are readable rather than "Chri..." and "Mike
fro..." — the truncation was eating exactly the half a dealer typed to tell two
Mikes apart. And a stack of $1,234,567 rendered at 7px; it now drops the
thousands separators before it drops the type size, so the same number is eight
characters at 10px with every digit intact. The comment above fitAmount was
right that abbreviating loses precision — but precision the dealer cannot read
is not precision.
Two things tried and reverted, both measured:
* Reserving the second name line made every puck the height of the tallest
(69-73px became a uniform 88). Fifteen pixels on all nine seats is felt
spent on chrome, which is what this round is trying to give back.
* Dropping the name a type step to fit more on one line still did not fit
"Christopher" on the button seat, where the dealer chip takes 22px of the
row — so it cost legibility and bought nothing.
Known limit: a single word too long for a puck breaks across the two lines, and
gets a hyphen only where the browser has hyphenation dictionaries. Real phones
do; headless Chromium does not, so audit screenshots show "Christo / pher". The
whole name is visible either way, which it was not before.
599 tests, 0 failures. 52 layout audits across 13 viewports x 4 table sizes
(2, 6, 9, 12 seats) and 20 across the five overlays: 0 failures, and the action
bar still does not change height mid-hand at any of them. The back link was
caught by the audit at 56x13 when it stopped being a button; its hit area is
44px now, paid for with negative margins so the header keeps the 73px.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
8b43614b1c |
Give the stylesheet a vocabulary, and stop the bar moving
The tool read as crowded on screens where nothing overlapped, and the reason turned out to be arithmetic rather than taste: holdem.css used 42 different spacing values, 31 font sizes, 14 border-radii and 139 colours, and defined three custom properties. Every number had been picked by hand at the moment it was needed. A 0.42rem gap beside a 0.45rem gap beside a 0.5rem gap gives the eye no rhythm to lock onto, so it reads the whole surface as noise. So: constrained scales, and everything snapped to them. Seven spacing steps on an 8px rhythm, eight type sizes, four corners, and a four-step surface ramp that stays off pure black — shadows are invisible on #000 and white-on-black smears on the OLED phone this is used on. 661 values rewritten; colour mentions halved, 381 to 174. Nothing here changes what the tool does. Two things were measurably moving under the dealer's thumb, and both are fixed by reserving space rather than by animating anything: * The undo pill named the last action, so it resized on every single tap. Measured across one hand: 148, 110, 109, 108, 109, 110, 120px. It is now a fixed 10rem, with the label in its own element so it can ellipsize without truncating through the keycap beside it. * The action bar is fixed to the bottom, so anything that changes its height moves its top edge. The status row wraps, and it wrapped differently as the street and pot changed. It now holds two lines' worth of room whenever a hand is running. Across 13 viewports x 3 table sizes, the bar no longer changes height mid-hand at all. Numbers that change while the dealer watches — pot, stacks, bets, standings — are now set in tabular figures, so a pot ticking $18 to $21 stops shuffling its digits sideways. Two regressions the sweeps caught on the way, both from the snapping: * The Add Chips field cleared 44px by way of its font size plus its padding, so moving either dropped it to 42. It states its own min-height now; a touch target shouldn't be an accident of two other numbers. * The bar came out 2px shorter in landscape, which grew the felt, which lifted the top seat's dealer button 2px into the header. That clearance was structurally 1-7px, so the top seat inset became a floor rather than a ceiling instead of chasing the 2px. tests/holdem/designTokens.test.js keeps the scales honest — a new value has to be a step or an exception with a reason. Fault-injected four ways. 599 tests, 0 failures. 39 layout audits across 13 viewports and 3 table sizes, plus 35 across the five overlays: 0 failures. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
42b9e83e8f |
Don't deal the hand when the dealer names a blind
The seat menu's "Make small blind" / "Make big blind" / "Blinds follow the button again" all re-posted the blinds whenever `Betting.onlyBlindsIn(state)` was true. That condition means "nothing has happened in this hand except the blinds" — which is true of a hand one second old and equally true of a table sitting in setup with no cards out and nothing committed. So naming a blind before the deal ran repostBlinds -> beginHand: betting opened, two stacks docked, the action bar up and waiting on the seat after the big blind. The dealer had said where the blinds go and the tool heard "deal". Betting.blindsArePosted() is the question these three actually wanted — the blinds are down only once a hand is open — and it's the guard makeDealer was already using by hand. Before the deal, naming a blind now moves a badge and nothing else; the toast says "Dee has the small blind next hand." rather than claiming she posted it. Mid-hand behaviour is unchanged: chips come back and go in again from the new positions, and the chip count stays straight. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
419c1de2a0 |
Merge pull request 'Hold'em dealer: 55 polish rounds, one merge' (#101) from release/casino-holdem-polish into master
Build and Deploy / build-deploy (push) Successful in 1m32s
Reviewed-on: #101 |
||
|
|
470fbc0d26 | Add the seat-name round to the release | ||
|
|
c4e842f3e3 |
Stop the button's chip sitting on top of the name underneath it
A phone screenshot, mid-hand, eight-handed: the seat with the button reads "Christop" and then a white D. The name is centred across the whole puck and the chip sits in its top-right corner on a layer above, so a long name runs underneath and stops — measured, "Christopher" lost 20px of its 64px box. The bad part is the silence. The text believed it had the room it was being painted out of, so no ellipsis appeared: nothing on screen said the name went on. On the one seat at the table a dealer looks at most. The name now keeps out of the chip's corner on the seat that has the button, so a long one ends in an ellipsis before it gets there. Only that seat — every other puck has its full width, because the chip on those is invisible. And the audit has a check for it now. Nothing it had would have caught this: the occlusion check is about controls a thumb has to reach, and this is a label a dealer has to read. The new one measures every seat's name against its own chip and badges, and the fix was undone by hand to confirm it fires — "20px under button.dealerChip.active". 182 audits across 13 viewports, both input kinds, plain and straddled tables, every overlay: clean. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
1d9353c94e | Add the hidden-means-hidden round to the release | ||
|
|
8616636212 |
Make hidden mean hidden, and check that it does
Three rounds inside the money, so this one was a look at the thing. On a desktop
screenshot of a table mid-session there is a small empty box under the bank
check: 21 pixels wide, 44 tall, no text, no purpose. It is the "Take $600 from 3
players" button from three rounds ago, which sets `hidden` when there is nothing
to record — and `hidden` was losing.
The attribute carries its `display: none` from the browser's own stylesheet,
which is the weakest rule in the cascade, so any class that sets a display beats
it. Every button class here sets `inline-flex`. This is not a new discovery in
this file either: seven overlays already carry their own `[hidden] { display:
none }`, each one presumably found the same way. One global rule now covers all
of them, so there is no ninth.
The audit had been telling me. Its touch check reported the box as "could not be
probed (drawn 21x44)" — in the warnings, which my sweeps printed only sometimes
and never failed on. So the check now says the plainer thing: an element that
claims to be hidden and is drawn anyway is a fault, not a note. It is in runAll
with the rest, and the bug was put back by hand to confirm it fires.
The same warning stream had a second thing in it: the waiting list's Add button
was 43px wide. Three letters, one pixel under the rule, and a thumb doesn't know
the word is short.
182 audits across 13 viewports, both input kinds, every overlay and a table with
a rake, a cash-out, a waiting list and six logged hands on it: clean, and the
only note left is the one that says the 44px rule stood itself down on a mouse.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
6174981cdc | Add the assumed-contributions round to the release | ||
|
|
4db6f6e4d3 |
Take the chips the pot was priced with, not just pay them out
Two rounds of asking where the tool disagrees with itself, and here is the last of it — the one that was actually creating money. There are two ways a contribution gets in. Tapping the action moves chips: the bet leaves the stack, sits in front of the player, and is swept into their hand total. The other is the "without live betting" panel — a base pot amount, or chips counted out on a seat — for a dealer recording a hand they are watching rather than driving. Nothing leaves anybody's stack there. The pot wizard prices both the same way, and awarding it credits the winner. So in the second mode the table simply grew. Measured at a three-handed table with a $50 base pot: stacks 200, 200, 200 before, pot priced at $150, and afterwards 350, 200, 200 — six hundred dollars of chips became seven hundred and fifty, and the bank check, newly able to see it, blamed a rebuy nobody had made. So before the pot is pushed, whatever was assumed rather than taken comes off the stacks that assumed it, and is recorded where a swept bet lives. Chips already committed or swept are left alone — they have been paid once, and taking them twice is the same bug the other way round. A base pot on top of a live bet collects only the difference. Nobody is charged more than they are holding, and an empty chair or a seat sitting out is charged nothing at all. The same hand now ends 300, 150, 150: the table has the six hundred it started with, and the bank check is balanced before, during and after. 13 new tests, with the already-paid test disabled on purpose to confirm five of them catch the double charge. Walked through the UI in base-pot mode: pot badge $150, wizard pricing three seats at $50 each, and afterwards the felt holding exactly what it held before. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
0ad5592687 | Add the one-stack-rule round to the release | ||
|
|
1c2e716508 |
Ask the same two questions the same way everywhere
The round before this found the bank check and the standings reading a seat's own
stack where the felt reads the table's starting stack — two parts of the tool
disagreeing about how much money was on the table. That was worth a sweep, and
the sweep found more.
The colour-up panel had the same fault. It is the panel that says "count out
2×$100 for this player", and on a table opened the ordinary way — a starting
stack and some names — it showed nothing at all. The one table where every stack
is a round number made of the largest chips there are was the table it had
nothing to say about.
The other question, "is anybody in this chair", existed in four spellings: in the
seat count, in the seat menu twice, in the waiting list, and twice more in the
state module. Three of them looked at name, chips and cash; one also looked at a
hand being dealt. A chair that counts as occupied for one and empty for another
is how somebody's chips get given away — measured, a seat marked in hand with no
name and no stack yet was on offer to the next person in the waiting queue, on
top of whoever was already playing it.
Both questions now have one implementation and one name:
"What is this seat holding?" HoldemState.chipsHeldBy
"Is anybody in this chair?" HoldemState.seatInPlay
10 new tests, including one that pins the bank check, the standings and the
settle-up plan to the same total, and one that pins the seat count and the open
seats to the same set of empty chairs. The occupancy test was cut back to its old
three-part form on purpose to confirm the waiting-list case fails.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
236c52567e | Add the starting-stack accounting round to the release | ||
|
|
1689ecb4f0 |
Count the chips the table is actually playing with
"Starting stack" is how a night opens. The setup panel offers it and says what it means — "seats without their own stack start here" — and everything a dealer looks at reads it: the puck, the action bar, what a player is allowed to bet. The money side didn't. The bank check and the standings read `seat.stack` directly, which is null for exactly those seats. Measured on a three-handed table with a $200 starting stack and nobody's own stack typed in: the felt played for $600, the bank check reported no chips in play and pronounced itself balanced, and the standings — the list a dealer settles the night from — listed nobody at all. Three players, six hundred dollars, and the tool's own accounting said the table was empty. Both now ask the same question the felt asks. The check reports $600 in play against nothing taken, which is the truth: those chips have not been paid for. Empty chairs are excluded, or the starting stack would make every one of them look like somebody holding two hundred dollars — a ten-seat table would have opened with $2,000 of imaginary chips. Being told is only half of it. The verdict names the seats rather than saying "somebody's rebuy probably wasn't recorded", because the dealer's next move is to go to a particular chair and the tool knows which. And the seats holding the table's starting stack — the dealer's own instruction, said once in the setup panel — can be written down in one tap instead of six trips through Add chips. Only those: what somebody's forgotten rebuy was is a question only the dealer can answer, and guessing it would be the tool inventing money, which is the fault this round is about. 14 new tests, with the stack source and the empty-chair test broken on purpose to confirm they fail. Walked through in the browser from a cleared table: set the starting stack, name three seats, and the panel goes from "balanced, no chips" to "$600 more in chips than cash taken. Ann, Ben and Cal are holding chips with no buy-in written down" — then to balanced, with the buy-ins recorded and undoable. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
b8655be258 | Add the waiting-list round to the release | ||
|
|
55eb569d3b |
Keep the list of who's next, instead of leaving it on a napkin
A full home game always has two or three people standing behind the chairs, and the list of who's next lives on a napkin or in the dealer's head. That is where the argument an hour later comes from. The tool had just learned to open a chair — somebody cashes out and the seat is free — and had nothing to say about who should sit in it. The Session panel keeps the list in order of arrival, which is the only ordering anybody has ever agreed to. Whoever is at the front is offered the first open chair by number, and everybody else waits their turn: a list that isn't kept in order is a pile of names, and jumping the queue is the thing a dealer gets shouted at for. With the table full it says so rather than offering a seat that doesn't exist. The same offer appears on the empty chair's own menu — "Seat Priya" — because tapping the chair somebody is about to sit in is what a dealer's hand does anyway. Sitting somebody down puts their name on the chair and nothing else. No stack, no buy-in, not dealt in: they haven't paid yet, and inventing chips for them would put money on the table nobody has handed over, which is the one thing the bank check exists to catch. Add chips is the next thing the dealer does, and it is one tap away in the same menu. Two people called Mike are two people — the list is keyed by id, not by name. Names are trimmed, blanks are refused, and a saved list from a table that never had one comes back empty rather than undefined. 15 new tests, with the occupied-chair guard removed and a stack handed to the new player on purpose to confirm they fail. Walked through the browser: two names added to a full table, a player cashed out to open seat 2, the front of the queue seated there with nothing in front of them, the bank still balanced, and the second name taken off the list. 156 audits with the list populated, including a name long enough to test the row. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
7e420ce747 | Add the misdeal round to the release | ||
|
|
86886ac126 |
Take a hand back, which the rulings sheet has been asking for all along
This tool has told dealers to redeal on a misdeal since the rulings sheet was written — first card to the wrong seat, a seat entitled to a hand dealt out, one of the first two cards exposed — and it had no way to do it. The only control that ended a live hand was Next Hand, which says out loud that it takes the money off the table without paying anybody. That is the right answer for an abandoned pot and the wrong one for a misdeal, where the blinds and the calls belong to the people who put them in. Undo covers the first second of it: "↶ dealing the hand" is one tap. But the action history lives in memory, so a dealer who is three calls in, or who has refreshed the page mid-hand, has nothing — and a refresh mid-hand is exactly the situation this tool has spent rounds making survivable everywhere else. Misdeal appears in the header while a hand is out and goes away when it isn't: before the deal there is nothing to take back, and afterwards it would be offering to unwind a hand that has already been paid. It says what it will do to the money — "$25 goes back to the players who put it in, cards are cleared, and the same button deals again" — and adds a note when two or more players have already acted, because most rooms let a deal stand at that point. It doesn't refuse: which conditions make a misdeal is the dealer's call and the sheet is right there. What comes back is everything, including chips already swept into hand totals on an earlier street — the first version returned only the current street's and would have quietly kept a flop's betting. What stays is the button, the blinds, the straddle and anyone posting in: a misdeal is the same hand dealt again, not the next one, so the hand number doesn't move and nothing is logged. 13 new tests, with the sweep and the straddle broken on purpose to confirm they catch it. Measured through the UI: pot $25 with three players in, take it back, every stack is exactly $200 again, the bank still balances, and dealing again posts the same blinds on the same players from the same button. 156 audits with the button live, including a landscape header that stays one row at 46px. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
c36d7f60bd | Add the cash-out round to the release | ||
|
|
f966d8c340 |
Pay somebody out and keep the chair, the cash and the record straight
The most ordinary thing that happens at a home game after midnight — somebody counts up and goes home — and the tool had nothing for it. Freeing the chair meant wiping the seat by hand, and the bank check then declared their whole stack missing: "someone has paid for chips they haven't been given", for the rest of the night. Measured on a balanced table, cashing a player out with $350 in front of them left the check $350 short. Wiping their buy-in as well silenced it, at the price of losing their entire night from the standings and from the file the dealer hands over at the end. Cashing out is two facts, and both have to survive the empty chair: the box took their cash, and the box handed their chips back. Both are kept now, so the chair opens and the money still adds up. Cash out… is in the seat menu. It says what to count — "$350 — 3×$100, 2×$25" — what they bought in for, and whether they leave up or down, because that is the sentence the dealer says while their hands are moving. The bank check gains a Paid out line and reads "balanced ... once the $350 paid out to players who have left is counted", alongside the discards and the house's cut. The standings keep them with a stripe and the words "· paid out", so nobody counts money out to them twice at 2am, and the export marks them the same way. It refuses to pay anybody with money still in the middle: taking chips out of a pot other people can win, or handing the same chips over twice, are both worse than waiting for the hand to finish. The chair gets a fresh id, because the person who had it has gone and the next player to sit down is not them. Two things this turned up. The first draft worked out what a seat was holding by itself, read `Number(null)` as a perfectly good zero, and offered to pay a player with no stack set nothing at all — it asks Betting now, which owns that rule. The second was mine and the audit caught it: marking the departed row with opacity took the In and Won cells to 3.83:1, under AA, on the very list a dealer settles from. It is a stripe instead. 19 new tests, with the departed buy-in dropped from the check and the pot guard disabled on purpose to confirm they fail. Walked through in the browser: menu, confirm, payout, bank, standings, undo, and the refusal. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
15cec18256 | Add the clock round to the release | ||
|
|
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 |
||
|
|
9c8d981e2d | Add the house-take round to the release | ||
|
|
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 |
||
|
|
907e0165db | Add the stadium ring round to the release | ||
|
|
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 |
||
|
|
86e1570618 | Add the phone felt round to the release |