14fb68dc041bd72e76fcef3371440df42380592d
7
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
b9b2237dbe |
Run the two checks the audit had and never used
runAll() did not include the contrast check — it simply wasn't in the list — and
touch targets sat behind an opt-in flag nothing passed. So "the audit is clean"
has meant nine checks rather than eleven for a long run of rounds, and the two
left out are the two whose faults a dealer meets in a dim room with their thumb.
Both were written, tested, and then never called; the contrast checker was built
across two rounds and every sweep since has been silent about colour.
The standalone runner had a second version of the same hole: the 44px rule
stands itself down where `hover: hover` says there's a pointer, and a headless
desktop browser is a pointer, so a single pass had never measured a touch target
at any viewport. It now runs every viewport twice, once with a mouse and once
with a thumb.
A check that decides it doesn't apply now says so, rather than reporting an empty
offenders list — which reads exactly like a pass.
Faded text is measured instead of filed as unchecked. `opacity` on a box fades
the box and its text together onto whatever is behind it, so a dimmed seat is not
grey text on a dark puck: it is a faded puck and faded text both landing on the
page. Both halves are composited that way now. Two exemptions remain and both say
so out loud: a disabled control, which WCAG itself excuses, and a link inside a
sentence, whose size is set by the line it sits in.
What running them found, and this round fixes:
- the chair number on every seat: #8a8a8a at 9px, 3.39:1 once the empty seat's
fade is composited, on the label that answers "which seat is that?";
- Quick Split's amount field at 37px tall and its delete button at 30px across,
on a page whose targets had never been measured;
- and on a 360x640 phone with the bar grown by a side pot, the bottom seat
sitting 38px inside it.
That last one brings back a cap this project measured and rejected once: capping
the felt to the room left fixed the six-seat ring and broke the nine-seat one.
That trade was real then and isn't now — seats are spaced by distance around the
rim rather than by angle, so a shorter felt no longer piles them up. Re-measured
across 30 combinations of five phone sizes and 2 to 12 seats: every one clean
except twelve seats at 360x640, where a 240px felt and a 64px puck cannot both be
true. Where the rim can't give each seat a puck's width, the pucks come down a
size, measured from the rim itself rather than from a media query.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
2f6d63400a |
Stop re-typing the layout audit into a console every round
Last round I found a regression I'd shipped two rounds earlier: the standings table's player-name column was zero pixels wide, at every viewport, and nobody noticed because the audit that would have caught it only ever existed in a previous session's scrollback. These checks have been re-written by hand at least six times, and each time they covered only whatever had just changed. So they're a file now. uiAudit.js runs in the page — no build step, no package.json, no dependency — and checks touch-action and tap highlights, that fields stay selectable and are big enough not to trigger iOS zoom, that nothing important is covered or stranded off-screen, that no grid column has collapsed to nothing, and that no text is painted outside its box. runUiAudit.js drives it over the table, all five overlays and Quick Split at thirteen viewports. Deliberately outside CI: the unit suite is dependency-free and runs in a second, this one needs a browser and a running site. Three real faults it found on its first run Fields were 13.3px on every iPad. The 16px minimum that stops iOS zooming the page on focus went in inside a max-width:700px block, and every iPad is wider than that — so a dealer on a tablet, which is the device this is most likely to be propped up on, got the table zoomed every time they touched the blind amount. The reason for that rule was never "narrow screen", it was "iOS". Font size applies at all widths now, and the 44px touch targets key off hover:none, which is what actually distinguishes a finger from a mouse. The seat menu couldn't be scrolled. It had no height limit and no overflow, so on a phone held sideways its first two actions — Fold and All-in — sat below the bottom of the screen with no way to reach them. A dealer in landscape could long-press a seat and then not fold it. The pot wizard sat under the action bar. Every other overlay is z-index 90-98; the wizard alone was at 50, below both the action bar at 55 and the toast at 80. In landscape one of its inputs was completely covered by the Fold button, and a toast could sit over "Confirm Winner(s)". It was the odd one out rather than a decision. Three faults in the audit itself, found by checking before acting Worth recording, because each would have had me "fix" something that wasn't broken: - an ellipsised name was reported as painting outside its seat. A Range measures laid-out text, which runs past the element while overflow:hidden quietly clips it. Only overflow that is actually visible can paint over a neighbour. - every link below the fold was reported as off-screen. Something under the fold on a page that scrolls is reachable; the check now asks whether scrolling can bring it into view, which is the thing that actually matters. - a button in the newly-scrollable seat menu came back "covered by null". elementFromPoint returns null for a point outside a scrollable ancestor's clip — scrolled out of view, not covered by anything. A toast partly over a control is a warning rather than a failure now: it dismisses itself and carries a close button, and on a desktop it necessarily lands over a table that fills the content column. Fully covered by a toast is still a failure, because that was a real bug once — a seat sat exactly under the Undo button. Final sweep: 13 viewports, 65 overlay checks, 13 full-page audits, 4 Quick Split audits, zero failures, one warning. 137 unit tests still passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |