Hold'em: every seat on the ring is the same seat, and the ring is measured before it is believed #122
@@ -1032,25 +1032,56 @@ button,
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Two lines, not an ellipsis.
|
/* ---- The puck is a stack of rows, and every row is the height of a row ----
|
||||||
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
|
Nine chairs on a ring, and until this block no two of them were the same size. A puck
|
||||||
typed to tell two Mikes apart, so the ellipsis was eating exactly the part that
|
was as tall as whatever happened to be written in it: measured at 1280x900 with five
|
||||||
carried the information. Wrapping keeps both, and a name is the one thing on the
|
players seated, 64px for Bob, 70 for Christopher because his name wrapped, 71 for
|
||||||
felt read at a glance rather than looked up. */
|
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 {
|
.seatPuckName {
|
||||||
/* Not shrunk to buy a line. Dropping a step did fit another character and a half,
|
/* The size a name is read at. Dropping a word is what deals with length first,
|
||||||
and still didn't get "Christopher" onto one line on the button seat, where the
|
because a word is a unit a dealer recognises and half a step of type is not; the
|
||||||
dealer chip takes 22px of the row — so it bought nothing and cost legibility on
|
type only moves for the name with no word left to drop — see .puckNameTight. */
|
||||||
the one label a dealer reads at a glance across a table. The name stays the size
|
font-size: var(--seatNameSize);
|
||||||
it was; the wrap deals with the length. */
|
|
||||||
font-size: var(--fs-sm);
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-bright);
|
color: var(--text-bright);
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
-webkit-line-clamp: 2;
|
-webkit-line-clamp: 1;
|
||||||
line-clamp: 2;
|
line-clamp: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
/* A single word longer than the puck has nothing to break on. `anywhere` was the
|
/* 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
|
first try and it gave "Christo / pher", which reads as a rendering fault; with
|
||||||
@@ -1066,72 +1097,46 @@ button,
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
/* Deliberately no reserved second line. Reserving one made every puck the height of
|
/* The row is as tall as the row, whether or not there is a name in it — see the
|
||||||
the tallest — measured, 69-73px became a uniform 88 — and 15px added to all nine
|
token block above .seatPuckStack. An open chair's blank label held no height at
|
||||||
seats is fifteen more pixels of felt spent on chrome, which is the thing this
|
all, so the one puck on the ring with nobody in it was the one puck a different
|
||||||
round is trying to give back. A ragged ring is cheaper than a crowded one, and
|
size from the rest. */
|
||||||
the ring was already ragged by 4px before any of this. */
|
min-height: var(--seatRowName);
|
||||||
margin-top: var(--sp-2);
|
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 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
|
A float used to do this on the roomier rings — three rules of it, indenting the name's
|
||||||
line of the name — and the chip is on one of them. Measured at 1280x900: the name box
|
first line and letting a long one drop below the chip onto a second. That answer only
|
||||||
is 78px wide, "Christopher" is 66px and fits, and on the seat holding the button it
|
existed because there was a second line to drop to. With one line there is nowhere to
|
||||||
was cut to 56px and broke as "Christop / her". Mid-word, and no hyphen (see below).
|
go, and a float would clip the name to nothing rather than shorten it: measured, the
|
||||||
The button moves every hand, so that is a player whose name reads properly for four
|
float plus a one-line clamp drew "Christopher" as an empty label, because the word
|
||||||
hands and comes apart on the fifth. On a Pixel 7 it went to three lines and the clamp
|
cannot fit beside the chip and its whole line is then hidden.
|
||||||
ate the end of it.
|
|
||||||
|
|
||||||
A float is what indents only the lines it sits beside, which is the whole of the
|
So the padding is the rule at every size, which is what the crowded ring always used.
|
||||||
problem. Line one gets 56px and the rest get 78, so a name too long to fit beside the
|
What made padding unacceptable before is fixed where it belongs instead: at 1280x900
|
||||||
chip drops below it whole instead of being broken to fit next to something that isn't
|
the name box is 78px, "Christopher" is 66, and the button seat cuts it to 56 — so it
|
||||||
there. A short name still sits up beside the chip exactly as it did.
|
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. */
|
||||||
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. */
|
|
||||||
.seat.seatHasButton .seatPuckName {
|
.seat.seatHasButton .seatPuckName {
|
||||||
padding-right: 22px;
|
padding-right: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Two things have to go with the float. It does nothing inside `display: -webkit-box`,
|
/* The name with no word left to drop.
|
||||||
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.
|
|
||||||
|
|
||||||
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
|
This is the rule the amounts have had all along, applied to the other thing a dealer
|
||||||
78px (desktop, iPad) "Katarzyna" was 2 lines of 49+6 now 1 line of 56
|
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
|
||||||
The float is one line tall, which is the height it has to be: tall enough to cover the
|
9px it stops being legible from across a table and an honest ellipsis is better than
|
||||||
chip's reach into the name — the chip's bottom edge is 26px down and the name's first
|
an unreadable word. The row it sits in does not move — see --seatRowName. */
|
||||||
line ends at 27.4 — and no taller, because a float that spills into the second line
|
.seatPuckName.puckNameTight {
|
||||||
indents that one too and puts the whole problem back. */
|
font-size: max(9px, calc(var(--seatNameSize) * 0.85));
|
||||||
#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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatPuckCards {
|
.seatPuckCards {
|
||||||
@@ -1156,8 +1161,17 @@ button,
|
|||||||
background: rgba(9, 255, 0, 0.1);
|
background: rgba(9, 255, 0, 0.1);
|
||||||
border-radius: var(--r-pill);
|
border-radius: var(--r-pill);
|
||||||
padding: 0 var(--sp-1);
|
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;
|
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
|
/* 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.
|
less height to fit on the ring.
|
||||||
|
|
||||||
`:empty` is exact here: both rows are written by updatePuckContent, which sets an
|
`: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
|
empty string rather than a space when there is nothing to say.
|
||||||
needs from the layout is that the ring re-measures when one of them fills — see
|
|
||||||
seatShapeSignature() in render.js, which now names both. */
|
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 {
|
.seatPuckCards:empty {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatPuckTotal:empty {
|
.seatPuckTotal:empty {
|
||||||
display: none;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dealerChip {
|
.dealerChip {
|
||||||
@@ -1325,6 +1356,17 @@ button,
|
|||||||
padding take it from 15px to 12, which clears the name at 5px of overhang instead of
|
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.
|
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. */
|
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 {
|
.blindBadge {
|
||||||
display: none;
|
display: none;
|
||||||
position: absolute;
|
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
|
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
|
here would push its neighbours into each other, which is the one thing the tight
|
||||||
layout exists to stop. */
|
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 {
|
.seatsTight .seatPuckName {
|
||||||
margin-top: var(--sp-1);
|
margin-top: var(--sp-1);
|
||||||
font-size: var(--fs-2xs);
|
|
||||||
-webkit-line-clamp: 1;
|
|
||||||
line-clamp: 1;
|
|
||||||
min-height: 0;
|
|
||||||
text-overflow: ellipsis;
|
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
|
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. */
|
width is a continuum and the type is a set of deliberate sizes. */
|
||||||
.seatsRoomy .seatPuckName {
|
#seatsLayer.seatsRoomy {
|
||||||
font-size: var(--fs-md);
|
--seatRowName: 18px; /* 15px of name at 1.2 */
|
||||||
|
--seatRowMoney: 17px; /* 12px of figures at 1.4 */
|
||||||
|
--seatNameSize: var(--fs-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatsRoomy .seatPuckStack,
|
.seatsRoomy .seatPuckStack,
|
||||||
@@ -3676,6 +3737,9 @@ body.hasActionBar #holdemApp {
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
margin-top: var(--sp-0);
|
margin-top: var(--sp-0);
|
||||||
white-space: nowrap;
|
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
|
/* "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,
|
SEAT_CLEARANCE_PX: 4,
|
||||||
// And between a puck's inner edge and the chip sitting in front of it.
|
// And between a puck's inner edge and the chip sitting in front of it.
|
||||||
BET_CLEARANCE_PX: 5,
|
BET_CLEARANCE_PX: 5,
|
||||||
// How far a blind badge hangs off the puck it belongs to — the `top: -9px` and
|
// How far a blind badge hangs off the puck it belongs to — the `top` and `bottom`
|
||||||
// `bottom: -9px` in holdem.css. It points away from the table at both ends, so it
|
// on .blindBadge in holdem.css, which seatFootprint.test.js holds this to. It points
|
||||||
// takes no room between two seats; what it must not do is reach *sideways*, which is
|
// away from the table at both ends; what it must not do is reach *sideways*, which
|
||||||
// what seatFootprint.test.js holds the stylesheet to.
|
// is the other thing that test checks.
|
||||||
|
//
|
||||||
SEAT_BADGE_OVERHANG_PX: 5,
|
SEAT_BADGE_OVERHANG_PX: 5,
|
||||||
|
|
||||||
// How much space two neighbouring seats need between their centres.
|
// How much space two neighbouring seats need between their centres.
|
||||||
@@ -792,29 +793,36 @@ const Render = {
|
|||||||
return widths;
|
return widths;
|
||||||
},
|
},
|
||||||
|
|
||||||
// What the table has changed about the shape of its pucks. Anything that can make a
|
// What the table has changed about the shape of its pucks — which is now almost
|
||||||
// name wrap to a second line belongs here, because the height that comes back from
|
// nothing, and that is the point.
|
||||||
// measuring is what the ring is sized against.
|
|
||||||
//
|
//
|
||||||
// The hole-card line and the amount pill belong here for the same reason, now that
|
// This used to name every player's name, the dealer's seat, whether anybody had hole
|
||||||
// an empty one collapses instead of holding its space: a puck with either of them
|
// cards and whether anybody had chips out, because each of those could change how
|
||||||
// filled is about 15px taller than the same puck without, and a ring still sized
|
// tall a puck was and the ring is sized against the tallest puck on it. Four things,
|
||||||
// from the measurement taken before the first chips went in is sized for a puck
|
// three of which flip during an ordinary hand, and every flip is a ten-candidate
|
||||||
// that no longer exists.
|
// 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
|
// A puck's height no longer depends on any of them. The name gets one line whatever
|
||||||
// the measurement is — measuredSeatBox takes the tallest puck on the ring, so what
|
// it says, the amount pill holds its row whether or not there is an amount in it, and
|
||||||
// matters is whether *anybody* has one. A boolean also flips about twice a hand
|
// the button's chip changes a padding rather than a line count — see the row-height
|
||||||
// rather than on every action, and each flip costs ten forced layouts, which is the
|
// tokens in holdem.css. What is left is the hole-card row, which still collapses when
|
||||||
// expense this cache exists to avoid.
|
// 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() {
|
seatShapeSignature() {
|
||||||
if (!this.state) return "";
|
if (!this.state) return "";
|
||||||
const seats = this.state.seats;
|
return this.state.seats.some(p => (p.cards || []).some(Boolean)) ? "c" : "";
|
||||||
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" : ""}`;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Which width the pucks get, and how wide a ring they sit on.
|
// Which width the pucks get, and how wide a ring they sit on.
|
||||||
@@ -871,14 +879,19 @@ const Render = {
|
|||||||
this.applySeatWidth(layer, width);
|
this.applySeatWidth(layer, width);
|
||||||
const puck = this.measuredSeatBox();
|
const puck = this.measuredSeatBox();
|
||||||
if (!puck) {
|
if (!puck) {
|
||||||
// Nothing laid out yet — the very first render. Take the widest and let
|
// Nothing on the felt to measure at all. Take the widest and say nothing
|
||||||
// the resize that follows do this properly.
|
// about it — no `puck`, so renderSeats' second pass knows this answer was
|
||||||
const radii = this.seatRingRadii();
|
// not measured and asks again once there is something to measure.
|
||||||
if (key) this.seatLayoutCache = { key, width, radii };
|
//
|
||||||
return { width, radii };
|
// 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({
|
candidates.push({
|
||||||
width,
|
width,
|
||||||
|
puck,
|
||||||
contained: this.seatRingRadii(puck),
|
contained: this.seatRingRadii(puck),
|
||||||
footprint: this.seatFootprint(puck),
|
footprint: this.seatFootprint(puck),
|
||||||
tooTall: this.tooTallForTheTable(),
|
tooTall: this.tooTallForTheTable(),
|
||||||
@@ -896,7 +909,10 @@ const Render = {
|
|||||||
rx: candidate.contained.rx + (this.RING_MAX_RX - candidate.contained.rx) * t,
|
rx: candidate.contained.rx + (this.RING_MAX_RX - candidate.contained.rx) * t,
|
||||||
ry: candidate.contained.ry + (this.RING_MAX_RY - candidate.contained.ry) * 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.
|
// so out loud rather than the layout quietly papering over it.
|
||||||
if (!chosen) {
|
if (!chosen) {
|
||||||
const last = candidates[candidates.length - 1];
|
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);
|
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
|
// 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
|
// starts writing to it: asked per seat they would force the browser to
|
||||||
// recompute the layout on every iteration.
|
// recompute the layout on every iteration.
|
||||||
const { radii } = this.resolveSeatLayout(count);
|
let { radii, puck } = this.resolveSeatLayout(count);
|
||||||
const aspect = this.feltAspect();
|
const aspect = this.feltAspect();
|
||||||
const { dealerIndex, sbIndex, bbIndex, manual: manualBlinds } = HoldemState.getBlindSeats(this.state);
|
const { dealerIndex, sbIndex, bbIndex, manual: manualBlinds } = HoldemState.getBlindSeats(this.state);
|
||||||
// A button on an empty chair normally means the table has drifted and wants
|
// A button on an empty chair normally means the table has drifted and wants
|
||||||
@@ -1475,8 +1495,31 @@ const Render = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.fitSeatNames(layer);
|
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);
|
const settled = this.settleSeatsOnTable(layer, aspect, radii);
|
||||||
|
|
||||||
// Kept so the board can ask for the chips to be laid out again if placing it
|
// 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);
|
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.
|
// One seat, onto the ring.
|
||||||
placeSeat(node, index, aspect, radii) {
|
placeSeat(node, index, aspect, radii) {
|
||||||
const pos = this.seatPosition(index, this.state.seatCount, 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
|
// 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
|
// 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
|
// than a fragment.
|
||||||
// always had — there is no honest way to shorten "Christopher" that a dealer
|
|
||||||
// glancing across a table would read faster than "Christo…".
|
|
||||||
//
|
//
|
||||||
// Written as three passes over all the seats rather than a loop per seat: each pass
|
// What is left when there is no word to drop is one word too long for its puck, and
|
||||||
// writes to every puck and then reads back from every puck, so the browser lays the
|
// the ellipsis used to be the end of the story: "Christo…", which is a name only
|
||||||
// felt out three times whatever the table's size. Interleaved, nine seats would
|
// 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.
|
// have cost nine layouts, and this runs on every render.
|
||||||
fitSeatNames(layer) {
|
fitSeatNames(layer) {
|
||||||
if (!layer) return;
|
if (!layer) return;
|
||||||
@@ -1589,21 +1656,37 @@ const Render = {
|
|||||||
if (!el || !el.textContent) return;
|
if (!el || !el.textContent) return;
|
||||||
const full = (player.name || "").trim();
|
const full = (player.name || "").trim();
|
||||||
// The tooltip is the whole name whether or not this one gets shortened: a
|
// 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;
|
el.title = full;
|
||||||
const words = full.split(/\s+/).filter(Boolean);
|
// Put back whatever a previous run of this took off, so what gets measured
|
||||||
if (words.length > 1) labels.push({ el, words, kept: words.length });
|
// 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;
|
if (!labels.length) return;
|
||||||
|
labels.forEach(label => { label.kept = label.words.length; });
|
||||||
|
|
||||||
for (let pass = 0; pass < this.SEAT_NAME_PASSES; pass++) {
|
for (let pass = 0; pass < this.SEAT_NAME_PASSES; pass++) {
|
||||||
const shorten = labels.filter(label => label.kept > 1 && this.overflowing(label.el));
|
const shorten = labels.filter(label => label.kept > 1 && this.overflowing(label.el));
|
||||||
if (!shorten.length) return;
|
if (!shorten.length) break;
|
||||||
shorten.forEach(label => {
|
shorten.forEach(label => {
|
||||||
label.kept = this.dropAWord(label.words, label.kept);
|
label.kept = this.dropAWord(label.words, label.kept);
|
||||||
label.el.textContent = label.words.slice(0, label.kept).join(" ");
|
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
|
// 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", () => {
|
describe("the spacing scale", () => {
|
||||||
// Hairlines and structural clearances, each one deliberate:
|
// Hairlines and structural clearances, each one deliberate:
|
||||||
// 0px/1px/3px/4px/22px — borders, hairline offsets, and the 22px of room a seat
|
// 0px/1px/3px/4px/25px — borders, hairline offsets, and the 25px of room a seat
|
||||||
// puck's name gives the dealer button beside it.
|
// 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
|
// 5rem/9rem — how far the page has to scroll past the fixed action
|
||||||
// bar. Not rhythm; clearance for a thing of a known size.
|
// 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", () => {
|
test("every gap, margin and padding comes off the scale", () => {
|
||||||
const offenders = valuesIn(
|
const offenders = valuesIn(
|
||||||
@@ -79,7 +81,10 @@ describe("the type scale", () => {
|
|||||||
// every text input states it. Not a typographic choice.
|
// every text input states it. Not a typographic choice.
|
||||||
// 1.05em, 0.44rem — relative bumps inside a component, and the shrink-to-fit for
|
// 1.05em, 0.44rem — relative bumps inside a component, and the shrink-to-fit for
|
||||||
// a six-figure stack on a 58px seat puck.
|
// 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", () => {
|
test("every font-size comes off the scale", () => {
|
||||||
const offenders = valuesIn(["font-size"]).filter(v => !ALLOWED.has(v.value));
|
const offenders = valuesIn(["font-size"]).filter(v => !ALLOWED.has(v.value));
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
// What the ring is allowed to be sized from.
|
// 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
|
// resolveSeatLayout buys its answer with ten forced layouts — ten candidate widths, each
|
||||||
// shows what a player has put in. Both used to hold their space anyway, and the pill,
|
// applied to the felt and read back — and caches it against this signature. So the
|
||||||
// which has a background and a pill radius and side padding, painted an 8x11px green
|
// signature has to move whenever a puck's height can, and must not move otherwise: one
|
||||||
// lozenge when it had nothing to say: measured on seven of nine seats at an ordinary
|
// that changed on every action would throw the cache away on every tap, which is the
|
||||||
// table, a coloured mark on almost every seat standing for nothing.
|
// whole cost it exists to avoid.
|
||||||
//
|
//
|
||||||
// The space was not free either. seatRingRadii sizes the ring from the tallest puck on
|
// It used to name four things: every player's name, the dealer's seat, whether anybody
|
||||||
// it, so two rows nobody had filled were setting how far apart the seats had to sit. On
|
// had hole cards, and whether anybody had chips out. Three of those flip during an
|
||||||
// an iPhone 14 nine-handed the puck measured 78x88 with 25px of that reserved for
|
// ordinary hand. A dealer calling a bet paid ten forced layouts for it, and so did the
|
||||||
// nothing; collapsed, the width search settles on 78x63. Twelve-handed on a Galaxy S8
|
// next street closing, and so did editing a name.
|
||||||
// the seats overlapped by 1556px² and now overlap by 87px².
|
|
||||||
//
|
//
|
||||||
// The collapse is only safe if the layout re-measures when one of those rows fills,
|
// None of them can change a puck's height any more. The stylesheet holds three rows open
|
||||||
// because resolveSeatLayout caches its ten forced layouts against this signature. That
|
// — name, amount, stack — at a height that does not depend on what is written in them:
|
||||||
// is what is checked here: the cache key has to move when the puck's shape can, and
|
// the name gets one line whatever it says, the amount pill keeps its row when it is
|
||||||
// stay still when it cannot — a signature that changed on every action would throw the
|
// empty, and the dealer's chip changes a padding rather than a line count. What is left
|
||||||
// cache away on every tap, which is the cost it exists to avoid.
|
// 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 test = require("node:test");
|
||||||
const { describe } = require("node:test");
|
const { describe } = require("node:test");
|
||||||
const assert = require("node:assert");
|
const assert = require("node:assert");
|
||||||
@@ -32,37 +38,39 @@ const signatureOf = state => {
|
|||||||
const table = () => makeTable({ seatCount: 6, stack: 200 });
|
const table = () => makeTable({ seatCount: 6, stack: 200 });
|
||||||
|
|
||||||
describe("the signature the ring layout is cached against", () => {
|
describe("the signature the ring layout is cached against", () => {
|
||||||
test("moves when the first chips go in", () => {
|
test("moves when a hole card is entered", () => {
|
||||||
// Before the blinds every pill is empty and the pucks are short; after them two
|
// The one row still allowed to collapse, and the one thing still worth
|
||||||
// seats are ~15px taller. A ring still sized from the first measurement is sized
|
// re-measuring for: a puck with cards on it is about 15px taller.
|
||||||
// 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", () => {
|
|
||||||
const state = table();
|
const state = table();
|
||||||
const none = signatureOf(state);
|
const none = signatureOf(state);
|
||||||
state.seats[0].cards = ["AS", "KC"];
|
state.seats[0].cards = ["AS", "KC"];
|
||||||
assert.notEqual(signatureOf(state), none);
|
assert.notEqual(signatureOf(state), none);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("but not again for the second player to put chips in", () => {
|
test("but not again for the second player to be dealt one", () => {
|
||||||
// The whole point of a boolean over the table. The ring is sized from the
|
// The point of a boolean over the table. The ring is sized from the tallest puck
|
||||||
// tallest puck on it, so once anybody has an amount the answer cannot change —
|
// on it, so once anybody has cards the answer cannot change.
|
||||||
// and re-measuring here would mean ten forced layouts on every single action.
|
|
||||||
const state = table();
|
const state = table();
|
||||||
state.seats[1].committed = 10;
|
state.seats[0].cards = ["AS", "KC"];
|
||||||
const first = signatureOf(state);
|
const first = signatureOf(state);
|
||||||
state.seats[2].committed = 10;
|
state.seats[1].cards = ["2H", "7D"];
|
||||||
assert.equal(signatureOf(state), first);
|
|
||||||
state.seats[3].committed = 250;
|
|
||||||
assert.equal(signatureOf(state), first);
|
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", () => {
|
test("nor when an amount merely changes size", () => {
|
||||||
const state = table();
|
const state = table();
|
||||||
state.seats[1].committed = 10;
|
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");
|
"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", () => {
|
test("and it stands still for a rename", () => {
|
||||||
// Renaming can wrap a name onto a second line, and the button's chip floats
|
// Renaming used to be able to wrap a name onto a second line. A name gets one
|
||||||
// into the name's first line, so both change how tall a puck measures.
|
// 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 state = table();
|
||||||
const base = signatureOf(state);
|
const base = signatureOf(state);
|
||||||
state.seats[0].name = "Bartholomew Fairweather";
|
state.seats[0].name = "Bartholomew Fairweather";
|
||||||
const renamed = signatureOf(state);
|
assert.equal(signatureOf(state), base);
|
||||||
assert.notEqual(renamed, base);
|
|
||||||
state.dealerSeatIndex = (state.dealerSeatIndex + 1) % state.seats.length;
|
|
||||||
assert.notEqual(signatureOf(state), renamed);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("and two tables that differ only in where a name is split stay different", () => {
|
test("and for the button moving round the table", () => {
|
||||||
// The names are joined on a control character no name can contain. Joined on
|
// The chip in the puck's corner takes 22px off the name's line. That is a
|
||||||
// nothing, "ann" + "abel" and "anna" + "bel" are both "annabel" and one of those
|
// narrower name, not a taller puck, so the ring it sits on is the same ring.
|
||||||
// tables would be laid out with the other's measurements — different names, so
|
const state = table();
|
||||||
// different wrapping, so different puck heights.
|
const base = signatureOf(state);
|
||||||
//
|
state.dealerSeatIndex = (state.dealerSeatIndex + 1) % state.seats.length;
|
||||||
// The first version of this used "Ann"/"Abel" against "Anna"/"Bel", which can
|
assert.equal(signatureOf(state), base);
|
||||||
// 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));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("a table nobody has sat down at", () => {
|
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
|
// 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();
|
const state = HoldemState.createDefault();
|
||||||
HoldemState.resizeSeats(state, 9);
|
HoldemState.resizeSeats(state, 9);
|
||||||
assert.match(signatureOf(state), /\|$/,
|
assert.equal(signatureOf(state), "",
|
||||||
"an untouched table should carry neither flag");
|
"an untouched table should carry nothing");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("but a table with an ante on it does have an amount", () => {
|
test("and neither does one with an ante on it", () => {
|
||||||
// computeContribution answers for a seat that has never been given a stack —
|
// computeContribution answers for a seat that has never been given a stack — the
|
||||||
// the table's base pot stands in for it — so a table dealt with "everyone antes
|
// table's base pot stands in for it — so a table dealt with "everyone antes 5"
|
||||||
// 5" has amounts on its pucks before anybody acts, and the ring has to know.
|
// 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();
|
const state = HoldemState.createDefault();
|
||||||
HoldemState.resizeSeats(state, 6);
|
HoldemState.resizeSeats(state, 6);
|
||||||
state.basePotAmount = 5;
|
state.basePotAmount = 5;
|
||||||
state.seats.forEach(p => { p.inHand = true; });
|
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", () => {
|
describe("the button seat's name", () => {
|
||||||
// The dealer button's chip sits in the puck's top-right corner. Keeping the name out
|
// The dealer button's chip sits in the puck's top-right corner, and the name has to
|
||||||
// from under it with `padding-right` takes the chip's width off *every* line of the
|
// keep out from under it. `padding-right` takes the chip's width off the name's line,
|
||||||
// name, and the chip is on one of them: measured at 1280x900, the name box is 78px,
|
// which is the whole of the answer now that there is only ever one line.
|
||||||
// "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
|
// A float used to do this on the roomier rings — three rules of it, indenting the
|
||||||
// whose name reads properly for four hands and comes apart on the fifth.
|
// first line so a name too long to fit beside the chip could drop below it onto a
|
||||||
const roomy = CSS.match(/#seatsLayer:not\(\.seatsTight\) \.seat\.seatHasButton \.seatPuckName \{([^}]*)\}/);
|
// second. That only worked because there was a second line. There isn't: see the row
|
||||||
const float = CSS.match(/#seatsLayer:not\(\.seatsTight\) \.seat\.seatHasButton \.seatPuckName::before \{([^}]*)\}/);
|
// 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 \{([^}]*)\}/);
|
const base = CSS.match(/\n\.seat\.seatHasButton \.seatPuckName \{([^}]*)\}/);
|
||||||
|
|
||||||
test("a ring with room for two lines indents only the line beside the chip", () => {
|
test("the chip's width comes off the name, at every size", () => {
|
||||||
assert.ok(roomy && float, "the roomy regime should use a float");
|
assert.ok(base, "the base rule should reserve the chip's width");
|
||||||
assert.match(float[1], /float:\s*right/);
|
assert.match(base[1], /padding-right:\s*25px/);
|
||||||
assert.match(roomy[1], /padding-right:\s*0/);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("the float is exactly one line tall", () => {
|
test("and it is as wide as the chip it stands in for", () => {
|
||||||
// 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", () => {
|
|
||||||
const chip = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])width:\s*(\d+)px/);
|
const chip = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])width:\s*(\d+)px/);
|
||||||
const inset = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])right:\s*(\d+)px/);
|
const inset = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])right:\s*(\d+)px/);
|
||||||
const floatW = float[1].match(/width:\s*(\d+)px/);
|
const pad = base[1].match(/padding-right:\s*(\d+)px/);
|
||||||
assert.ok(chip && inset && floatW);
|
assert.ok(chip && inset && pad);
|
||||||
assert.equal(Number(floatW[1]), Number(chip[1]) + Number(inset[1]),
|
assert.equal(Number(pad[1]), Number(chip[1]) + Number(inset[1]),
|
||||||
"the float should cover the chip and the gap it is inset by");
|
"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
|
// ladder — which words go, in what order, and when it stops — and the browser's part
|
||||||
// is one boolean per pass.
|
// is one boolean per pass.
|
||||||
const label = (text, fits) => {
|
const label = (text, fits) => {
|
||||||
|
const classes = new Set();
|
||||||
const el = {
|
const el = {
|
||||||
textContent: text,
|
textContent: text,
|
||||||
title: "",
|
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
|
// The browser's answer, standing in for a real one: this name fits if
|
||||||
// `fits` says the version currently written to it does.
|
// `fits` says the version currently written to it does.
|
||||||
get scrollWidth() { return fits(this.textContent) ? 10 : 100; },
|
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");
|
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", () => {
|
test("a single word steps its type down rather than being cut", () => {
|
||||||
// There is no honest way to shorten "Christopher" that a dealer glancing across
|
// There is no honest way to shorten "Christopher" at a word, and "Christo…" is a
|
||||||
// a table would read faster than "Christo…".
|
// 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);
|
const el = label("Christopher", () => false);
|
||||||
seatsNamed("Christopher");
|
seatsNamed("Christopher");
|
||||||
Render.fitSeatNames(layer([el]));
|
Render.fitSeatNames(layer([el]));
|
||||||
assert.equal(el.textContent, "Christopher", "never cut mid-word here");
|
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", () => {
|
test("a name that fits is not touched", () => {
|
||||||
@@ -323,6 +327,32 @@ describe("a name that will not fit", () => {
|
|||||||
seatsNamed("Mike from work");
|
seatsNamed("Mike from work");
|
||||||
Render.fitSeatNames(layer([el]));
|
Render.fitSeatNames(layer([el]));
|
||||||
assert.equal(el.textContent, "Mike from work");
|
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", () => {
|
test("the whole name is on the tooltip whether it was shortened or not", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user