// Who the felt marks "ALL IN". // // The mark's whole value is that a dealer can scan the ring and see who can still be // bet at, so the test that matters most is the last one: the seats it marks are // exactly the seats the betting round refuses to ask for another chip. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const fs = require("fs"); const path = require("path"); const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load"); const CSS = fs.readFileSync( path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "css", "casino", "holdem.css"), "utf8"); // The declarations inside one rule, by its exact selector. const ruleBody = selector => { const at = CSS.indexOf(`\n${selector} {`); if (at < 0) return null; const open = CSS.indexOf("{", at); return CSS.slice(open + 1, CSS.indexOf("}", open)); }; test("a player who moved all-in is marked", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 }); state.seats[3].stack = 12; Betting.beginHand(state); Betting.allIn(state, state.actingSeatIndex); assert.equal(Betting.isAllIn(state, state.seats[3]), true); assert.equal(state.seats[3].stack, 0); }); test("a player with chips behind is not", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 }); Betting.beginHand(state); assert.equal(Betting.isAllIn(state, state.seats[0]), false); assert.equal(Betting.isAllIn(state, state.seats[1]), false); }); test("a busted player who was dealt in but has put nothing in is not", () => { // An empty stack alone isn't all-in — it's someone who shouldn't have been dealt // in. Calling that "ALL IN" on the felt would be a lie about a live pot. const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 }); state.seats[2].stack = 0; assert.equal(Betting.isAllIn(state, state.seats[2]), false); }); test("a folded player with an empty stack is not", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 }); state.seats[1].committed = 5; state.seats[1].stack = 0; state.seats[1].folded = true; assert.equal(Betting.isAllIn(state, state.seats[1]), false); }); test("an empty chair is not", () => { const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 }); assert.equal(Betting.isAllIn(state, state.seats[4]), false); }); test("a seat with no stack of its own falls back to the table's and isn't marked", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 }); state.defaultStack = 200; state.seats[2].stack = null; state.seats[2].committed = 10; assert.equal(Betting.isAllIn(state, state.seats[2]), false); }); test("the seats marked are exactly the ones the action passes over", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 }); state.seats[1].stack = 8; state.seats[3].stack = 30; Betting.beginHand(state); // Everyone in, two of them for less than the others can cover. for (let step = 0; step < 4; step++) Betting.allIn(state, state.actingSeatIndex); const marked = state.seats .map((player, index) => (Betting.isAllIn(state, player) ? index : null)) .filter(index => index !== null); const skipped = state.seats .map((player, index) => player.inHand && !player.folded && Betting.nextToAct(state, index - 1) !== index ? index : null) .filter(index => index !== null); assert.deepEqual(marked, skipped); assert.ok(marked.length > 0, "the scenario should actually produce all-in players"); }); test("nobody is marked once the next hand starts", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 }); state.seats[2].stack = 5; Betting.beginHand(state); Betting.allIn(state, state.actingSeatIndex); assert.ok(state.seats.some(player => Betting.isAllIn(state, player))); // The short stack got there by winning nothing; give them chips back the way a // dealer would before the next deal. HoldemState.startNextHand(state); state.seats[2].stack = 50; assert.equal(state.seats.filter(player => Betting.isAllIn(state, player)).length, 0); }); test("marking nobody doesn't move a chip", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 }); state.seats[3].stack = 12; Betting.beginHand(state); const before = chipsOnTable(state); state.seats.forEach(player => Betting.isAllIn(state, player)); assert.equal(chipsOnTable(state), before); }); // ---- And the shape of the mark, which is the felt's problem ------------------ // // "ALL IN" is the only thing that ever lands on the stack line with a space in it, and // a space is somewhere to break. Measured at 360x640 with twelve players all-in, it // wanted 36px and had 34: it broke onto a second line, and an 11px line became 22. // // That is not a typography complaint. The puck went from 55px tall to 66, and puck // height is what the seat ring is short of — at the ends of an ellipse the seats are // separated vertically, so 11px taller is 11px further into the seat next door. The // layout search then tried every width it has, found none that cleared, and fell back // to the narrowest, which is also the one that wraps hardest. // // So the rule every design system gives a badge applies here too: one line, always. describe("the mark is one line, because the ring cannot afford two", () => { test("a stack line can never wrap", () => { const body = ruleBody(".seatPuckStack"); assert.ok(body, "the .seatPuckStack rule should exist"); assert.match(body, /white-space:\s*nowrap/, "the stack line must not be allowed to wrap — a second line is 11px the ring hasn't got"); }); test("and on the tightest pucks it gives up its letter-spacing to fit", () => { // Not wrapping alone only converts a wrap into a spill: a line that cannot wrap // and still doesn't fit paints over the seat beside it instead. The spacing is // 3.6px of the 36 it wanted, and it is the one of the mark's three signals that // can go — the warm colour and the bold weight stay. const base = ruleBody(".seatPuckStack.seatPuckAllIn"); const tight = ruleBody(".seatsTight .seatPuckStack.seatPuckAllIn"); assert.ok(base, "the base all-in rule should exist"); assert.ok(tight, "the tight-tier all-in rule should exist"); const spacingOf = body => { const match = body.match(/letter-spacing:\s*([\d.]+)em/); return match ? Number(match[1]) : null; }; const wide = spacingOf(base); const narrow = spacingOf(tight); assert.ok(wide !== null && narrow !== null, "both rules should set letter-spacing in em"); assert.ok(narrow < wide, `the tight tier should space the mark more tightly than the ordinary one (${narrow} vs ${wide})`); // The arithmetic that has to hold for it to fit at all: six characters at 10px // with 4px of padding either side and a 1px border, inside a 44px puck. const CONTENT_PX = 44 - 2 * 4 - 2 * 1; const inkAt = spacing => 32.4 + spacing * 10 * "ALL IN".length; assert.ok(inkAt(narrow) <= CONTENT_PX, `"ALL IN" must fit the narrowest puck: ${inkAt(narrow).toFixed(1)}px into ${CONTENT_PX}px`); assert.ok(inkAt(wide) > CONTENT_PX, "the ordinary spacing is what didn't fit — if it does now, this test is measuring the wrong thing"); }); test("the two signals that survive are the ones doing the work", () => { // The spacing exists so "ALL IN" doesn't read as money at a glance. Dropping it // on the narrow pucks is only safe while the colour and the weight remain, so // they are pinned here rather than left to be tidied away later. const base = ruleBody(".seatPuckStack.seatPuckAllIn"); assert.match(base, /color:\s*#ffcf6b/i, "the warm colour separates it from a grey stack"); assert.match(base, /font-weight:\s*700/, "and the weight does the rest"); }); });