Make every seat on the ring the same seat, and measure it before believing it
Nine chairs, and until this no two of them were the same size. Measured at 1280x900 with five players seated: 64px for Bob, 70 for Christopher because his name wrapped, 71 for Alexandra because she had chips out, 89 for "Mike from work" who had both. The ragged edge was the smaller half of it. The larger half is that the sizes moved. A puck grew 17px the moment its player put money in and shrank again when the street closed, so the felt reflowed twice a hand under the thumb of someone trying to tap it — and because the ring is sized against the tallest puck on it, the whole table could resize with it. Three rows are held open instead: the name, the amount, and the stack. The name gets one line whatever it says, the amount pill keeps its row when it is empty rather than collapsing, and each row is a token rather than an em so an amount stepping its type down to fit shortens its digits and not its row. What the puck says changes all night; how big it is does not. It costs nothing. measuredSeatBox already took the tallest puck on the ring, on purpose, so the room was reserved at every seat and the short ones simply weren't drawn into it. What pays for the third row is the second line of name going away: an earlier round measured uniform-with-two-lines at 88px and rightly refused it. Uniform-with-one is 71. A name with no word to drop steps its type down instead of ellipsising. fitSeatNames turns "Mike from work" into "Mike", which is what the table calls him, and could do nothing at all with "Christopher" — so that one was cut to "Christo…" on a felt that may also have a Christine at it. The amounts on the same puck have never taken that trade; this is fitAmount's rule applied to the other thing a dealer reads across a table. One step, floored at 9px, then the ellipsis. The float that kept the button seat's name off the dealer chip goes with the second line — three rules of it, indenting the first line so a long name could drop below the chip onto a second. There is no second line to drop to, and left in place it drew "Christopher" as an empty label. The padding every crowded ring already used is the rule at all sizes now, widened from 22px to 25 so it actually covers the chip and the gap it is inset by. Then the part that was quietly wrong. renderSeats builds nine empty pucks and fills them afterwards, so the layout search ran against blank boxes and cached the answer under a key naming a table it had never measured. On a 390px phone that is a ring sized for empty seats — and the comment where it was cached said "let the resize that follows do this properly". Nothing resizes a phone. Turning the phone sideways and back was the only thing that ever fixed it, and it fixed it into a different layout: same viewport, same table, 90px pucks on load and 72px after a rotate. So the finished pucks are measured against the box the search settled on, and if they disagree the search is asked again with the real thing in front of it. Once, and the second answer is cached. Fresh load and rotate-and-back now agree exactly. That also lets the cache key shrink to almost nothing. It used to name every player's name, the dealer's seat, whether anybody had cards and whether anybody had chips out — four things, three of which flip during an ordinary hand, each flip a ten-candidate re-search. None of them can change a puck's height any more. Measured over three calls in a row: no re-search, and not one of the nine seats moved a pixel. The crowded ring gives up the 1.4 leading on its money rows to pay for the row it now holds open — 10px of figures is 12px of ink and the rest was blank. Without it the Galaxy S8 twelve-handed, where the pucks are already at the 44px floor, put neighbouring seats' blind badges down onto each other's "ALL IN". Hanging the badges further out was tried first and is the wrong direction: the badge landing on a stack figure belongs to the seat next door, so every pixel further out is a pixel further onto a neighbour. 992 unit tests. 320 layout audits across 13 viewports with a mouse and a thumb: one failure, the chip behind seat 11 that master already carries. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -1032,25 +1032,56 @@ button,
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Two lines, not an ellipsis.
|
||||
A 96px puck fits about nine characters on a line, so "Christopher" came out as
|
||||
"Chri…" and "Mike from work" as "Mike fro…" — and "from work" is the half a dealer
|
||||
typed to tell two Mikes apart, so the ellipsis was eating exactly the part that
|
||||
carried the information. Wrapping keeps both, and a name is the one thing on the
|
||||
felt read at a glance rather than looked up. */
|
||||
/* ---- The puck is a stack of rows, and every row is the height of a row ----
|
||||
|
||||
Nine chairs on a ring, and until this block no two of them were the same size. A puck
|
||||
was as tall as whatever happened to be written in it: measured at 1280x900 with five
|
||||
players seated, 64px for Bob, 70 for Christopher because his name wrapped, 71 for
|
||||
Alexandra because she had chips out, 89 for "Mike from work" who had both. Twenty-five
|
||||
pixels of difference around one ring, and the ragged edge was the smaller half of it.
|
||||
|
||||
The larger half is that the sizes *moved*. A puck grew 17px the moment its player put
|
||||
money in and shrank again when the street closed, so the felt reflowed twice a hand
|
||||
under a dealer's thumb — and the ring is sized against the tallest puck on it, which
|
||||
meant every one of those changes could resize the whole table.
|
||||
|
||||
Three rows are held open instead: the name, the amount, and the stack. Each is a token
|
||||
rather than an em, so an amount that steps its type down to fit (see .puckAmountLong)
|
||||
shortens its digits and not its row. What the puck says changes all night; how big it
|
||||
is does not.
|
||||
|
||||
Sized so it costs nothing. The ring already reserved room for the tallest puck at every
|
||||
seat — measuredSeatBox() takes the maximum, on purpose — so drawing the short ones at
|
||||
that height spends room that was already spent. What pays for the third row is the
|
||||
second line of name going away above: uniform-with-two-lines was 88px, which an earlier
|
||||
round measured and rightly refused. Uniform-with-one is 71. */
|
||||
#seatsLayer {
|
||||
--seatRowName: 15px; /* 12px of name at 1.2 */
|
||||
--seatRowMoney: 14px; /* 10px of figures at 1.4 */
|
||||
--seatNameSize: var(--fs-sm);
|
||||
}
|
||||
|
||||
/* One line, and the ellipsis is not what saves it.
|
||||
A 96px puck fits about nine characters, so "Christopher" came out as "Chri…" and
|
||||
"Mike from work" as "Mike fro…" — and "from work" is the half a dealer typed to tell
|
||||
two Mikes apart, so the ellipsis was eating exactly the part that carried the
|
||||
information. The second line was the first answer to that. fitSeatNames() is the
|
||||
better one: it drops whole trailing words, so the same name becomes "Mike", which is
|
||||
what the table calls him and reads at a glance rather than being looked up.
|
||||
|
||||
The second line is what made the pucks different sizes from each other, and its cost
|
||||
was never only the pixels. See the row heights below. */
|
||||
.seatPuckName {
|
||||
/* Not shrunk to buy a line. Dropping a step did fit another character and a half,
|
||||
and still didn't get "Christopher" onto one line on the button seat, where the
|
||||
dealer chip takes 22px of the row — so it bought nothing and cost legibility on
|
||||
the one label a dealer reads at a glance across a table. The name stays the size
|
||||
it was; the wrap deals with the length. */
|
||||
font-size: var(--fs-sm);
|
||||
/* The size a name is read at. Dropping a word is what deals with length first,
|
||||
because a word is a unit a dealer recognises and half a step of type is not; the
|
||||
type only moves for the name with no word left to drop — see .puckNameTight. */
|
||||
font-size: var(--seatNameSize);
|
||||
font-weight: 600;
|
||||
color: var(--text-bright);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
overflow: hidden;
|
||||
/* A single word longer than the puck has nothing to break on. `anywhere` was the
|
||||
first try and it gave "Christo / pher", which reads as a rendering fault; with
|
||||
@@ -1066,72 +1097,46 @@ button,
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
line-height: 1.2;
|
||||
/* Deliberately no reserved second line. Reserving one made every puck the height of
|
||||
the tallest — measured, 69-73px became a uniform 88 — and 15px added to all nine
|
||||
seats is fifteen more pixels of felt spent on chrome, which is the thing this
|
||||
round is trying to give back. A ragged ring is cheaper than a crowded one, and
|
||||
the ring was already ragged by 4px before any of this. */
|
||||
/* The row is as tall as the row, whether or not there is a name in it — see the
|
||||
token block above .seatPuckStack. An open chair's blank label held no height at
|
||||
all, so the one puck on the ring with nobody in it was the one puck a different
|
||||
size from the rest. */
|
||||
min-height: var(--seatRowName);
|
||||
margin-top: var(--sp-2);
|
||||
}
|
||||
|
||||
/* The button's chip sits in this puck's top-right corner, on a layer above the text, so
|
||||
the name has to keep out from under it.
|
||||
the name has to keep out from under it. The chip, plus the 3px it is inset by.
|
||||
|
||||
It used to do that with `padding-right: 22px`, which 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 wide, "Christopher" is 66px and fits, and on the seat holding the button it
|
||||
was cut to 56px and broke as "Christop / her". Mid-word, and no hyphen (see below).
|
||||
The button moves every hand, so that is a player whose name reads properly for four
|
||||
hands and comes apart on the fifth. On a Pixel 7 it went to three lines and the clamp
|
||||
ate the end of it.
|
||||
A float used to do this on the roomier rings — three rules of it, indenting the name's
|
||||
first line and letting a long one drop below the chip onto a second. That answer only
|
||||
existed because there was a second line to drop to. With one line there is nowhere to
|
||||
go, and a float would clip the name to nothing rather than shorten it: measured, the
|
||||
float plus a one-line clamp drew "Christopher" as an empty label, because the word
|
||||
cannot fit beside the chip and its whole line is then hidden.
|
||||
|
||||
A float is what indents only the lines it sits beside, which is the whole of the
|
||||
problem. 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. A short name still sits up beside the chip exactly as it did.
|
||||
|
||||
The padding stays for the crowded ring, and it has to. A tight puck gives the name one
|
||||
line on purpose — see `.seatsTight .seatPuckName` — and one line has nowhere to move
|
||||
to, so the only way to keep it off the chip there is to make the line shorter. Trying
|
||||
the float on both cost a whole sweep to find out: with two lines granted, the name
|
||||
dropped below the float 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 that has room for two lines, and the padding is for the
|
||||
one that doesn't. Two rules, matching the two regimes the puck already had. */
|
||||
So the padding is the rule at every size, which is what the crowded ring always used.
|
||||
What made padding unacceptable before is fixed where it belongs instead: at 1280x900
|
||||
the name box is 78px, "Christopher" is 66, and the button seat cuts it to 56 — so it
|
||||
was broken mid-word as "Christop / her" for the one hand in nine that player has the
|
||||
button. It now steps its type down and stays whole. See .puckNameTight. */
|
||||
.seat.seatHasButton .seatPuckName {
|
||||
padding-right: 22px;
|
||||
padding-right: 25px;
|
||||
}
|
||||
|
||||
/* Two things have to go with the float. It does nothing inside `display: -webkit-box`,
|
||||
so the clamp is replaced by the same two lines held as a max-height. And
|
||||
`overflow-wrap: break-word` has to stand down, because it is what stops the float
|
||||
working: it breaks a word to fit the short line rather than letting it move past the
|
||||
float to the long one, which is the whole manoeuvre.
|
||||
/* The name with no word left to drop.
|
||||
|
||||
Measured, at the name-box widths a roomy button seat gets:
|
||||
fitSeatNames() shortens "Mike from work" to "Mike", which is what the table calls him.
|
||||
It can do nothing at all with "Christopher", and what was left for that name was the
|
||||
ellipsis — "Christop…" — on a felt that may well also have a Christine at it.
|
||||
|
||||
78px (desktop, iPad) "Christopher" was 2 lines of 54+12 now 1 line of 66
|
||||
78px (desktop, iPad) "Katarzyna" was 2 lines of 49+6 now 1 line of 56
|
||||
|
||||
The float is one line tall, which is the height it has to be: tall enough to cover the
|
||||
chip's reach into the name — the chip's bottom edge is 26px down and the name's first
|
||||
line ends at 27.4 — and no taller, because a float that spills into the second line
|
||||
indents that one too and puts the whole problem back. */
|
||||
#seatsLayer:not(.seatsTight) .seat.seatHasButton .seatPuckName {
|
||||
padding-right: 0;
|
||||
display: block;
|
||||
max-height: 2.4em;
|
||||
overflow: hidden;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
#seatsLayer:not(.seatsTight) .seat.seatHasButton .seatPuckName::before {
|
||||
content: "";
|
||||
float: right;
|
||||
/* The chip, plus the 3px it is inset by. */
|
||||
width: 25px;
|
||||
height: 1.2em;
|
||||
This is the rule the amounts have had all along, applied to the other thing a dealer
|
||||
reads across a table: shrink rather than abbreviate, because a name read wrong is a
|
||||
worse failure than a name read small. One step only, and floored, because past about
|
||||
9px it stops being legible from across a table and an honest ellipsis is better than
|
||||
an unreadable word. The row it sits in does not move — see --seatRowName. */
|
||||
.seatPuckName.puckNameTight {
|
||||
font-size: max(9px, calc(var(--seatNameSize) * 0.85));
|
||||
}
|
||||
|
||||
.seatPuckCards {
|
||||
@@ -1156,8 +1161,17 @@ button,
|
||||
background: rgba(9, 255, 0, 0.1);
|
||||
border-radius: var(--r-pill);
|
||||
padding: 0 var(--sp-1);
|
||||
min-height: 1.1em;
|
||||
/* The token, not an em: .puckAmountLong steps this pill's type down so a tournament
|
||||
stack keeps every digit, and an em would have taken the row down with it. */
|
||||
min-height: var(--seatRowMoney);
|
||||
line-height: 1.4;
|
||||
/* An inline-block sits on its own last line's baseline, and an empty one has no
|
||||
last line — so it sits on its bottom edge instead and the line box comes out 2px
|
||||
shorter than the same pill with a figure in it. Which put the two seats that had
|
||||
money out 2px below everyone else: the one difference this row was reserved to
|
||||
remove, reappearing as its own mirror image. Aligned to the top, the pill is the
|
||||
same box whether or not there is anything written in it. */
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* Neither row has anything in it most of the time, and both were holding their space
|
||||
@@ -1176,15 +1190,32 @@ button,
|
||||
less height to fit on the ring.
|
||||
|
||||
`:empty` is exact here: both rows are written by updatePuckContent, which sets an
|
||||
empty string rather than a space when there is nothing to say. What the collapse
|
||||
needs from the layout is that the ring re-measures when one of them fills — see
|
||||
seatShapeSignature() in render.js, which now names both. */
|
||||
empty string rather than a space when there is nothing to say.
|
||||
|
||||
The cards row still collapses, and the amount pill no longer does. They are not the
|
||||
same case, and the difference is how often each one changes.
|
||||
|
||||
Hole cards are a whole-table decision — a dealer either tracks them or doesn't, and
|
||||
for most tables the row is empty from the first hand to the last. Holding 15px open on
|
||||
nine seats all night for a feature the table isn't using is the crowding that comment
|
||||
above is about.
|
||||
|
||||
Chips are the opposite. Somebody has money out for most of every hand, and *which*
|
||||
seats changes on every action. Collapsing that row is what made a puck grow 17px when
|
||||
its player called and shrink again when the street closed, under the thumb of someone
|
||||
trying to tap it — and because the ring is sized against the tallest puck, the whole
|
||||
table could resize with it. The pill keeps its row and hides its paint instead, so the
|
||||
lozenge that comment is about never appears and the row underneath never moves.
|
||||
|
||||
Visibility rather than display, because that is the difference between a box that
|
||||
holds its place and a box that isn't there. It leaves the accessibility tree either
|
||||
way, which is right: there is nothing to announce. */
|
||||
.seatPuckCards:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.seatPuckTotal:empty {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dealerChip {
|
||||
@@ -1325,6 +1356,17 @@ button,
|
||||
padding take it from 15px to 12, which clears the name at 5px of overhang instead of
|
||||
9 — and four pixels off what every seat is charged is a whole size step of puck back.
|
||||
The label is the same 10px bold it always was; what went was the air around it. */
|
||||
/* How far it hangs off the edge, and the number matters in two places at once: this is
|
||||
what render.js's SEAT_BADGE_OVERHANG_PX reserves between two seats on the ring, so the
|
||||
two have to keep agreeing. seatFootprint.test.js holds them to it.
|
||||
|
||||
Hanging it further out was tried, to stop it reaching into the writing on its own puck,
|
||||
and it is the wrong direction: on the crowded rings the badge that lands on a stack
|
||||
figure is the one belonging to the seat *next door*, so every pixel further out is a
|
||||
pixel further onto a neighbour. Measured on a Galaxy S8 twelve-handed with everyone
|
||||
all-in, -8px took the worst case from four of seven probes across an "ALL IN" to five.
|
||||
The room a crowded puck needs comes out of its own leading instead — see
|
||||
--seatRowMoney on .seatsTight. */
|
||||
.blindBadge {
|
||||
display: none;
|
||||
position: absolute;
|
||||
@@ -1442,12 +1484,29 @@ button,
|
||||
hasn't got — it already gives up the hole cards to fit — and a puck that grew taller
|
||||
here would push its neighbours into each other, which is the one thing the tight
|
||||
layout exists to stop. */
|
||||
/* The clamp used to be here, because this was the only ring that gave the name one line.
|
||||
Every ring does now, so what is left is the type and the tighter row it sits in. */
|
||||
/* Tighter leading on both money rows, which is where a crowded ring pays for holding
|
||||
the amount row open. 1.4 is a comfortable line for a figure with room around it and
|
||||
this ring has none: a 10px figure is 12px of ink, and the extra 2px was blank.
|
||||
|
||||
Four pixels off the puck, and it is the four that keep this ring where it was. The
|
||||
Galaxy S8 twelve-handed is the tightest table in the sweep — the pucks are already at
|
||||
the 44px floor there — and at 58px tall the neighbouring seats' badges came down onto
|
||||
each other's stack figures. At 54 they don't, which is the height that table's pucks
|
||||
measured before the amount row was held open at all. */
|
||||
#seatsLayer.seatsTight {
|
||||
--seatRowName: 12px; /* 10px of name at 1.2 */
|
||||
--seatRowMoney: 12px; /* and 10px of figures at 1.2, not 1.4 */
|
||||
--seatNameSize: var(--fs-2xs);
|
||||
}
|
||||
|
||||
.seatsTight .seatPuckTotal {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.seatsTight .seatPuckName {
|
||||
margin-top: var(--sp-1);
|
||||
font-size: var(--fs-2xs);
|
||||
-webkit-line-clamp: 1;
|
||||
line-clamp: 1;
|
||||
min-height: 0;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@@ -1480,8 +1539,10 @@ button,
|
||||
|
||||
Steps on the scale rather than a clamp(), for the same reason the tight set is: the
|
||||
width is a continuum and the type is a set of deliberate sizes. */
|
||||
.seatsRoomy .seatPuckName {
|
||||
font-size: var(--fs-md);
|
||||
#seatsLayer.seatsRoomy {
|
||||
--seatRowName: 18px; /* 15px of name at 1.2 */
|
||||
--seatRowMoney: 17px; /* 12px of figures at 1.4 */
|
||||
--seatNameSize: var(--fs-md);
|
||||
}
|
||||
|
||||
.seatsRoomy .seatPuckStack,
|
||||
@@ -3676,6 +3737,9 @@ body.hasActionBar #holdemApp {
|
||||
font-variant-numeric: tabular-nums;
|
||||
margin-top: var(--sp-0);
|
||||
white-space: nowrap;
|
||||
/* Held open on the empty chairs too, so an open seat is the same puck as a taken
|
||||
one with nothing written in it — which is what an open seat is. */
|
||||
min-height: var(--seatRowMoney);
|
||||
}
|
||||
|
||||
/* "ALL IN" takes the stack line's place, so it has to not read as a stack. Wider
|
||||
|
||||
@@ -631,10 +631,11 @@ const Render = {
|
||||
SEAT_CLEARANCE_PX: 4,
|
||||
// And between a puck's inner edge and the chip sitting in front of it.
|
||||
BET_CLEARANCE_PX: 5,
|
||||
// How far a blind badge hangs off the puck it belongs to — the `top: -9px` and
|
||||
// `bottom: -9px` in holdem.css. It points away from the table at both ends, so it
|
||||
// takes no room between two seats; what it must not do is reach *sideways*, which is
|
||||
// what seatFootprint.test.js holds the stylesheet to.
|
||||
// How far a blind badge hangs off the puck it belongs to — the `top` and `bottom`
|
||||
// on .blindBadge in holdem.css, which seatFootprint.test.js holds this to. It points
|
||||
// away from the table at both ends; what it must not do is reach *sideways*, which
|
||||
// is the other thing that test checks.
|
||||
//
|
||||
SEAT_BADGE_OVERHANG_PX: 5,
|
||||
|
||||
// How much space two neighbouring seats need between their centres.
|
||||
@@ -792,29 +793,36 @@ const Render = {
|
||||
return widths;
|
||||
},
|
||||
|
||||
// What the table has changed about the shape of its pucks. Anything that can make a
|
||||
// name wrap to a second line belongs here, because the height that comes back from
|
||||
// measuring is what the ring is sized against.
|
||||
// What the table has changed about the shape of its pucks — which is now almost
|
||||
// nothing, and that is the point.
|
||||
//
|
||||
// The hole-card line and the amount pill belong here for the same reason, now that
|
||||
// an empty one collapses instead of holding its space: a puck with either of them
|
||||
// filled is about 15px taller than the same puck without, and a ring still sized
|
||||
// from the measurement taken before the first chips went in is sized for a puck
|
||||
// that no longer exists.
|
||||
// This used to name every player's name, the dealer's seat, whether anybody had hole
|
||||
// cards and whether anybody had chips out, because each of those could change how
|
||||
// tall a puck was and the ring is sized against the tallest puck on it. Four things,
|
||||
// three of which flip during an ordinary hand, and every flip is a ten-candidate
|
||||
// re-search: ten widths applied and read back, ten forced layouts, on a phone, while
|
||||
// somebody is waiting to know whether the bet is called.
|
||||
//
|
||||
// Booleans over the whole table rather than a list per seat, because that is what
|
||||
// the measurement is — measuredSeatBox takes the tallest puck on the ring, so what
|
||||
// matters is whether *anybody* has one. A boolean also flips about twice a hand
|
||||
// rather than on every action, and each flip costs ten forced layouts, which is the
|
||||
// expense this cache exists to avoid.
|
||||
// A puck's height no longer depends on any of them. The name gets one line whatever
|
||||
// it says, the amount pill holds its row whether or not there is an amount in it, and
|
||||
// the button's chip changes a padding rather than a line count — see the row-height
|
||||
// tokens in holdem.css. What is left is the hole-card row, which still collapses when
|
||||
// the table isn't tracking cards, because that is a decision a table makes once and
|
||||
// not a thing that changes under a dealer's thumb.
|
||||
//
|
||||
// A boolean over the whole table rather than a list per seat, because that is what
|
||||
// the measurement is: measuredSeatBox takes the tallest puck on the ring, so what
|
||||
// matters is whether *anybody* has one.
|
||||
//
|
||||
// Being wrong here is a stale ring rather than a wrong one, and it would not stay
|
||||
// wrong: renderSeats measures the finished pucks against the box the search settled
|
||||
// on and asks again if they no longer match. That check is what catches a fifth thing
|
||||
// nobody thought of, and it is what catches the first render of all — where the pucks
|
||||
// are built empty and the search that used to run against them was measuring nine
|
||||
// blank boxes and caching the answer under this table's key.
|
||||
seatShapeSignature() {
|
||||
if (!this.state) return "";
|
||||
const seats = this.state.seats;
|
||||
const anyCards = seats.some(p => (p.cards || []).some(Boolean));
|
||||
const anyAmount = seats.some(p => Betting.handTotal(p) > 0 ||
|
||||
HoldemState.computeContribution(p, this.state.chipLegend, this.state.basePotAmount) > 0);
|
||||
return `${seats.map(p => p.name || "").join("")}|${this.state.dealerSeatIndex}` +
|
||||
`|${anyCards ? "c" : ""}${anyAmount ? "a" : ""}`;
|
||||
return this.state.seats.some(p => (p.cards || []).some(Boolean)) ? "c" : "";
|
||||
},
|
||||
|
||||
// Which width the pucks get, and how wide a ring they sit on.
|
||||
@@ -871,14 +879,19 @@ const Render = {
|
||||
this.applySeatWidth(layer, width);
|
||||
const puck = this.measuredSeatBox();
|
||||
if (!puck) {
|
||||
// Nothing laid out yet — the very first render. Take the widest and let
|
||||
// the resize that follows do this properly.
|
||||
const radii = this.seatRingRadii();
|
||||
if (key) this.seatLayoutCache = { key, width, radii };
|
||||
return { width, radii };
|
||||
// Nothing on the felt to measure at all. Take the widest and say nothing
|
||||
// about it — no `puck`, so renderSeats' second pass knows this answer was
|
||||
// not measured and asks again once there is something to measure.
|
||||
//
|
||||
// It used to cache this one, on the reasoning that "the resize that
|
||||
// follows" would put it right. Nothing resizes a phone. A table opened
|
||||
// upright kept a ring measured before it had any seats on it until the
|
||||
// dealer turned the phone sideways and back.
|
||||
return { width, radii: this.seatRingRadii() };
|
||||
}
|
||||
candidates.push({
|
||||
width,
|
||||
puck,
|
||||
contained: this.seatRingRadii(puck),
|
||||
footprint: this.seatFootprint(puck),
|
||||
tooTall: this.tooTallForTheTable(),
|
||||
@@ -896,7 +909,10 @@ const Render = {
|
||||
rx: candidate.contained.rx + (this.RING_MAX_RX - candidate.contained.rx) * t,
|
||||
ry: candidate.contained.ry + (this.RING_MAX_RY - candidate.contained.ry) * t,
|
||||
};
|
||||
if (fits(candidate, radii)) { chosen = { width: candidate.width, radii }; break; }
|
||||
if (fits(candidate, radii)) {
|
||||
chosen = { width: candidate.width, radii, puck: candidate.puck };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,7 +921,11 @@ const Render = {
|
||||
// so out loud rather than the layout quietly papering over it.
|
||||
if (!chosen) {
|
||||
const last = candidates[candidates.length - 1];
|
||||
chosen = { width: last.width, radii: { rx: this.RING_MAX_RX, ry: this.RING_MAX_RY } };
|
||||
chosen = {
|
||||
width: last.width,
|
||||
radii: { rx: this.RING_MAX_RX, ry: this.RING_MAX_RY },
|
||||
puck: last.puck,
|
||||
};
|
||||
}
|
||||
|
||||
this.applySeatWidth(layer, chosen.width);
|
||||
@@ -1408,7 +1428,7 @@ const Render = {
|
||||
// these read the layout back, so they are taken once, here, before the loop
|
||||
// starts writing to it: asked per seat they would force the browser to
|
||||
// recompute the layout on every iteration.
|
||||
const { radii } = this.resolveSeatLayout(count);
|
||||
let { radii, puck } = this.resolveSeatLayout(count);
|
||||
const aspect = this.feltAspect();
|
||||
const { dealerIndex, sbIndex, bbIndex, manual: manualBlinds } = HoldemState.getBlindSeats(this.state);
|
||||
// A button on an empty chair normally means the table has drifted and wants
|
||||
@@ -1475,8 +1495,31 @@ const Render = {
|
||||
});
|
||||
|
||||
this.fitSeatNames(layer);
|
||||
// The names have just changed how tall some of these pucks are, so this comes
|
||||
// after them: a two-line name shortened to one line is 15px of height back.
|
||||
|
||||
// The pucks are only now what they will be drawn as, and the ring was sized
|
||||
// before any of it went in. Usually those agree — a puck's height is a stack of
|
||||
// rows the stylesheet holds open, and none of the writing above changes it.
|
||||
//
|
||||
// Twice they don't. The first render of all builds nine empty pucks and then
|
||||
// fills them, so a search run against that measured blank boxes; and any
|
||||
// assumption in seatShapeSignature that turns out to be wrong lands here too.
|
||||
// Both used to be permanent, because the answer was cached under a key naming a
|
||||
// table the measurement had never seen: a phone that never resizes kept a ring
|
||||
// sized for empty seats for the whole session.
|
||||
//
|
||||
// So the finished pucks are measured against the ones the ring was built from,
|
||||
// and if they have moved the search is asked again with the real thing in front
|
||||
// of it. Once. The second answer is cached and the render after this one reads it
|
||||
// back, which is what stops this being a loop.
|
||||
if (this.seatBoxMoved(puck)) {
|
||||
this.seatLayoutCache = null;
|
||||
({ radii, puck } = this.resolveSeatLayout(count));
|
||||
for (let index = 0; index < layer.childElementCount; index++) {
|
||||
this.placeSeat(layer.children[index], index, aspect, radii);
|
||||
}
|
||||
this.fitSeatNames(layer);
|
||||
}
|
||||
|
||||
const settled = this.settleSeatsOnTable(layer, aspect, radii);
|
||||
|
||||
// Kept so the board can ask for the chips to be laid out again if placing it
|
||||
@@ -1485,6 +1528,23 @@ const Render = {
|
||||
this.renderBets(aspect, settled);
|
||||
},
|
||||
|
||||
// Are the pucks on the felt still the puck the ring was measured for?
|
||||
//
|
||||
// A pixel of slack, the same as settleSeatsOnTable's, because the two measurements
|
||||
// are taken at different moments and a sub-pixel difference between them is a
|
||||
// rounding artefact rather than a table that has changed shape. Without it this
|
||||
// would ask the search to run again on every render for the rest of the session.
|
||||
//
|
||||
// No box at all means the search never measured one — the first render, before the
|
||||
// pucks were filled in. That is exactly the case worth asking again about.
|
||||
seatBoxMoved(puck) {
|
||||
if (!puck) return true;
|
||||
const now = this.measuredSeatBox();
|
||||
if (!now) return false;
|
||||
return Math.abs(now.width - puck.width) > this.SEAT_SETTLE_SLACK_PX ||
|
||||
Math.abs(now.height - puck.height) > this.SEAT_SETTLE_SLACK_PX;
|
||||
},
|
||||
|
||||
// One seat, onto the ring.
|
||||
placeSeat(node, index, aspect, radii) {
|
||||
const pos = this.seatPosition(index, this.state.seatCount, aspect, radii);
|
||||
@@ -1572,13 +1632,20 @@ const Render = {
|
||||
//
|
||||
// So a name that overflows drops whole trailing words until it fits: "Mike from
|
||||
// work" becomes "Mike from", then "Mike", and stops there with a real word rather
|
||||
// than a fragment. A single word that still doesn't fit is left to the ellipsis it
|
||||
// always had — there is no honest way to shorten "Christopher" that a dealer
|
||||
// glancing across a table would read faster than "Christo…".
|
||||
// than a fragment.
|
||||
//
|
||||
// Written as three passes over all the seats rather than a loop per seat: each pass
|
||||
// writes to every puck and then reads back from every puck, so the browser lays the
|
||||
// felt out three times whatever the table's size. Interleaved, nine seats would
|
||||
// What is left when there is no word to drop is one word too long for its puck, and
|
||||
// the ellipsis used to be the end of the story: "Christo…", which is a name only
|
||||
// until a Christine sits down. The amounts on the same puck have never accepted that
|
||||
// trade — see fitAmount, which steps a tournament stack's type down rather than
|
||||
// rounding it off — and a misread name is the same kind of failure as a misread
|
||||
// stack. So the last resort before the ellipsis is one step of type. Every name that
|
||||
// has one, not only the ones with a spare word, because "Christopher" is exactly the
|
||||
// case this is for.
|
||||
//
|
||||
// Written as passes over all the seats rather than a loop per seat: each pass writes
|
||||
// to every puck and then reads back from every puck, so the browser lays the felt out
|
||||
// a fixed number of times whatever the table's size. Interleaved, nine seats would
|
||||
// have cost nine layouts, and this runs on every render.
|
||||
fitSeatNames(layer) {
|
||||
if (!layer) return;
|
||||
@@ -1589,21 +1656,37 @@ const Render = {
|
||||
if (!el || !el.textContent) return;
|
||||
const full = (player.name || "").trim();
|
||||
// The tooltip is the whole name whether or not this one gets shortened: a
|
||||
// label that wraps to two clamped lines was already hiding some of it.
|
||||
// label clamped to its one line was already hiding some of it.
|
||||
el.title = full;
|
||||
const words = full.split(/\s+/).filter(Boolean);
|
||||
if (words.length > 1) labels.push({ el, words, kept: words.length });
|
||||
// Put back whatever a previous run of this took off, so what gets measured
|
||||
// below is the name against the puck it has now. Normally a no-op —
|
||||
// updatePuckContent has just written the full name — and not a no-op on the
|
||||
// second pass of a render that had to resolve the ring twice, which is the
|
||||
// one time a puck's width changes underneath a name already fitted to it.
|
||||
if (el.textContent !== full) el.textContent = full;
|
||||
// Cleared before anything is measured, so a name that fitted last render at
|
||||
// a smaller size is measured at the size it is meant to be read at. Left on,
|
||||
// it would be a ratchet: once a puck went small it could never come back.
|
||||
el.classList.remove("puckNameTight");
|
||||
labels.push({ el, words: full.split(/\s+/).filter(Boolean), kept: 0 });
|
||||
});
|
||||
if (!labels.length) return;
|
||||
labels.forEach(label => { label.kept = label.words.length; });
|
||||
|
||||
for (let pass = 0; pass < this.SEAT_NAME_PASSES; pass++) {
|
||||
const shorten = labels.filter(label => label.kept > 1 && this.overflowing(label.el));
|
||||
if (!shorten.length) return;
|
||||
if (!shorten.length) break;
|
||||
shorten.forEach(label => {
|
||||
label.kept = this.dropAWord(label.words, label.kept);
|
||||
label.el.textContent = label.words.slice(0, label.kept).join(" ");
|
||||
});
|
||||
}
|
||||
|
||||
// One read for the whole table, then one write, in that order — the same reason
|
||||
// the loop above is written in passes.
|
||||
labels
|
||||
.filter(label => this.overflowing(label.el))
|
||||
.forEach(label => label.el.classList.add("puckNameTight"));
|
||||
},
|
||||
|
||||
// Words a name can't end on. Dropping "work" off "Mike from work" leaves "Mike
|
||||
|
||||
@@ -51,11 +51,13 @@ const valuesIn = (properties, source = body) => {
|
||||
|
||||
describe("the spacing scale", () => {
|
||||
// Hairlines and structural clearances, each one deliberate:
|
||||
// 0px/1px/3px/4px/22px — borders, hairline offsets, and the 22px of room a seat
|
||||
// puck's name gives the dealer button beside it.
|
||||
// 0px/1px/3px/4px/25px — borders, hairline offsets, and the 25px of room a seat
|
||||
// puck's name gives the dealer button beside it: the chip's
|
||||
// own 22px plus the 3px it is inset by, which theRail's
|
||||
// "as wide as the chip it stands in for" holds it to.
|
||||
// 5rem/9rem — how far the page has to scroll past the fixed action
|
||||
// bar. Not rhythm; clearance for a thing of a known size.
|
||||
const ALLOWED = new Set(["0px", "1px", "2px", "3px", "4px", "22px", "5rem", "9rem"]);
|
||||
const ALLOWED = new Set(["0px", "1px", "2px", "3px", "4px", "25px", "5rem", "9rem"]);
|
||||
|
||||
test("every gap, margin and padding comes off the scale", () => {
|
||||
const offenders = valuesIn(
|
||||
@@ -79,7 +81,10 @@ describe("the type scale", () => {
|
||||
// every text input states it. Not a typographic choice.
|
||||
// 1.05em, 0.44rem — relative bumps inside a component, and the shrink-to-fit for
|
||||
// a six-figure stack on a 58px seat puck.
|
||||
const ALLOWED = new Set(["16px", "1.05em", "0.44rem"]);
|
||||
// 9px — the floor under the same shrink-to-fit applied to a name. It is a limit
|
||||
// rather than a size: past it the word stops being legible from across a
|
||||
// table and an honest ellipsis is the better answer.
|
||||
const ALLOWED = new Set(["16px", "1.05em", "0.44rem", "9px"]);
|
||||
|
||||
test("every font-size comes off the scale", () => {
|
||||
const offenders = valuesIn(["font-size"]).filter(v => !ALLOWED.has(v.value));
|
||||
|
||||
@@ -1,22 +1,28 @@
|
||||
// What the ring is allowed to be sized from.
|
||||
//
|
||||
// Two rows in every seat puck are usually empty — the hole-card line, and the pill that
|
||||
// shows what a player has put in. Both used to hold their space anyway, and the pill,
|
||||
// which has a background and a pill radius and side padding, painted an 8x11px green
|
||||
// lozenge when it had nothing to say: measured on seven of nine seats at an ordinary
|
||||
// table, a coloured mark on almost every seat standing for nothing.
|
||||
// resolveSeatLayout buys its answer with ten forced layouts — ten candidate widths, each
|
||||
// applied to the felt and read back — and caches it against this signature. So the
|
||||
// signature has to move whenever a puck's height can, and must not move otherwise: one
|
||||
// that changed on every action would throw the cache away on every tap, which is the
|
||||
// whole cost it exists to avoid.
|
||||
//
|
||||
// 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 seats had to sit. On
|
||||
// an iPhone 14 nine-handed the puck measured 78x88 with 25px of that reserved for
|
||||
// nothing; collapsed, the width search settles on 78x63. Twelve-handed on a Galaxy S8
|
||||
// the seats overlapped by 1556px² and now overlap by 87px².
|
||||
// It used to name four things: every player's name, the dealer's seat, whether anybody
|
||||
// had hole cards, and whether anybody had chips out. Three of those flip during an
|
||||
// ordinary hand. A dealer calling a bet paid ten forced layouts for it, and so did the
|
||||
// next street closing, and so did editing a name.
|
||||
//
|
||||
// The collapse is only safe if the layout re-measures when one of those rows fills,
|
||||
// because resolveSeatLayout caches its ten forced layouts against this signature. That
|
||||
// is what is checked here: the cache key has to move when the puck's shape can, and
|
||||
// stay still when it cannot — a signature that changed on every action would throw the
|
||||
// cache away on every tap, which is the cost it exists to avoid.
|
||||
// None of them can change a puck's height any more. The stylesheet holds three rows open
|
||||
// — name, amount, stack — at a height that does not depend on what is written in them:
|
||||
// the name gets one line whatever it says, the amount pill keeps its row when it is
|
||||
// empty, and the dealer's chip changes a padding rather than a line count. What is left
|
||||
// is the hole-card row, which still collapses, because whether a table tracks hole cards
|
||||
// is something it decides once rather than something that changes under a thumb.
|
||||
//
|
||||
// The other half of that bargain is in renderSeats: it measures the finished pucks
|
||||
// against the box the search settled on and asks again if they disagree. That is what
|
||||
// makes a wrong assumption here a stale frame rather than a stale session — and it is
|
||||
// what catches the first render of all, where the pucks are built empty and the search
|
||||
// that used to run against them was measuring nine blank boxes.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
@@ -32,37 +38,39 @@ const signatureOf = state => {
|
||||
const table = () => makeTable({ seatCount: 6, stack: 200 });
|
||||
|
||||
describe("the signature the ring layout is cached against", () => {
|
||||
test("moves when the first chips go in", () => {
|
||||
// Before the blinds every pill is empty and the pucks are short; after them two
|
||||
// seats are ~15px taller. A ring still sized from the first measurement is sized
|
||||
// for a puck that no longer exists.
|
||||
const state = table();
|
||||
const empty = signatureOf(state);
|
||||
state.seats[1].committed = 10;
|
||||
assert.notEqual(signatureOf(state), empty,
|
||||
"the ring would still be sized for pucks with no amount on them");
|
||||
});
|
||||
|
||||
test("and when a hole card is entered", () => {
|
||||
test("moves when a hole card is entered", () => {
|
||||
// The one row still allowed to collapse, and the one thing still worth
|
||||
// re-measuring for: a puck with cards on it is about 15px taller.
|
||||
const state = table();
|
||||
const none = signatureOf(state);
|
||||
state.seats[0].cards = ["AS", "KC"];
|
||||
assert.notEqual(signatureOf(state), none);
|
||||
});
|
||||
|
||||
test("but not again for the second player to put chips in", () => {
|
||||
// The whole point of a boolean over the table. The ring is sized from the
|
||||
// tallest puck on it, so once anybody has an amount the answer cannot change —
|
||||
// and re-measuring here would mean ten forced layouts on every single action.
|
||||
test("but not again for the second player to be dealt one", () => {
|
||||
// The point of a boolean over the table. The ring is sized from the tallest puck
|
||||
// on it, so once anybody has cards the answer cannot change.
|
||||
const state = table();
|
||||
state.seats[1].committed = 10;
|
||||
state.seats[0].cards = ["AS", "KC"];
|
||||
const first = signatureOf(state);
|
||||
state.seats[2].committed = 10;
|
||||
assert.equal(signatureOf(state), first);
|
||||
state.seats[3].committed = 250;
|
||||
state.seats[1].cards = ["2H", "7D"];
|
||||
assert.equal(signatureOf(state), first);
|
||||
});
|
||||
|
||||
test("and it stands still while the chips move", () => {
|
||||
// The expensive one. Somebody has money out for most of every hand and which
|
||||
// seats it is changes on every action, so this used to be a ten-layout re-search
|
||||
// twice a hand — and the pucks it was re-measuring for are the same height now
|
||||
// whether there is an amount in them or not.
|
||||
const state = table();
|
||||
const quiet = signatureOf(state);
|
||||
state.seats[1].committed = 10;
|
||||
assert.equal(signatureOf(state), quiet,
|
||||
"the amount pill holds its row whether or not it has an amount in it");
|
||||
state.seats[2].committed = 250;
|
||||
assert.equal(signatureOf(state), quiet);
|
||||
});
|
||||
|
||||
test("nor when an amount merely changes size", () => {
|
||||
const state = table();
|
||||
state.seats[1].committed = 10;
|
||||
@@ -72,54 +80,163 @@ describe("the signature the ring layout is cached against", () => {
|
||||
"a wider number is the amount fitter's problem, not the ring's");
|
||||
});
|
||||
|
||||
test("and it still moves for the things it always moved for", () => {
|
||||
// Renaming can wrap a name onto a second line, and the button's chip floats
|
||||
// into the name's first line, so both change how tall a puck measures.
|
||||
test("and it stands still for a rename", () => {
|
||||
// Renaming used to be able to wrap a name onto a second line. A name gets one
|
||||
// line now, and one that does not fit drops a word or steps its type down — see
|
||||
// fitSeatNames. Neither moves the row it sits in.
|
||||
const state = table();
|
||||
const base = signatureOf(state);
|
||||
state.seats[0].name = "Bartholomew Fairweather";
|
||||
const renamed = signatureOf(state);
|
||||
assert.notEqual(renamed, base);
|
||||
state.dealerSeatIndex = (state.dealerSeatIndex + 1) % state.seats.length;
|
||||
assert.notEqual(signatureOf(state), renamed);
|
||||
assert.equal(signatureOf(state), base);
|
||||
});
|
||||
|
||||
test("and two tables that differ only in where a name is split stay different", () => {
|
||||
// The names are joined on a control character no name can contain. Joined on
|
||||
// nothing, "ann" + "abel" and "anna" + "bel" are both "annabel" and one of those
|
||||
// tables would be laid out with the other's measurements — different names, so
|
||||
// different wrapping, so different puck heights.
|
||||
//
|
||||
// The first version of this used "Ann"/"Abel" against "Anna"/"Bel", which can
|
||||
// never collide: the capital letters land in different places, so it passed with
|
||||
// the separator taken out and proved nothing at all. Lower case is what makes it
|
||||
// a real test.
|
||||
const a = table();
|
||||
const b = table();
|
||||
a.seats[0].name = "ann"; a.seats[1].name = "abel";
|
||||
b.seats[0].name = "anna"; b.seats[1].name = "bel";
|
||||
assert.notEqual(signatureOf(a), signatureOf(b));
|
||||
test("and for the button moving round the table", () => {
|
||||
// The chip in the puck's corner takes 22px off the name's line. That is a
|
||||
// narrower name, not a taller puck, so the ring it sits on is the same ring.
|
||||
const state = table();
|
||||
const base = signatureOf(state);
|
||||
state.dealerSeatIndex = (state.dealerSeatIndex + 1) % state.seats.length;
|
||||
assert.equal(signatureOf(state), base);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a table nobody has sat down at", () => {
|
||||
test("has no amounts and no cards on it", () => {
|
||||
test("carries no flag at all", () => {
|
||||
// The empty-table case is the one the collapse is most visible on, and it is
|
||||
// also the one where a stray default could quietly keep both rows open forever.
|
||||
// also the one where a stray default could quietly keep the row open forever.
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 9);
|
||||
assert.match(signatureOf(state), /\|$/,
|
||||
"an untouched table should carry neither flag");
|
||||
assert.equal(signatureOf(state), "",
|
||||
"an untouched table should carry nothing");
|
||||
});
|
||||
|
||||
test("but a table with an ante on it does have an amount", () => {
|
||||
// computeContribution answers for a seat that has never been given a stack —
|
||||
// the table's base pot stands in for it — so a table dealt with "everyone antes
|
||||
// 5" has amounts on its pucks before anybody acts, and the ring has to know.
|
||||
test("and neither does one with an ante on it", () => {
|
||||
// computeContribution answers for a seat that has never been given a stack — the
|
||||
// table's base pot stands in for it — so a table dealt with "everyone antes 5"
|
||||
// has amounts on its pucks before anybody acts. That used to have to be in here.
|
||||
// The pucks are the same height with it as without, so it does not any more.
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 6);
|
||||
state.basePotAmount = 5;
|
||||
state.seats.forEach(p => { p.inHand = true; });
|
||||
assert.match(signatureOf(state), /a$/);
|
||||
assert.equal(signatureOf(state), "");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// The other half: the stylesheet has to actually hold those rows open, or the signature
|
||||
// above is a promise about a puck that doesn't exist.
|
||||
//
|
||||
// Read as text, the way the seat sheet's rules and the reduced-motion rules are. What
|
||||
// height a row is, is a decision that lives in the file — the browser is where it is
|
||||
// proved, and runUiAudit.js is what proves it there.
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const CSS = fs.readFileSync(
|
||||
path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"), "utf8");
|
||||
|
||||
const ruleFor = selector => {
|
||||
const escaped = selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const found = CSS.match(new RegExp(`(^|[},])\\s*${escaped}\\s*\\{([^}]*)\\}`, "m"));
|
||||
return found ? found[2] : null;
|
||||
};
|
||||
|
||||
describe("every puck on the ring is the same puck", () => {
|
||||
test("each of the three rows is a row high whether or not it says anything", () => {
|
||||
// A blank label had no height at all, so the one puck with nobody in it was the
|
||||
// one puck a different size from the rest — and a seat that had put chips in was
|
||||
// a different size again from one that hadn't.
|
||||
["name", "total", "stack"].forEach(row => {
|
||||
const selector = `.seatPuck${row[0].toUpperCase()}${row.slice(1)}`;
|
||||
const body = ruleFor(selector) || "";
|
||||
assert.match(body, /min-height:\s*var\(--seatRow(Name|Money)\)/,
|
||||
`${selector} does not hold its row open`);
|
||||
});
|
||||
});
|
||||
|
||||
test("and the row heights are tokens, not ems", () => {
|
||||
// fitAmount steps a tournament stack's type down so every digit stays on the
|
||||
// puck, and .puckNameTight does the same to a name with no word to drop. In ems
|
||||
// each of those would have taken its row down with it, and the puck with it.
|
||||
["--seatRowName", "--seatRowMoney"].forEach(token =>
|
||||
assert.match(CSS, new RegExp(`\\${token}:\\s*\\d+px`), `${token} is not a fixed height`));
|
||||
});
|
||||
|
||||
test("the amount pill hides its paint rather than giving up its place", () => {
|
||||
// Collapsing it is what made a puck grow 17px when its player called and shrink
|
||||
// again when the street closed. Painting an empty pill is what collapsing it was
|
||||
// introduced to stop — an 8x11px green lozenge on seven of nine seats, standing
|
||||
// for nothing. Neither, now: the row stays and the pill is not drawn.
|
||||
assert.match(ruleFor(".seatPuckTotal:empty") || "", /visibility:\s*hidden/);
|
||||
assert.doesNotMatch(ruleFor(".seatPuckTotal:empty") || "", /display:\s*none/);
|
||||
});
|
||||
|
||||
test("but the hole-card row still collapses, because a table decides that once", () => {
|
||||
// Holding 15px open on nine seats all night for a row most tables never fill is
|
||||
// the crowding this is meant to avoid, and it is the one thing left that the
|
||||
// signature above still watches for.
|
||||
assert.match(ruleFor(".seatPuckCards:empty") || "", /display:\s*none/);
|
||||
});
|
||||
|
||||
test("an empty pill sits on its top edge, not on a baseline it hasn't got", () => {
|
||||
// An inline-block sits on its last line's baseline, and an empty one has no last
|
||||
// line — so it sits on its bottom edge instead and the line box comes out 2px
|
||||
// shorter than the same pill with a figure in it. That put the seats with money
|
||||
// out 2px below everyone else: the one difference this row exists to remove,
|
||||
// reappearing as its own mirror image.
|
||||
assert.match(ruleFor(".seatPuckTotal") || "", /vertical-align:\s*top/);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("and the ring checks its own measurement afterwards", () => {
|
||||
// The signature above is an assumption about what can change a puck. This is what
|
||||
// happens when the assumption is wrong, and it is also what happens on the first
|
||||
// render of all: buildSeatNodes puts nine empty pucks on the felt and the search runs
|
||||
// before anything is written into them, so it was measuring nine blank boxes and
|
||||
// caching the answer under a key naming a table it had never seen.
|
||||
//
|
||||
// That used to be permanent. The comment where it was cached said "let the resize
|
||||
// that follows do this properly" — and nothing resizes a phone. A table opened
|
||||
// upright kept a ring measured for empty seats until the dealer turned the phone
|
||||
// sideways and back, which is the only thing that ever changed the key.
|
||||
const withBox = (box, run) => {
|
||||
const before = Render.measuredSeatBox;
|
||||
Render.measuredSeatBox = () => box;
|
||||
try { return run(); } finally { Render.measuredSeatBox = before; }
|
||||
};
|
||||
|
||||
test("a layout that measured nothing is always asked again", () => {
|
||||
// No box on the answer means the search never had a puck in front of it.
|
||||
assert.equal(withBox({ width: 72, height: 56 }, () => Render.seatBoxMoved(null)), true);
|
||||
assert.equal(withBox({ width: 72, height: 56 }, () => Render.seatBoxMoved(undefined)), true);
|
||||
});
|
||||
|
||||
test("one that measured the puck that is now on the felt is not", () => {
|
||||
assert.equal(
|
||||
withBox({ width: 72, height: 56 }, () => Render.seatBoxMoved({ width: 72, height: 56 })),
|
||||
false);
|
||||
});
|
||||
|
||||
test("and a pixel of difference is a rounding artefact, not a table that moved", () => {
|
||||
// The two measurements are taken at different moments. Without the slack this
|
||||
// would ask the search to run again on every render for the rest of the session,
|
||||
// which is ten forced layouts a tap — the exact cost the cache exists to avoid.
|
||||
assert.equal(
|
||||
withBox({ width: 72.4, height: 56.4 }, () => Render.seatBoxMoved({ width: 72, height: 56 })),
|
||||
false);
|
||||
assert.equal(
|
||||
withBox({ width: 72, height: 58 }, () => Render.seatBoxMoved({ width: 72, height: 56 })),
|
||||
true, "two pixels taller is a puck that has changed shape");
|
||||
});
|
||||
|
||||
test("nothing on the felt to measure is not a reason to ask again", () => {
|
||||
// Between a table being resized and the new pucks being built there is a moment
|
||||
// with no seats at all. Asking again there would clear a good answer and replace
|
||||
// it with one measured from nothing.
|
||||
assert.equal(withBox(null, () => Render.seatBoxMoved({ width: 72, height: 56 })), false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,53 +115,46 @@ describe("the bar lines up with the table", () => {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("the button seat's name", () => {
|
||||
// The dealer button's chip sits in the puck's top-right corner. Keeping the name out
|
||||
// from under it with `padding-right` 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". The button moves every hand, so that is a player
|
||||
// whose name reads properly for four hands and comes apart on the fifth.
|
||||
const roomy = CSS.match(/#seatsLayer:not\(\.seatsTight\) \.seat\.seatHasButton \.seatPuckName \{([^}]*)\}/);
|
||||
const float = CSS.match(/#seatsLayer:not\(\.seatsTight\) \.seat\.seatHasButton \.seatPuckName::before \{([^}]*)\}/);
|
||||
// The dealer button's chip sits in the puck's top-right corner, and the name has to
|
||||
// keep out from under it. `padding-right` takes the chip's width off the name's line,
|
||||
// which is the whole of the answer now that there is only ever one line.
|
||||
//
|
||||
// A float used to do this on the roomier rings — three rules of it, indenting the
|
||||
// first line so a name too long to fit beside the chip could drop below it onto a
|
||||
// second. That only worked because there was a second line. There isn't: see the row
|
||||
// heights in holdem.css, and the fitSeatNames tests below for what happens instead to
|
||||
// a name that does not fit.
|
||||
const base = CSS.match(/\n\.seat\.seatHasButton \.seatPuckName \{([^}]*)\}/);
|
||||
|
||||
test("a ring with room for two lines indents only the line beside the chip", () => {
|
||||
assert.ok(roomy && float, "the roomy regime should use a float");
|
||||
assert.match(float[1], /float:\s*right/);
|
||||
assert.match(roomy[1], /padding-right:\s*0/);
|
||||
test("the chip's width comes off the name, at every size", () => {
|
||||
assert.ok(base, "the base rule should reserve the chip's width");
|
||||
assert.match(base[1], /padding-right:\s*25px/);
|
||||
});
|
||||
|
||||
test("the float is exactly one line tall", () => {
|
||||
// Tall enough to cover the chip's reach into the name, and no taller: a float
|
||||
// that spills into the second line indents that one too, which puts the whole
|
||||
// problem back.
|
||||
assert.match(float[1], /height:\s*1\.2em/);
|
||||
});
|
||||
|
||||
test("break-word stands down where the float is doing the work", () => {
|
||||
// It is what stops the float working — it breaks a word to fit the short line
|
||||
// rather than letting it move past the float to the long one.
|
||||
assert.match(roomy[1], /overflow-wrap:\s*normal/);
|
||||
});
|
||||
|
||||
test("a crowded ring keeps the padding, because it has only one line", () => {
|
||||
// `.seatsTight .seatPuckName` is `line-clamp: 1` on purpose, and one line has
|
||||
// nowhere to move to — the only way to keep it off the chip is to shorten it.
|
||||
// Trying the float on both cost a whole sweep to find out: 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.
|
||||
assert.ok(base, "the base rule should still reserve the chip's width");
|
||||
assert.match(base[1], /padding-right:\s*22px/);
|
||||
assert.match(CSS, /\.seatsTight \.seatPuckName \{[^}]*line-clamp:\s*1/);
|
||||
});
|
||||
|
||||
test("the float is as wide as the chip it stands in for", () => {
|
||||
test("and it is as wide as the chip it stands in for", () => {
|
||||
const chip = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])width:\s*(\d+)px/);
|
||||
const inset = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])right:\s*(\d+)px/);
|
||||
const floatW = float[1].match(/width:\s*(\d+)px/);
|
||||
assert.ok(chip && inset && floatW);
|
||||
assert.equal(Number(floatW[1]), Number(chip[1]) + Number(inset[1]),
|
||||
"the float should cover the chip and the gap it is inset by");
|
||||
const pad = base[1].match(/padding-right:\s*(\d+)px/);
|
||||
assert.ok(chip && inset && pad);
|
||||
assert.equal(Number(pad[1]), Number(chip[1]) + Number(inset[1]),
|
||||
"the padding should cover the chip and the gap it is inset by");
|
||||
});
|
||||
|
||||
test("the float that used to do this is gone, not merely unused", () => {
|
||||
// Left in place with a one-line clamp it would clip the name to nothing: the
|
||||
// word cannot fit beside the float, so it moves to a second line, and the second
|
||||
// line is what the clamp hides. Measured — "Christopher" drew as an empty label.
|
||||
assert.equal(/\.seatPuckName::before/.test(CSS), false,
|
||||
"a float and one line of name cannot both be true");
|
||||
});
|
||||
|
||||
test("and no rule anywhere gives a puck's name a second line", () => {
|
||||
// This is what every puck being the same height rests on. A name is a label, and
|
||||
// the ring is sized against the tallest puck on it, so one name wrapping is one
|
||||
// seat setting how far apart all of them have to sit.
|
||||
const clamps = CSS.match(/(?<![\w-])line-clamp:\s*(\d+)/g) || [];
|
||||
assert.ok(clamps.length > 0, "the name should still be clamped to something");
|
||||
clamps.forEach(clamp => assert.match(clamp, /:\s*1$/, clamp));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -234,9 +227,17 @@ describe("a name that will not fit", () => {
|
||||
// ladder — which words go, in what order, and when it stops — and the browser's part
|
||||
// is one boolean per pass.
|
||||
const label = (text, fits) => {
|
||||
const classes = new Set();
|
||||
const el = {
|
||||
textContent: text,
|
||||
title: "",
|
||||
// The step of type a name takes when it has no word left to drop, held as
|
||||
// the same set of strings the real element's classList is.
|
||||
classList: {
|
||||
add: name => classes.add(name),
|
||||
remove: name => classes.delete(name),
|
||||
contains: name => classes.has(name),
|
||||
},
|
||||
// The browser's answer, standing in for a real one: this name fits if
|
||||
// `fits` says the version currently written to it does.
|
||||
get scrollWidth() { return fits(this.textContent) ? 10 : 100; },
|
||||
@@ -309,13 +310,16 @@ describe("a name that will not fit", () => {
|
||||
assert.equal(Render.dropAWord(["Mike"], 1), 1, "one word has nothing to drop");
|
||||
});
|
||||
|
||||
test("a single word is left to the ellipsis it always had", () => {
|
||||
// There is no honest way to shorten "Christopher" that a dealer glancing across
|
||||
// a table would read faster than "Christo…".
|
||||
test("a single word steps its type down rather than being cut", () => {
|
||||
// There is no honest way to shorten "Christopher" at a word, and "Christo…" is a
|
||||
// name only until a Christine sits down. The amounts on the same puck have never
|
||||
// taken that trade — fitAmount shrinks a tournament stack rather than rounding it
|
||||
// — and a misread name is the same kind of failure as a misread stack.
|
||||
const el = label("Christopher", () => false);
|
||||
seatsNamed("Christopher");
|
||||
Render.fitSeatNames(layer([el]));
|
||||
assert.equal(el.textContent, "Christopher", "never cut mid-word here");
|
||||
assert.ok(el.classList.contains("puckNameTight"), "and never left overflowing either");
|
||||
});
|
||||
|
||||
test("a name that fits is not touched", () => {
|
||||
@@ -323,6 +327,32 @@ describe("a name that will not fit", () => {
|
||||
seatsNamed("Mike from work");
|
||||
Render.fitSeatNames(layer([el]));
|
||||
assert.equal(el.textContent, "Mike from work");
|
||||
assert.ok(!el.classList.contains("puckNameTight"), "nothing to shrink for");
|
||||
});
|
||||
|
||||
test("a name shortened until it fits is not also shrunk", () => {
|
||||
// The step of type is the last resort, not a second helping. "Mike" fits, so it
|
||||
// is read at the size everything else on the felt is.
|
||||
const el = label("Mike from work", text => text === "Mike");
|
||||
seatsNamed("Mike from work");
|
||||
Render.fitSeatNames(layer([el]));
|
||||
assert.equal(el.textContent, "Mike");
|
||||
assert.ok(!el.classList.contains("puckNameTight"));
|
||||
});
|
||||
|
||||
test("the step comes off again when the puck it was for is gone", () => {
|
||||
// Otherwise it is a ratchet: a name that had to shrink on a nine-handed phone
|
||||
// would still be shrunk on the same table six-handed, or on a desktop, because
|
||||
// nothing ever measured it at the size it was supposed to be read at.
|
||||
const el = label("Christopher", () => false);
|
||||
seatsNamed("Christopher");
|
||||
Render.fitSeatNames(layer([el]));
|
||||
assert.ok(el.classList.contains("puckNameTight"));
|
||||
|
||||
const roomy = label("Christopher", () => true);
|
||||
roomy.classList.add("puckNameTight");
|
||||
Render.fitSeatNames(layer([roomy]));
|
||||
assert.ok(!roomy.classList.contains("puckNameTight"));
|
||||
});
|
||||
|
||||
test("the whole name is on the tooltip whether it was shortened or not", () => {
|
||||
|
||||
Reference in New Issue
Block a user