// 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$/); }); });