// The bar's pot list is not allowed to grow with the table. // // #actionBar is fixed over the felt, so every pixel it takes is a pixel the table // loses — and this list took one row per all-in with no ceiling. Measured on a 360x640 // phone, five players idle: bar 151px. Six all-in: 303px. Twelve all-in: 411px, which // is 64% of the screen, leaving 96px of felt to draw twelve seats in. A fixed overlay // whose height depends on the state is a fault whatever it is showing, so the cap is // checked here rather than left to whoever next adds an all-in to the audit sweep. // // The two ends survive because they are the two that are live. Side pots are paid from // the newest one back down to the main pot, so the top layer is the next thing that // happens and the main pot is the last, and those are the two a dealer says out loud // while the chips are stacked — "main pot 7,200, side pot 4,500". The layers between // are the middle of that count-down, wanted at showdown, which is the pot wizard, which // passes no cap and still lists all of them. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { Render, Money, PotCalculator } = require("./load"); const nameFor = id => `P${id}`; // The same stub potBreakdown.test.js uses — enough of an element to say what the row // is made of, and no jsdom in a suite that runs in a second. const fakeDom = () => { const make = () => ({ className: "", textContent: "", title: "", children: [], classList: { add() {} }, appendChild(child) { this.children.push(child); return child; }, }); global.document = { createElement: make }; return () => { delete global.document; }; }; // A table where everyone is all-in for a different amount, which is the shape that // makes the most pot layers — the worst case and the one that was measured. `count` // players ladder into `count - 1` layers: two of them share a single undivided pot and // the breakdown declines to draw at all, which is why nothing below three appears here. const laddered = count => PotCalculator.layers( Array.from({ length: count }, (_, i) => ({ id: `s${i}`, contribution: 100 + i * 130, folded: false, }))); const build = (pots, options) => { const undo = fakeDom(); try { const wrap = Render.buildPotBreakdown(pots, [], nameFor, null, options); return wrap.children.map(row => ({ className: row.className, label: row.children[0].textContent, amount: row.children[1].textContent, who: row.children[2].textContent, title: row.children[2].title, })); } finally { undo(); } }; describe("the bar's copy of the pot list", () => { test("stops growing once there are more pots than it has room for", () => { // The property that matters: rows are a function of the cap, not of the table. // Twelve-handed and six-handed have to render the same number of lines. for (let players = 3; players <= 12; players++) { const rows = build(laddered(players), { maxLayers: 4 }); assert.ok(rows.length <= 4, `${players} all-in drew ${rows.length} rows; the bar is sized for 4`); } }); test("and the number of rows never depends on how many people are all-in", () => { const wide = [8, 9, 10, 11, 12].map(n => build(laddered(n), { maxLayers: 4 }).length); assert.deepEqual(wide, [3, 3, 3, 3, 3], "past the cap every table should draw main, the elision and the top pot"); }); test("keeps the two pots a dealer actually announces", () => { const pots = laddered(12); const rows = build(pots, { maxLayers: 4 }); assert.equal(rows[0].label, pots[0].label, "the main pot is the last one paid"); assert.equal(rows[rows.length - 1].label, pots[pots.length - 1].label, "the newest side pot is the first one paid"); }); test("and leaves them in the order the pots were made", () => { const rows = build(laddered(12), { maxLayers: 4 }); assert.match(rows[1].className, /potLayerMore/, "the elision belongs where the gap is, not appended after the list"); }); }); describe("the money in the rows that were elided", () => { test("is still on the screen, added up", () => { // The one thing a summary row must not do is lose chips. Whatever is drawn has // to account for every pot, or a dealer counting the announcement against the // chips in the middle finds a shortfall the tool caused. for (let players = 3; players <= 12; players++) { const pots = laddered(players); const total = pots.reduce((sum, pot) => sum + pot.amount, 0); const drawn = build(pots, { maxLayers: 4 }) .reduce((sum, row) => sum + Number(row.amount.replace(/[^0-9.]/g, "")), 0); assert.equal(drawn, total, `${players} all-in: rows add to ${drawn}, the pots add to ${total}`); } }); test("and says how many it stands for", () => { const rows = build(laddered(12), { maxLayers: 4 }); const pots = laddered(12); assert.equal(rows[1].label, `${pots.length - 2} more side pots`); }); test("with the list itself in a title, for anyone with a pointer", () => { const pots = laddered(12); const rows = build(pots, { maxLayers: 4 }); pots.slice(1, -1).forEach(pot => { assert.ok(rows[1].title.includes(pot.label), `${pot.label} vanished without trace`); assert.ok(rows[1].title.includes(Money.format(pot.amount)), `${pot.label}'s amount vanished without trace`); }); }); test("and points at where the whole list is", () => { // The title is a pointer affordance, so it cannot be the answer on a phone. The // visible text has to say where the rest lives. assert.match(build(laddered(12), { maxLayers: 4 })[1].who, /resolve/i); }); }); describe("when eliding would not buy anything", () => { test("a list that fits is left alone", () => { for (let players = 3; players <= 5; players++) { const pots = laddered(players); assert.ok(pots.length <= 4, "this loop is meant to cover the fitting sizes"); const rows = build(pots, { maxLayers: 4 }); assert.equal(rows.length, pots.length); assert.deepEqual(rows.map(r => r.label), pots.map(p => p.label)); } }); test("and a single middle layer is not replaced by a line saying there is one", () => { // At exactly the cap the middle is one row, and swapping one row for one row is // pure indirection: the dealer loses an amount and gains a sentence. const pots = laddered(5); assert.equal(pots.length, 4, "five all-in ladder into exactly the cap"); const rows = build(pots, { maxLayers: 4 }); assert.equal(rows.length, 4); assert.ok(!rows.some(r => /potLayerMore/.test(r.className))); }); }); describe("the wizard, which has room", () => { test("still gets every layer", () => { // Same builder, no cap — this is the announcement read out before anybody shows // a hand, and it is in a dialog that scrolls. Capping it there would hide the // pots at the moment they are being paid. const pots = laddered(12); const rows = build(pots, undefined); assert.equal(rows.length, pots.length); assert.ok(!rows.some(r => /potLayerMore/.test(r.className))); }); }); // ---- And smaller still once nobody can act ----------------------------------- // // The list is decision support. A player working out whether to call is owed the side // pot they would be calling into, and that is worth three rows of a fixed bar. // // Once everyone is all-in there is no decision left to support: the only thing that // happens for the rest of the hand is the dealer turning cards. The list went on // standing there anyway, and it is not free — measured at 360x640 with twelve players // all-in, the bar was 267px of a 640px window. That is 42%, against the sweep's own // "persistent chrome under 30%", and the felt pays it on the exact table shape that has // the most seats to fit into what is left. // // So during a run-out it collapses to the main pot and one line for everything else. // The main pot stays because it is the number a dealer says out loud; the layering is // what the pot wizard is for, and the summary row has always said so in as many words. describe("the list during a run-out, when nobody has a decision left", () => { test("is two rows however many people are all-in", () => { const rows = [3, 6, 9, 12].map(n => build(laddered(n), { maxLayers: 4, summarise: true }).length); assert.deepEqual(rows, [2, 2, 2, 2]); }); test("and that is fewer than the same table gets while the betting is live", () => { // The point of the change, stated as a comparison rather than a constant, so it // still means something if the ordinary cap is ever retuned. const live = build(laddered(12), { maxLayers: 4 }).length; const runout = build(laddered(12), { maxLayers: 4, summarise: true }).length; assert.ok(runout < live, `a run-out should be shorter than live betting (${runout} vs ${live})`); }); test("still names the main pot, which is the one said out loud", () => { const pots = laddered(12); const rows = build(pots, { maxLayers: 4, summarise: true }); assert.equal(rows[0].label, pots[0].label); assert.equal(rows[0].amount, Money.format(pots[0].amount)); }); test("and still accounts for every chip", () => { // The same rule as the elision above, and the one a summary row must not break: // a dealer counting the announcement against the chips in the middle must not // find a shortfall the tool invented. for (let players = 3; players <= 12; players++) { const pots = laddered(players); const total = pots.reduce((sum, pot) => sum + pot.amount, 0); const drawn = build(pots, { maxLayers: 4, summarise: true }) .reduce((sum, row) => sum + Number(row.amount.replace(/[^0-9.]/g, "")), 0); assert.equal(drawn, total, `${players} all-in: rows add to ${drawn}, the pots add to ${total}`); } }); test("says how many side pots it stands for, and counts them properly at one", () => { const many = build(laddered(12), { maxLayers: 4, summarise: true }); assert.equal(many[1].label, `${laddered(12).length - 1} side pots`); // Three players ladder into two pots: a main and exactly one side. const one = build(laddered(3), { maxLayers: 4, summarise: true }); assert.equal(one[1].label, "1 side pot", "a single side pot is not '1 side pots'"); }); test("and points at where the full list actually is", () => { const rows = build(laddered(12), { maxLayers: 4, summarise: true }); assert.match(rows[1].who, /resolve/, "the row that hides the layers has to say where they went"); assert.match(rows[1].title, /Side pot/, "and a pointer still gets the full list without resolving anything"); }); });