Let a large felt have larger seats, and the type that goes with them
The felt grows with the window and the pucks did not: 96px was the only ceiling there was. Nine-handed, the seats covered 19% of the felt on a phone, 11% on a 1280px desktop and 8% at 1440px and beyond — and the stack figure a dealer reads sat at 10px on a 2560px monitor, the same 10px as on a 360px phone, well under every published floor for text meant to be read. Two things stop this costing more than it buys. Every candidate width is a forced layout. Simply raising the ceiling took the layout pass from 24ms to 73ms twelve-handed on a desktop, so a screen that cannot use the wider widths must not measure them: the extra pass is offered only when the arithmetic already says the seats have room to spare, and a phone measures exactly the ten widths, in the same order, that it measured before. Timed with the order alternated so neither carries the warm-up: 10.7ms against 10.4ms on a phone, and +3 to +6ms where the widths are actually used. And a wide puck on a narrow felt is worse than a small one. The first version of this let phones through, and a 144px puck on an iPhone's 278px ring is half the width of the table: six-handed, all six seats sat over the rail by 15px against 5px before, reading as a stack of cards rather than a table with people around it. So the ceiling is a fifth of the felt's shorter span, capped at 144. Five is measured rather than chosen — the shorter span is 233-303px on every phone in the sweep including sideways and 617-714px on tablets and desktops, so any divisor between 3.2 and 6.4 separates them — and the test pins both ends. Above the ordinary ceiling the puck takes the larger type as well, which is the point of the extra width. Nine-handed: an iPad goes 96px/12px to 134px/15px, a 1440px desktop 96px/12px to 140px/15px, and the seats go from 8% of the felt to 12%. Every phone, at every seat count, is untouched. The sweep is byte-identical to before this change: same six configurations, same 63 offenders, empty diff. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -1358,6 +1358,33 @@ button,
|
||||
font-size: var(--fs-2xs);
|
||||
}
|
||||
|
||||
/* The other end of the same idea. A puck past the ordinary 96px ceiling has room the
|
||||
type was never given: the felt grows with the window and the text did not, so a
|
||||
dealer on a 2560px monitor read their stacks at the same 10px as one on a 360px
|
||||
phone, on a table where the seats had dropped to 8% of the felt.
|
||||
|
||||
Two steps up the scale for the two numbers a dealer reads across the table, one for
|
||||
the chair number in the corner, which is a reference rather than a reading. Not
|
||||
proportional to the width — 96 to 144 is 1.5x and this is 1.25x — because a name is
|
||||
a label on a puck, not a headline, and the puck also has to hold it: 15px is the
|
||||
largest size at which "Mike from work", the longest name the sweep uses, still comes
|
||||
out on one line at 144px.
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.seatsRoomy .seatPuckStack,
|
||||
.seatsRoomy .seatPuckTotal {
|
||||
font-size: var(--fs-sm);
|
||||
}
|
||||
|
||||
.seatsRoomy .seatChairNumber {
|
||||
font-size: var(--fs-xs);
|
||||
}
|
||||
|
||||
/* The hole cards give up their line here. A puck this close to its neighbours is 64px
|
||||
tall and the seats at the ends of a landscape felt are 55px apart, so the line has to
|
||||
come from somewhere — and of the four things on a puck, the cards are the one the
|
||||
|
||||
@@ -405,6 +405,43 @@ const Render = {
|
||||
// The step the search walks down in. Six pixels is finer than the difference between
|
||||
// two font sizes and coarse enough to settle in ten tries.
|
||||
SEAT_WIDTH_STEP: 6,
|
||||
// And the ceiling when the felt has room to spare, which on anything tablet-sized or
|
||||
// larger it does. 96px was the only ceiling there was, so the felt grew with the
|
||||
// window and the seats did not: measured nine-handed, the pucks covered 19% of the
|
||||
// felt on a phone, 11% on a 1280px desktop and 8% at 1440px and beyond, with the
|
||||
// stack figure a dealer reads sitting at 10px on a 2560px monitor.
|
||||
//
|
||||
// 144 rather than further. Past it the extra width buys nothing a dealer can use —
|
||||
// at 144 a 15px "Mike from work" is already on one line, which is the longest thing
|
||||
// a name is asked to be — and a puck much wider than its own contents stops reading
|
||||
// as a seat and starts reading as a card.
|
||||
SEAT_WIDTH_ROOMY_MAX: 144,
|
||||
// Coarser above the ordinary ceiling, because up there the difference is 8% of the
|
||||
// puck rather than the difference between two font sizes, and every candidate width
|
||||
// costs a forced layout.
|
||||
SEAT_WIDTH_ROOMY_STEP: 12,
|
||||
// And the puck may not be more than a fifth of the shorter span of the ring.
|
||||
//
|
||||
// The containment arithmetic keeps a puck inside the table's *box*; the rail it
|
||||
// looks like it should stay inside is an ellipse, and at the top and bottom of the
|
||||
// ring the ellipse curves away from the box. Seats have always sat a little over
|
||||
// the rail because of it — measured before any of this, 10px on a desktop and 46px
|
||||
// on a phone held sideways — and the wider the puck, the wider that gap opens.
|
||||
//
|
||||
// Which made the first version of this a bad trade on exactly the screens it was
|
||||
// supposed to leave alone. A 144px puck on an iPhone's 278px ring is half the width
|
||||
// of the table: six-handed, all six sat over the rail by 15px against 5px before,
|
||||
// and it read as a stack of cards rather than a table with people around it.
|
||||
//
|
||||
// A fifth is measured, not chosen. The shorter span is 248-318px on every phone in
|
||||
// the sweep including sideways, and 672-978px on tablets and desktops, so any
|
||||
// divisor between about 3.3 and 7 splits them; five sits in the middle with room
|
||||
// either side. Above it the pass is skipped and a phone measures exactly the ten
|
||||
// widths, and settles on exactly the puck, that it did before.
|
||||
SEAT_ROOMY_SPAN_RATIO: 5,
|
||||
// A puck past the ordinary ceiling gets the larger type, which is the point of the
|
||||
// extra width — see `.seatsRoomy` in holdem.css.
|
||||
SEAT_ROOMY_FROM_PX: 108,
|
||||
// Below this a puck cannot hold two lines of name and a row of hole cards, so the
|
||||
// typography drops to the tight set in one go. Width scales continuously; type
|
||||
// changes deliberately, which is the split the modern guidance actually draws —
|
||||
@@ -529,11 +566,45 @@ const Render = {
|
||||
applySeatWidth(layer, width) {
|
||||
layer.style.setProperty("--seatPuckW", `${width}px`);
|
||||
layer.classList.toggle("seatsTight", width < this.SEAT_TIGHT_BELOW_PX);
|
||||
layer.classList.toggle("seatsRoomy", width >= this.SEAT_ROOMY_FROM_PX);
|
||||
},
|
||||
|
||||
// Is it worth asking about pucks wider than the ordinary ceiling?
|
||||
//
|
||||
// Arithmetic, not a measurement, and that is the whole point: closestSeatGapPx is
|
||||
// the tightest centre-to-centre distance the seats will sit at on the widest ring
|
||||
// they could have, so if that already clears the widest ordinary puck and its
|
||||
// clearance, the felt has room going spare and the coarse pass above the ceiling
|
||||
// will find some of it. If it does not — every phone in the sweep, at every seat
|
||||
// count — the pass is skipped and exactly the ten widths that were measured before
|
||||
// are measured now.
|
||||
//
|
||||
// Worth being strict about, because the candidates are not free. Raising the ceiling
|
||||
// for everybody took the layout pass from 24ms to 73ms on a twelve-handed desktop,
|
||||
// and it is the phones, which would gain nothing at all, that can least afford it.
|
||||
roomForWiderPucks(count, box) {
|
||||
if (!box) return false;
|
||||
if (this.roomyWidthCeiling(box) <= this.SEAT_WIDTH_MAX) return false;
|
||||
const gap = this.closestSeatGapPx(count, box, this.seatRingRadii());
|
||||
return gap > this.SEAT_WIDTH_MAX + this.SEAT_CLEARANCE_PX;
|
||||
},
|
||||
|
||||
// The widest a puck may be drawn on this felt, whatever the seats would allow.
|
||||
roomyWidthCeiling(box) {
|
||||
if (!box || !box.width || !box.height) return this.SEAT_WIDTH_MAX;
|
||||
const span = Math.min(box.width, box.height) / this.SEAT_ROOMY_SPAN_RATIO;
|
||||
return Math.min(this.SEAT_WIDTH_ROOMY_MAX, Math.floor(span));
|
||||
},
|
||||
|
||||
// Widest first, because the widest that fits is the one we want.
|
||||
seatWidthCandidates() {
|
||||
seatWidthCandidates(count, box) {
|
||||
const widths = [];
|
||||
if (count != null && this.roomForWiderPucks(count, box)) {
|
||||
const ceiling = this.roomyWidthCeiling(box);
|
||||
for (let w = ceiling; w > this.SEAT_WIDTH_MAX; w -= this.SEAT_WIDTH_ROOMY_STEP) {
|
||||
widths.push(w);
|
||||
}
|
||||
}
|
||||
for (let w = this.SEAT_WIDTH_MAX; w > this.SEAT_WIDTH_MIN; w -= this.SEAT_WIDTH_STEP) {
|
||||
widths.push(w);
|
||||
}
|
||||
@@ -616,7 +687,7 @@ const Render = {
|
||||
}
|
||||
|
||||
const candidates = [];
|
||||
for (const width of this.seatWidthCandidates()) {
|
||||
for (const width of this.seatWidthCandidates(count, box)) {
|
||||
this.applySeatWidth(layer, width);
|
||||
const puck = this.measuredSeatBox();
|
||||
if (!puck) {
|
||||
|
||||
@@ -442,6 +442,73 @@ is what makes it a test. Four faults were injected in total; the other three (dr
|
||||
two flags, letting the count into the key instead of a boolean, and counting only hand
|
||||
bets so a table playing antes is missed) fail 3, 2 and 1 of the eight.
|
||||
|
||||
## Room the felt had and the type never got
|
||||
|
||||
The felt grows with the window and the pucks did not — 96px was the only ceiling there
|
||||
was. Nine-handed, the seats covered 19% of the felt on a phone, 11% on a 1280px desktop
|
||||
and **8% at 1440px and beyond**, with the stack figure a dealer reads at **10px on a
|
||||
2560px monitor**, the same 10px as on a 360px phone.
|
||||
|
||||
Two things stop the fix costing more than it buys, and `seatWidthSearch.test.js` checks
|
||||
both:
|
||||
|
||||
- **Every candidate width is a forced layout.** Raising the ceiling for everybody took
|
||||
the layout pass from 24ms to 73ms twelve-handed on a desktop. A screen that cannot use
|
||||
the wider widths must not measure them, so a phone is checked to measure *exactly* the
|
||||
ten widths it measured before — same list, same order — and the measured cost on a
|
||||
phone is unchanged (10.7ms against 10.4ms, which is noise).
|
||||
- **A wide puck on a narrow felt is worse than a small one.** The first version let
|
||||
phones through, and a 144px puck on an iPhone's 278px ring is half the width of the
|
||||
table: six-handed, all six seats sat over the rail by 15px against 5px before, and it
|
||||
read as a stack of cards rather than a table with people around it.
|
||||
|
||||
So the ceiling is a fifth of the felt's **shorter** span, capped at 144. Five is measured,
|
||||
not chosen: the shorter span is 233-303px on every phone in the sweep including sideways,
|
||||
and 617-714px on tablets and desktops, so any divisor between 3.2 and 6.4 separates them.
|
||||
The test pins both ends, so if the felt ever changes shape enough to bring those together
|
||||
it says so rather than letting the phones quietly start paying.
|
||||
|
||||
Above the ordinary ceiling the puck also takes the larger type — `.seatsRoomy`, two steps
|
||||
up the scale for the two numbers read across the table and one for the chair number.
|
||||
Steps 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.
|
||||
|
||||
| nine-handed | before | after |
|
||||
| --- | --- | --- |
|
||||
| iPad puck / name | 96px / 12px | **134px / 15px** |
|
||||
| desktop 1440 puck / name | 96px / 12px | **140px / 15px** |
|
||||
| seats as a share of the felt, 1440 | 8% | **12%** |
|
||||
| every phone, every seat count | unchanged | unchanged |
|
||||
|
||||
### What the rail check is actually measuring, which is not the rail
|
||||
|
||||
Found while checking the above and **not fixed** — written down here because the number is
|
||||
worth having and the fix is not a small one.
|
||||
|
||||
`#pokerTable` is `border-radius: 50%`. Both the settle pass in `render.js` and
|
||||
`seatsOnTheTable()` in the audit compare a puck against the four edges of the table's
|
||||
**bounding box**, which is a different and much slacker question than the one the message
|
||||
"hangs Npx past the rail" claims to answer: at the top and bottom of the ring the oval
|
||||
curves away from its own box, so a puck can be comfortably inside the box and visibly
|
||||
outside the felt.
|
||||
|
||||
Measured against the ellipse instead — a point is outside when `(dx/a)² + (dy/b)² > 1`,
|
||||
and the ray from the centre crosses the boundary at `1/√t` of the way out — across 35
|
||||
configurations the check is seeing **303px of overhang as zero**, including **46px on a
|
||||
phone held sideways** that has been there for as long as the check has.
|
||||
|
||||
Settling against the ellipse was tried and reverted, because it is not the local fix it
|
||||
looks like. Pulling the ring in far enough to clear the oval crowds the seats, and on a
|
||||
twelve-handed phone with everybody all-in it put seat 3's centre inside seat 4 — the one
|
||||
trade the width search explicitly refuses to make, since a dealer can see a puck over the
|
||||
rail but cannot see a tap answering for the wrong player. Guarding against that instead
|
||||
leaves the ring out and the overhang worse: the sweep went from 6 failing configurations
|
||||
to 77, with 355 rail reports.
|
||||
|
||||
Which is the finding. On a crowded table **keeping the seats off the rail and keeping them
|
||||
off each other are in genuine conflict**, and resolving it needs the two settled together
|
||||
rather than one bolted onto the other.
|
||||
|
||||
## Why the hand evaluator is tested the way it is
|
||||
|
||||
`handRank.js` is the only module here that can pay the wrong person without anybody
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
// How wide a seat puck is allowed to get, and on which screens.
|
||||
//
|
||||
// The felt grows with the window and the pucks did not: 96px was the only ceiling there
|
||||
// was, so nine-handed the seats covered 19% of the felt on a phone, 11% on a 1280px
|
||||
// desktop and 8% at 1440px and beyond — with the stack figure a dealer reads sitting at
|
||||
// 10px on a 2560px monitor, the same 10px as on a 360px phone.
|
||||
//
|
||||
// Two things stop the fix from costing more than it buys, and both are checked here.
|
||||
//
|
||||
// Every candidate width is a forced layout, and raising the ceiling for everybody took
|
||||
// the layout pass from 24ms to 73ms on a twelve-handed desktop — so a screen that cannot
|
||||
// use the extra widths must not measure them. And a 144px puck on an iPhone's 278px ring
|
||||
// is half the width of the table: the first version of this let phones through, and
|
||||
// six-handed all six seats sat over the rail by 15px against 5px before, reading as a
|
||||
// stack of cards rather than a table with people around it.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Render } = require("./load");
|
||||
|
||||
// Ring boxes read off the running page, not invented. #seatsLayer, nine-handed, at the
|
||||
// viewports the sweep visits.
|
||||
const RINGS = {
|
||||
"Galaxy S8": { width: 233, height: 282 },
|
||||
"iPhone 14": { width: 263, height: 470 },
|
||||
"Pro Max": { width: 303, height: 535 },
|
||||
"phone sideways": { width: 565, height: 257 },
|
||||
iPad: { width: 689, height: 689 },
|
||||
desktop: { width: 857, height: 617 },
|
||||
"desktop 1440": { width: 992, height: 714 },
|
||||
};
|
||||
const SMALL = ["Galaxy S8", "iPhone 14", "Pro Max", "phone sideways"];
|
||||
const LARGE = ["iPad", "desktop", "desktop 1440"];
|
||||
|
||||
describe("which felts may have wider pucks at all", () => {
|
||||
test("no phone does, held either way up", () => {
|
||||
// The guarantee the phones are owed: they measure the widths they always
|
||||
// measured and settle on the puck they always settled on.
|
||||
SMALL.forEach(name => {
|
||||
assert.ok(Render.roomyWidthCeiling(RINGS[name]) <= Render.SEAT_WIDTH_MAX,
|
||||
`${name} would be allowed a ${Render.roomyWidthCeiling(RINGS[name])}px puck`);
|
||||
[2, 4, 6, 9, 12].forEach(count => assert.equal(
|
||||
Render.roomForWiderPucks(count, RINGS[name]), false,
|
||||
`${name} ${count}-handed`));
|
||||
});
|
||||
});
|
||||
|
||||
test("and every tablet and desktop does", () => {
|
||||
LARGE.forEach(name => {
|
||||
assert.ok(Render.roomyWidthCeiling(RINGS[name]) > Render.SEAT_WIDTH_MAX, name);
|
||||
});
|
||||
});
|
||||
|
||||
test("with a comfortable margin either side of the line", () => {
|
||||
// The rule is a fifth of the shorter span, and the point of five is that it is
|
||||
// not near either group. If a change to the felt ever moves these together,
|
||||
// this says so before the phones quietly start paying for it.
|
||||
const biggestSmall = Math.max(...SMALL.map(n => Math.min(RINGS[n].width, RINGS[n].height)));
|
||||
const smallestLarge = Math.min(...LARGE.map(n => Math.min(RINGS[n].width, RINGS[n].height)));
|
||||
assert.ok(smallestLarge > biggestSmall * 1.8,
|
||||
`${biggestSmall}px and ${smallestLarge}px are too close for one divisor to separate`);
|
||||
// Every divisor that keeps the phones out is at least the widest phone span over
|
||||
// the ordinary ceiling; every one that lets the tablets in is below the narrowest
|
||||
// large span over the same. 303/96 = 3.2 and 617/96 = 6.4, and five is between.
|
||||
const floor = biggestSmall / Render.SEAT_WIDTH_MAX;
|
||||
const cap = smallestLarge / Render.SEAT_WIDTH_MAX;
|
||||
assert.ok(Render.SEAT_ROOMY_SPAN_RATIO >= floor,
|
||||
`${Render.SEAT_ROOMY_SPAN_RATIO} lets a phone through; it has to be at least ${floor.toFixed(2)}`);
|
||||
assert.ok(Render.SEAT_ROOMY_SPAN_RATIO < cap,
|
||||
`${Render.SEAT_ROOMY_SPAN_RATIO} shuts the tablets out; it has to be under ${cap.toFixed(2)}`);
|
||||
});
|
||||
|
||||
test("and a felt with no size at all is not one of them", () => {
|
||||
assert.equal(Render.roomForWiderPucks(9, null), false);
|
||||
assert.equal(Render.roomForWiderPucks(9, { width: 0, height: 0 }), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the widths a felt actually measures", () => {
|
||||
const widthsFor = (name, count) => Render.seatWidthCandidates(count, RINGS[name]);
|
||||
|
||||
test("a phone measures exactly the ten it measured before", () => {
|
||||
const expected = [];
|
||||
for (let w = Render.SEAT_WIDTH_MAX; w > Render.SEAT_WIDTH_MIN; w -= Render.SEAT_WIDTH_STEP) {
|
||||
expected.push(w);
|
||||
}
|
||||
expected.push(Render.SEAT_WIDTH_MIN);
|
||||
SMALL.forEach(name => [6, 9, 12].forEach(count =>
|
||||
assert.deepEqual(widthsFor(name, count), expected, `${name} ${count}-handed`)));
|
||||
});
|
||||
|
||||
test("a desktop measures a handful more, and no more than a handful", () => {
|
||||
const extra = widthsFor("desktop 1440", 9).length - widthsFor("Galaxy S8", 9).length;
|
||||
assert.ok(extra > 0, "the wider widths never got offered");
|
||||
assert.ok(extra <= 6, `${extra} extra forced layouts is more than this is worth`);
|
||||
});
|
||||
|
||||
test("never wider than the felt's own ceiling", () => {
|
||||
Object.keys(RINGS).forEach(name => [2, 6, 9, 12].forEach(count => {
|
||||
const widest = Math.max(...widthsFor(name, count));
|
||||
assert.ok(widest <= Math.max(Render.SEAT_WIDTH_MAX, Render.roomyWidthCeiling(RINGS[name])),
|
||||
`${name} ${count}-handed offered ${widest}px`);
|
||||
assert.ok(widest <= Render.SEAT_WIDTH_ROOMY_MAX);
|
||||
}));
|
||||
});
|
||||
|
||||
test("always widest first, always down to the floor", () => {
|
||||
// The search takes the first width that fits, so the order is the whole
|
||||
// algorithm — and the floor has to be on the list or a crowded table has
|
||||
// nothing left to fall back to.
|
||||
Object.keys(RINGS).forEach(name => [2, 9, 12].forEach(count => {
|
||||
const widths = widthsFor(name, count);
|
||||
widths.forEach((w, i) => {
|
||||
if (i === 0) return;
|
||||
assert.ok(w < widths[i - 1], `${name}: ${widths.join(", ")} is not descending`);
|
||||
});
|
||||
assert.equal(widths[widths.length - 1], Render.SEAT_WIDTH_MIN);
|
||||
}));
|
||||
});
|
||||
|
||||
test("and a crowded table on a big felt is told no like any other", () => {
|
||||
// Room on the felt is necessary, not sufficient: the seats still have to clear
|
||||
// each other. Enough of them on any ring and the wider widths stop being
|
||||
// offered, which is the arithmetic doing the refusing rather than the
|
||||
// measurement having to.
|
||||
const crowded = [24, 30, 40].map(count => Render.roomForWiderPucks(count, RINGS.desktop));
|
||||
assert.deepEqual(crowded, [false, false, false]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user