Stop two empty rows in every seat puck setting the size of the table
A seat puck carries the name, the hole cards, the amount that player has put in, and their stack. Two of those are empty most of the time and both held their space anyway. The amount pill was worse than empty space. It has a background, a pill radius and side padding, so with nothing in it, it painted an 8x11px green lozenge — measured on seven of nine seats at an ordinary table. One of those is a component holding its place; nine is a rash, and a dealer has to learn to ignore a colour that on the other two seats means "this player has money out". The space was not free either. seatRingRadii sizes the ring from the tallest puck on it, so two rows nobody had filled were setting how far apart the whole table had to sit. Across 21 tables — seven viewports, six, nine and twelve handed — every single puck comes back shorter and six come back wider: an iPhone 14 nine-handed 78x88 -> 78x63, twelve-handed 44x65 -> 66x55, which is the one name in the set that was clipped and now is not. The collapse is only safe because the layout cache moved with it. resolveSeatLayout measures ten candidate widths, each a forced layout, against seatShapeSignature() — which listed the names and the dealer seat, everything that could wrap a name onto a second line. It did not mention these two rows, because while they always held their space they could never change a puck's height. Now they can, so the signature names them: booleans over the whole table, because measuredSeatBox takes the tallest puck on the ring, and because a per-seat key would be correct and would throw the cache away on every tap. The ring settles by 24-26px when the blinds go in — and it settled by 23-25px there before this round too, since the action bar changes height at that moment and the felt is measured against it. The one place it moves is a place it already moved. uiAudit gains emptyDecoration(): nothing on the felt may paint a background or a border with no content in it. Its one exemption is on the list because the check reported it rather than because it was predicted — .betChipDisc is a circle filled with the denomination colour of the chip beside it, so the colour is the content. Three others were written from memory first and matched no element on the page at all. The sweep is unchanged at 6 failing configurations and 63 offenders. All four table failures are the all-in shape, where every pill is filled and the collapse buys nothing; the ordinary tables this helps were already passing, so what moved is room a name has rather than whether anything was covered. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -1075,6 +1075,33 @@ button,
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Neither row has anything in it most of the time, and both were holding their space
|
||||
anyway — the cards line on every seat of every table that isn't tracking hole cards,
|
||||
and the amount pill on everyone who hasn't put chips in yet.
|
||||
|
||||
The pill was worse than empty space. It has a background, a pill radius and side
|
||||
padding, so with no text it painted an 8x11px green lozenge in the middle of the puck
|
||||
— measured on seven of nine seats at an ordinary table, a coloured mark on almost
|
||||
every seat that stood for nothing. Nine of those is not a table, it is a rash.
|
||||
|
||||
The space they held was not free either. The ring is sized against the tallest puck
|
||||
on it, so two rows nobody had filled were setting how far apart twelve seats had to
|
||||
sit: on an iPhone 14 nine-handed the puck measured 78x88 with 25px of it reserved for
|
||||
nothing. Collapsed, the search settles on 78x63 — same width for the name, a quarter
|
||||
less height to fit on the ring.
|
||||
|
||||
`:empty` is exact here: both rows are written by updatePuckContent, which sets an
|
||||
empty string rather than a space when there is nothing to say. What the collapse
|
||||
needs from the layout is that the ring re-measures when one of them fills — see
|
||||
seatShapeSignature() in render.js, which now names both. */
|
||||
.seatPuckCards:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.seatPuckTotal:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.dealerChip {
|
||||
position: absolute;
|
||||
/* Inside its own seat, not on the rim.
|
||||
|
||||
@@ -544,9 +544,26 @@ const Render = {
|
||||
// What the table has changed about the shape of its pucks. Anything that can make a
|
||||
// name wrap to a second line belongs here, because the height that comes back from
|
||||
// measuring is what the ring is sized against.
|
||||
//
|
||||
// The hole-card line and the amount pill belong here for the same reason, now that
|
||||
// an empty one collapses instead of holding its space: a puck with either of them
|
||||
// filled is about 15px taller than the same puck without, and a ring still sized
|
||||
// from the measurement taken before the first chips went in is sized for a puck
|
||||
// that no longer exists.
|
||||
//
|
||||
// Booleans over the whole table rather than a list per seat, because that is what
|
||||
// the measurement is — measuredSeatBox takes the tallest puck on the ring, so what
|
||||
// matters is whether *anybody* has one. A boolean also flips about twice a hand
|
||||
// rather than on every action, and each flip costs ten forced layouts, which is the
|
||||
// expense this cache exists to avoid.
|
||||
seatShapeSignature() {
|
||||
if (!this.state) return "";
|
||||
return `${this.state.seats.map(p => p.name || "").join("")}|${this.state.dealerSeatIndex}`;
|
||||
const seats = this.state.seats;
|
||||
const anyCards = seats.some(p => (p.cards || []).some(Boolean));
|
||||
const anyAmount = seats.some(p => Betting.handTotal(p) > 0 ||
|
||||
HoldemState.computeContribution(p, this.state.chipLegend, this.state.basePotAmount) > 0);
|
||||
return `${seats.map(p => p.name || "").join("")}|${this.state.dealerSeatIndex}` +
|
||||
`|${anyCards ? "c" : ""}${anyAmount ? "a" : ""}`;
|
||||
},
|
||||
|
||||
// Which width the pucks get, and how wide a ring they sit on.
|
||||
|
||||
@@ -380,6 +380,68 @@ Three injected faults — the cap ignored, the summary carrying only the first e
|
||||
money, the elision appended rather than kept in place — fail 6, 1 and 5 of the 11 tests
|
||||
respectively, and the audit check reports the original 64% when the cap is taken out.
|
||||
|
||||
## Two empty rows were setting how far apart twelve seats had to sit
|
||||
|
||||
Every seat puck carries four rows: the name, the hole cards, the amount that player has
|
||||
put in, and their stack. Two of those are empty most of the time, and both held their
|
||||
space anyway.
|
||||
|
||||
The amount pill was worse than empty space. It has a background, a pill radius and side
|
||||
padding, so with no text it painted an **8x11px green lozenge** — measured on **seven of
|
||||
nine seats** at an ordinary table. One of those is a component holding its place; nine is
|
||||
a rash, and a dealer has to learn to ignore a colour that on the other two seats means
|
||||
"this player has money out".
|
||||
|
||||
The space was not free either. `seatRingRadii` sizes the ring from the tallest puck on
|
||||
it, so two rows nobody had filled were setting the spacing for the whole table:
|
||||
|
||||
| | before | after |
|
||||
| --- | --- | --- |
|
||||
| iPhone 14, 9 seats | 78x88 puck | **78x63** |
|
||||
| iPhone 14, 12 seats | 44x65 | **66x55** |
|
||||
| Pro Max, 9 seats | 84x88 | **96x63** |
|
||||
| Galaxy S8, 12 seats | 8 overlapping pairs, 1556px² | 4 pairs, **87px²** |
|
||||
|
||||
- **`emptyDecoration()`** — nothing inside `#pokerTableWrap` may paint a background or a
|
||||
border with no content in it. The rule is about paint, not space: an element may hold
|
||||
its place invisibly, and several do. With the old reservation restored it reports all
|
||||
seven pills; live it reports none.
|
||||
|
||||
The one exemption, `.betChipDisc`, is on the list because the check reported it, not
|
||||
because it was predicted — it is a 13x13px circle filled with the denomination colour of
|
||||
the chip beside it, so the colour *is* the content. Three other exemptions were written
|
||||
from memory first and matched no element on the page at all, which is the argument for
|
||||
building that list from a run.
|
||||
|
||||
### The collapse is only safe because the cache key moved
|
||||
|
||||
`resolveSeatLayout` measures ten candidate widths, each one a forced layout, and caches
|
||||
the result against `seatShapeSignature()`. That signature listed the names and the dealer
|
||||
seat — everything that could wrap a name onto a second line. It did not mention the two
|
||||
rows, because they always held their space and so could never change a puck's height.
|
||||
|
||||
Once they collapse, they can. `seatPuckShape.test.js` checks the key moves when the first
|
||||
chips go in and when a hole card is entered, and — just as important — that it does *not*
|
||||
move for the second player to bet, or when an amount merely gets wider. It is a boolean
|
||||
over the whole table because that is what the measurement is: `measuredSeatBox` takes the
|
||||
tallest puck on the ring, so what matters is whether anybody has one. A per-seat key would
|
||||
be correct and would throw away the cache on every single tap.
|
||||
|
||||
Measured on the running page: the ring settles by 24-26px when the blinds are posted —
|
||||
and it settled by **23-25px there before this round too**, because the action bar changes
|
||||
height at that moment and the felt is measured against it. The one place the ring moves
|
||||
is a place it already moved.
|
||||
|
||||
### A test that passed for the wrong reason
|
||||
|
||||
The signature joins names on a control character so that "ann" + "abel" and "anna" + "bel"
|
||||
stay distinguishable. The first version of that test used `Ann`/`Abel` against
|
||||
`Anna`/`Bel` — which can never collide whatever the separator, because the capitals land
|
||||
in different places. It passed with the separator deleted and proved nothing. Lower case
|
||||
is what makes it a test. Four faults were injected in total; the other three (dropping the
|
||||
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.
|
||||
|
||||
## 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,125 @@
|
||||
// What the ring is allowed to be sized from.
|
||||
//
|
||||
// Two rows in every seat puck are usually empty — the hole-card line, and the pill that
|
||||
// shows what a player has put in. Both used to hold their space anyway, and the pill,
|
||||
// which has a background and a pill radius and side padding, painted an 8x11px green
|
||||
// lozenge when it had nothing to say: measured on seven of nine seats at an ordinary
|
||||
// table, a coloured mark on almost every seat standing for nothing.
|
||||
//
|
||||
// The space was not free either. seatRingRadii sizes the ring from the tallest puck on
|
||||
// it, so two rows nobody had filled were setting how far apart the seats had to sit. On
|
||||
// an iPhone 14 nine-handed the puck measured 78x88 with 25px of that reserved for
|
||||
// nothing; collapsed, the width search settles on 78x63. Twelve-handed on a Galaxy S8
|
||||
// the seats overlapped by 1556px² and now overlap by 87px².
|
||||
//
|
||||
// The collapse is only safe if the layout re-measures when one of those rows fills,
|
||||
// because resolveSeatLayout caches its ten forced layouts against this signature. That
|
||||
// is what is checked here: the cache key has to move when the puck's shape can, and
|
||||
// stay still when it cannot — a signature that changed on every action would throw the
|
||||
// cache away on every tap, which is the cost it exists to avoid.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Render, HoldemState, makeTable } = require("./load");
|
||||
|
||||
// seatShapeSignature reads Render.state, so a table has to be hung on it first.
|
||||
const signatureOf = state => {
|
||||
const before = Render.state;
|
||||
Render.state = state;
|
||||
try { return Render.seatShapeSignature(); } finally { Render.state = before; }
|
||||
};
|
||||
|
||||
const table = () => makeTable({ seatCount: 6, stack: 200 });
|
||||
|
||||
describe("the signature the ring layout is cached against", () => {
|
||||
test("moves when the first chips go in", () => {
|
||||
// Before the blinds every pill is empty and the pucks are short; after them two
|
||||
// seats are ~15px taller. A ring still sized from the first measurement is sized
|
||||
// for a puck that no longer exists.
|
||||
const state = table();
|
||||
const empty = signatureOf(state);
|
||||
state.seats[1].committed = 10;
|
||||
assert.notEqual(signatureOf(state), empty,
|
||||
"the ring would still be sized for pucks with no amount on them");
|
||||
});
|
||||
|
||||
test("and when a hole card is entered", () => {
|
||||
const state = table();
|
||||
const none = signatureOf(state);
|
||||
state.seats[0].cards = ["AS", "KC"];
|
||||
assert.notEqual(signatureOf(state), none);
|
||||
});
|
||||
|
||||
test("but not again for the second player to put chips in", () => {
|
||||
// The whole point of a boolean over the table. The ring is sized from the
|
||||
// tallest puck on it, so once anybody has an amount the answer cannot change —
|
||||
// and re-measuring here would mean ten forced layouts on every single action.
|
||||
const state = table();
|
||||
state.seats[1].committed = 10;
|
||||
const first = signatureOf(state);
|
||||
state.seats[2].committed = 10;
|
||||
assert.equal(signatureOf(state), first);
|
||||
state.seats[3].committed = 250;
|
||||
assert.equal(signatureOf(state), first);
|
||||
});
|
||||
|
||||
test("nor when an amount merely changes size", () => {
|
||||
const state = table();
|
||||
state.seats[1].committed = 10;
|
||||
const small = signatureOf(state);
|
||||
state.seats[1].committed = 999999;
|
||||
assert.equal(signatureOf(state), small,
|
||||
"a wider number is the amount fitter's problem, not the ring's");
|
||||
});
|
||||
|
||||
test("and it still moves for the things it always moved for", () => {
|
||||
// Renaming can wrap a name onto a second line, and the button's chip floats
|
||||
// into the name's first line, so both change how tall a puck measures.
|
||||
const state = table();
|
||||
const base = signatureOf(state);
|
||||
state.seats[0].name = "Bartholomew Fairweather";
|
||||
const renamed = signatureOf(state);
|
||||
assert.notEqual(renamed, base);
|
||||
state.dealerSeatIndex = (state.dealerSeatIndex + 1) % state.seats.length;
|
||||
assert.notEqual(signatureOf(state), renamed);
|
||||
});
|
||||
|
||||
test("and two tables that differ only in where a name is split stay different", () => {
|
||||
// The names are joined on a control character no name can contain. Joined on
|
||||
// nothing, "ann" + "abel" and "anna" + "bel" are both "annabel" and one of those
|
||||
// tables would be laid out with the other's measurements — different names, so
|
||||
// different wrapping, so different puck heights.
|
||||
//
|
||||
// The first version of this used "Ann"/"Abel" against "Anna"/"Bel", which can
|
||||
// never collide: the capital letters land in different places, so it passed with
|
||||
// the separator taken out and proved nothing at all. Lower case is what makes it
|
||||
// a real test.
|
||||
const a = table();
|
||||
const b = table();
|
||||
a.seats[0].name = "ann"; a.seats[1].name = "abel";
|
||||
b.seats[0].name = "anna"; b.seats[1].name = "bel";
|
||||
assert.notEqual(signatureOf(a), signatureOf(b));
|
||||
});
|
||||
});
|
||||
|
||||
describe("a table nobody has sat down at", () => {
|
||||
test("has no amounts and no cards on it", () => {
|
||||
// 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.
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 9);
|
||||
assert.match(signatureOf(state), /\|$/,
|
||||
"an untouched table should carry neither flag");
|
||||
});
|
||||
|
||||
test("but a table with an ante on it does have an amount", () => {
|
||||
// computeContribution answers for a seat that has never been given a stack —
|
||||
// the table's base pot stands in for it — so a table dealt with "everyone antes
|
||||
// 5" has amounts on its pucks before anybody acts, and the ring has to know.
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 6);
|
||||
state.basePotAmount = 5;
|
||||
state.seats.forEach(p => { p.inHand = true; });
|
||||
assert.match(signatureOf(state), /a$/);
|
||||
});
|
||||
});
|
||||
@@ -967,6 +967,57 @@ const LayoutAudit = {
|
||||
return { name: "hidden means hidden", offenders };
|
||||
},
|
||||
|
||||
// A mark on the felt that stands for nothing.
|
||||
//
|
||||
// The amount pill inside every seat puck has a background, a pill radius and side
|
||||
// padding, so a player who had not put chips in yet got an 8x11px green lozenge
|
||||
// where their bet would go — measured on seven of nine seats at an ordinary table.
|
||||
// One of those is a component holding its place; nine of them is a rash, and a
|
||||
// dealer reading the felt has to learn to ignore a colour that on three other seats
|
||||
// means "this player has money out".
|
||||
//
|
||||
// It cost room as well as attention. The ring is sized from the tallest puck on it,
|
||||
// so a row nobody had filled was setting how far apart the seats had to sit.
|
||||
//
|
||||
// The rule is about paint, not about space: an element may hold its place invisibly,
|
||||
// and several here do. What it may not do is draw a background or a border with
|
||||
// nothing inside it.
|
||||
//
|
||||
// One real exemption, and it is on the list because the check reported it rather
|
||||
// than because it was predicted: .betChipDisc is a 13x13px circle filled with the
|
||||
// denomination colour of the chip beside it, published per chip from render.js. It
|
||||
// has no text because the colour *is* the content. Anything carrying its own
|
||||
// picture in a background-image is exempt on the same grounds, without needing to be
|
||||
// named. Nothing else here needs an entry — three others were guessed at first and
|
||||
// matched no element on the page, which is its own warning about writing this list
|
||||
// from memory instead of from a run.
|
||||
emptyDecoration() {
|
||||
const DECORATIVE = new Set(["betChipDisc"]);
|
||||
const opaque = value => value && value !== "none" && !/rgba\([^)]*,\s*0\s*\)/.test(value)
|
||||
&& value !== "rgba(0, 0, 0, 0)" && value !== "transparent";
|
||||
|
||||
const offenders = [...document.querySelectorAll("#pokerTableWrap *")]
|
||||
.filter(el => {
|
||||
if (el.children.length || (el.textContent || "").trim()) return false;
|
||||
if ([...el.classList].some(c => DECORATIVE.has(c))) return false;
|
||||
const box = el.getBoundingClientRect();
|
||||
if (box.width < 2 || box.height < 2) return false;
|
||||
const cs = getComputedStyle(el);
|
||||
if (cs.display === "none" || cs.visibility === "hidden" || Number(cs.opacity) === 0) return false;
|
||||
if (opaque(cs.backgroundImage)) return false;
|
||||
return opaque(cs.backgroundColor) ||
|
||||
["Top", "Right", "Bottom", "Left"].some(side =>
|
||||
parseFloat(cs[`border${side}Width`]) > 0 && opaque(cs[`border${side}Color`]));
|
||||
})
|
||||
.map(el => {
|
||||
const box = el.getBoundingClientRect();
|
||||
return `${this.describe(el)} :: nothing in it, painted ` +
|
||||
`${Math.round(box.width)}x${Math.round(box.height)}px`;
|
||||
});
|
||||
|
||||
return { name: "nothing on the felt is painted for nothing", offenders };
|
||||
},
|
||||
|
||||
// A seat's name, painted under the decoration on its own puck.
|
||||
//
|
||||
// The chip that marks the button sits in the puck's top-right corner on a layer
|
||||
@@ -1293,6 +1344,7 @@ const LayoutAudit = {
|
||||
this.seatOwnership(),
|
||||
this.oneDealerButton(),
|
||||
this.hiddenButPainted(),
|
||||
this.emptyDecoration(),
|
||||
this.seatLabelsClear(),
|
||||
this.seatsKeepTheirText(),
|
||||
this.chipsKeepTheirAmounts(),
|
||||
|
||||
Reference in New Issue
Block a user