The chips were the last thing on the table nobody had measured, and the sweep had never seen one: it audited a table that had never had a bet placed on it, so a third of what is on the felt during a hand went unlooked at for the tool's whole life. It posts blinds, deals and calls three times now before it measures anything. What that found, and what this fixes: - Chips accumulated. Seats are kept and updated in place rather than rebuilt, so clearing the bets layer stopped clearing the ones anchored inside a seat and nothing else ever did. An iPhone 14 had one chip per seat after the deal and five by the third render; a dealer's every tap left another disc on the felt. - The ring-or-cling decision asked how wide the window was. A phone held sideways is 844px across and gives the felt 117px: it passed the width test, took the ring, and every chip on it was painted under a puck — the seats carry z-index 1 and the bets layer carries none, so a chip that lands under one is gone, and with it the only number saying what that player has in. Measured now, and only measured. - Getting clear of a box was `edgeReach(puck) + edgeReach(chip)`, which is right only straight up, down or sideways. Two axis-aligned boxes are clear when they are apart on one axis, so the travel is the reach of the box you get by adding their widths and their heights. At 768x1024 a $10 leaving a seat on the diagonal wanted 88px and got 77, and came to rest 11px inside its own player's puck — every seat off the four compass points, which is most of them. - Every chip was laid out at the first chip's size. A chip is as wide as the number on it: "$5" is 21px and "$10" is 27px on a phone, so each $10 was placed as if it were 6px narrower than it is. The widest is no better: at 768x1024 one 47px chip made the whole table give up a ring the other four had room for. - Anchored chips settled where they landed. At 640x360 the felt is 528x83 and a puck is 55 of those pixels tall: clearing it takes 41px, the players facing each other are 70px apart, and both chips set off 41px into a gap that is 70 wide. Three overlapping pairs on a twelve-handed table. Each chip now gives ground only to something it is actually standing on, a pixel at a time — a midline rule or a single scale factor both punish the seats at the ends of a rail, which were never going to hit anything. - Nothing knew the pot total was there. Seat 1's $10 was sitting on "Pot $65". Two things off the felt, from the same sweep: - The toast outlived its news. "Next hand — button on seat 2. [Undo]" stayed up while the hand was dealt and three players called: an offer to move the button, wired to a snapshot that predates the hand, so taking it would have thrown the hand away without saying so. It was also in the way of thirty small screens' seats and controls. The bar drops it as it takes the next action; the bar's own Undo names what it would actually take back. - The pot dialog cut a name off. Three players in the pot on a Galaxy S8, two rows visible, the third below a hairline that reads as the end of the list — a dealer ticking the two they could see would have paid two thirds of a three-way pot. The 85vh budget was still subtracting the action bar's height from a dialog that has sat above the bar since its z-index was fixed, and a player's ceiling moves beside their name on a short screen. All three fit now, and where a list does run on, it runs on under a fade rather than stopping dead on a row boundary. The pot badge goes on a phone held sideways: 86x66 of a 692x113 felt, for a number the action bar prints three inches below it, standing in the only band a bet chip has. The audit stopped calling one thing another. An element clipped away by a panel that scrolls is not covered by whatever is painted at that height — the wizard's third winner came back for four viewports as "fully covered by button Confirm Winner(s)". 190 audits: 25 failing configurations to 7, and every one of those seven is the seat ring hanging past the rail on a landscape phone, which is the known residual from the last round. 663 unit tests, 44 of them new. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
107 lines
5.2 KiB
JavaScript
107 lines
5.2 KiB
JavaScript
// The toast that outlived the thing it was talking about.
|
|
//
|
|
// Toasts are news about the last action, and they carry an Undo. Take another action
|
|
// and both of those go wrong at once: the sentence describes a step that is now two
|
|
// steps back, and the Undo behind it restores a snapshot from before everything since.
|
|
// "Next hand — button on seat 2. [Undo]" left up while a hand is dealt and three
|
|
// players call is an offer to move the button, wired to a button that deletes the hand.
|
|
//
|
|
// It is also simply in the way. The toast is a fixed strip near the bottom of the
|
|
// screen; on a 360x640 phone there is nowhere for it to sit that is clear of both the
|
|
// felt and the action bar, so a stale one covers seats and controls for eight seconds.
|
|
//
|
|
// So the bar drops it as it takes the next action. These tests hold that funnel shut:
|
|
// every action a dealer can take from the bar goes through `commit`, and `commit`
|
|
// clears the toast before it does anything else.
|
|
const test = require("node:test");
|
|
const { describe } = require("node:test");
|
|
const assert = require("node:assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const { ActionBar, Betting, makeTable } = require("./load");
|
|
|
|
const SRC = path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "js", "casino", "holdem");
|
|
const barSource = fs.readFileSync(path.join(SRC, "actionBar.js"), "utf8");
|
|
|
|
// A bar sat at a real table, with the toast replaced by something that keeps a log
|
|
// and the re-render stubbed out — none of these rules need the DOM back.
|
|
function onTable() {
|
|
const state = makeTable({ seatCount: 6, stack: 200, smallBlind: 1, bigBlind: 2, dealerSeatIndex: 0 });
|
|
const log = [];
|
|
globalThis.Toast = {
|
|
onScreen: null,
|
|
show(message) { this.onScreen = message; log.push(`show ${message}`); },
|
|
dismiss() { this.onScreen = null; log.push("dismiss"); },
|
|
};
|
|
ActionBar.state = state;
|
|
ActionBar.render = () => {};
|
|
ActionBar.hooks = {
|
|
beginAction: label => log.push(`begin ${label}`),
|
|
save: () => log.push("save"),
|
|
rerender: () => log.push("rerender"),
|
|
};
|
|
return { bar: ActionBar, state, log };
|
|
}
|
|
|
|
test.afterEach(() => { delete globalThis.Toast; });
|
|
|
|
describe("a toast does not outlive its own news", () => {
|
|
test("the next action from the bar clears it", () => {
|
|
const { bar, state, log } = onTable();
|
|
Toast.show("Next hand — button on seat 2.");
|
|
|
|
bar.commit("dealing the hand", () => Betting.beginHand(state));
|
|
|
|
assert.equal(Toast.onScreen, null, "the hand is dealt; the sentence about seating the table is not news any more");
|
|
assert.ok(log.indexOf("dismiss") < log.indexOf("begin dealing the hand"),
|
|
`the toast goes before the action is recorded, so nothing shown during the action is swept up with it: ${log.join(", ")}`);
|
|
});
|
|
|
|
test("a toast raised by the action itself survives", () => {
|
|
const { bar, log } = onTable();
|
|
bar.commit("something that talks back", () => Toast.show("Bob is all in."));
|
|
|
|
assert.equal(Toast.onScreen, "Bob is all in.",
|
|
"commit clears what was there when it started, not what its own work puts up");
|
|
assert.deepEqual(log, ["dismiss", "begin something that talks back", "show Bob is all in.", "save", "rerender"]);
|
|
});
|
|
|
|
test("with no toast on screen it is still just one dismiss", () => {
|
|
const { bar, log } = onTable();
|
|
bar.commit("a fold", () => {});
|
|
assert.equal(log.filter(l => l === "dismiss").length, 1);
|
|
});
|
|
|
|
test("a page with no Toast at all does not throw", () => {
|
|
const { bar, state } = onTable();
|
|
delete globalThis.Toast;
|
|
assert.doesNotThrow(() => bar.commit("dealing the hand", () => Betting.beginHand(state)));
|
|
});
|
|
});
|
|
|
|
describe("every bar action really does go through that funnel", () => {
|
|
// The dismiss is worth nothing if some button reaches past `commit` to mutate the
|
|
// table itself. Actions are written as `this.commit(...)`; a `Betting.` call made
|
|
// straight from a click handler would be one that skips the clean-up — and the
|
|
// undo history with it, which is the older reason this funnel exists.
|
|
test("no click handler calls the betting engine directly", () => {
|
|
const strays = barSource
|
|
.split("\n")
|
|
.map((line, i) => [i + 1, line])
|
|
.filter(([, line]) => /=>\s*Betting\.(fold|check|call|allIn|raiseTo|beginHand|closeStreet)\(/.test(line))
|
|
.filter(([, line]) => !/this\.commit\(/.test(line))
|
|
.map(([n, line]) => `actionBar.js:${n}: ${line.trim()}`);
|
|
|
|
assert.deepEqual(strays, [], "these change the table without going through commit");
|
|
});
|
|
|
|
test("commit is what clears the toast, not each button", () => {
|
|
const dismissals = barSource.split("\n").filter(l => /Toast\.dismiss\(\)/.test(l)).length;
|
|
// Two: this one, and the raise pad — which opens without committing anything,
|
|
// and needs the strip clear because the pad makes the bar tall enough to push
|
|
// a toast up over the seats.
|
|
assert.equal(dismissals, 2,
|
|
"a third dismissal means a button is doing by hand what commit already does");
|
|
});
|
|
});
|