Files
JoshHeaps.Net/tests/holdem/potBreakdown.test.js
T
jheapsandClaude Opus 5 bd8ee11f77 Say who can win each pot in the fewest names that answer it
The pot breakdown listed every eligible player for every pot. Pot
eligibility is nested — the main pot is everyone still live and each side
pot above it drops whoever was all-in below — so the main pot's line was
every name at the table and each side pot repeated it minus one.

The lines too long to fit were exactly the ones nobody needed spelled
out, and the short ones at the bottom were the side pots a dealer
actually announces by name. Measured on an iPhone 14: a single all-in
five-handed already clipped the main pot, and twelve-handed the first row
wanted 521px of a 254px slot. There was no count and no title, so a
dealer reading "Alexandra…" could not tell whether three people or eleven
were eligible — the number the showdown turns on.

Each line now names whichever of "who is in" and "who is out" is shorter,
which nesting guarantees is never more than half the table, and leads
with the count because the line is right-aligned and it is the tail that
gets cut off. "all 9", "8 in, not Mike from work". On a phone this takes
one, two and three all-ins from every main-pot line clipped to nothing
clipped at all; twelve-handed goes 6 clipped rows to 5, and even a cut
row still opens with "9 in".

The wizard's copy of the same announcement wraps instead — it is a dialog
that scrolls, and it is what the dealer reads out before anybody shows a
hand — so nothing there is ever cut off. The full list is also on the
title for anyone with a pointer, which is the extra rather than the
answer, since a tooltip is not a mechanism a keyboard reaches.

potCalculator.test.js now proves the nesting across 20,000 random tables
and asserts on its own generator, so the loop cannot go vacuous. Audit
unchanged at 2 failing configurations and 3 offenders, both pre-existing.
787 unit tests pass.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-05 21:23:19 -06:00

128 lines
6.2 KiB
JavaScript

// Who can win each pot, said in the fewest names that answer it.
//
// The breakdown used to list every eligible player for every pot. Pot eligibility is
// nested — potCalculator.test.js checks that across 20,000 tables — so the main pot's
// line was every name at the table and each side pot repeated it minus one. The lines
// that were too long to fit were exactly the ones nobody needed spelled out, and the
// short ones at the bottom were the side pots a dealer actually announces by name.
//
// Measured on an iPhone 14 before this: one all-in five-handed already clipped the main
// pot, and twelve-handed the first row wanted 521px of a 254px slot, with no count and
// no title — a dealer reading "Alexandra…" could not tell whether three people or
// eleven were eligible, which is the number the showdown turns on.
const test = require("node:test");
const { describe } = require("node:test");
const assert = require("node:assert");
const { Render } = require("./load");
// Names are "P<id>" so a test reads as ids and the joining is still exercised.
const nameFor = id => `P${id}`;
const say = (eligible, everyone) => Render.describeEligible(eligible, everyone, nameFor);
describe("the eligible line", () => {
test("says so plainly when the whole table can win it", () => {
// The main pot, every hand. Twelve names to say "nobody is excluded yet" was
// the single longest line in the component and the least informative.
assert.equal(say(["a", "b", "c"], ["a", "b", "c"]), "all 3");
assert.equal(say(["a"], ["a"]), "all 1");
});
test("names who is out when that is the shorter list", () => {
const table = ["a", "b", "c", "d", "e", "f", "g", "h"];
assert.equal(say(["b", "c", "d", "e", "f", "g", "h"], table), "7 in, not Pa");
assert.equal(say(["c", "d", "e", "f", "g", "h"], table), "6 in, not Pa and Pb");
assert.equal(say(["d", "e", "f", "g", "h"], table), "5 in, not Pa, Pb and Pc");
});
test("names who is in when that is the shorter list", () => {
// The far side pots, which are the ones a dealer announces by name: "side pot
// is 80 between Ben and Carlos".
const table = ["a", "b", "c", "d", "e", "f", "g", "h"];
assert.equal(say(["g", "h"], table), "2 in, Pg and Ph");
assert.equal(say(["h"], table), "1 in, Ph");
assert.equal(say(["f", "g", "h"], table), "3 in, Pf, Pg and Ph");
});
test("and when the two are the same length, says who is in", () => {
// A tie is not a reason to make the dealer do the subtraction: "who can win
// this" is the question, so answer it directly when it costs nothing.
const table = ["a", "b", "c", "d"];
assert.equal(say(["c", "d"], table), "2 in, Pc and Pd");
});
test("leads with the count, because it is the tail that gets cut off", () => {
// .potLayerWho is right-aligned with an ellipsis, so a line too long for a phone
// loses its end. Every form has to begin with the number of hands to ask for.
const table = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l"];
[table, ["c", "d", "e", "f", "g", "h", "i", "j", "k", "l"], ["k", "l"]]
.forEach(eligible => assert.match(say(eligible, table), /^(all \d+|\d+ in,)/,
`"${say(eligible, table)}" buries the count where a narrow screen can eat it`));
});
test("never spells out more than half the table", () => {
// The point of picking the shorter side. Nested eligibility makes the two lists
// complements, so this is a guarantee rather than a hope.
const table = Array.from({ length: 12 }, (_, i) => `s${i}`);
for (let inCount = 1; inCount <= 12; inCount++) {
const line = say(table.slice(0, inCount), table);
const named = (line.match(/P/g) || []).length;
assert.ok(named <= 6, `${inCount} of 12 named ${named} people: "${line}"`);
}
});
});
// A stub rather than a DOM: the file has no jsdom and does not want one. Enough of an
// element to answer what the row is made of.
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; };
};
describe("the row it builds", () => {
const pots = [
{ label: "Main pot", amount: 300, eligible: ["a", "b", "c", "d"] },
{ label: "Side pot 1", amount: 150, eligible: ["c", "d"] },
];
const textOf = row => row.children.map(c => c.textContent);
test("carries the full list in a title, for anyone with a pointer", () => {
const undo = fakeDom();
try {
const wrap = Render.buildPotBreakdown(pots, [], nameFor);
const who = wrap.children.map(row => row.children[2]);
assert.equal(who[0].title, "Pa, Pb, Pc, Pd");
assert.equal(who[1].title, "Pc, Pd");
// The visible text is the short form; the title is the long one. A tooltip
// is not a mechanism a keyboard reaches, which is why it is the extra and
// not the answer.
assert.equal(who[0].textContent, "all 4");
assert.equal(who[1].textContent, "2 in, Pc and Pd");
} finally { undo(); }
});
test("puts the label, the money and the people in that order", () => {
const undo = fakeDom();
try {
const wrap = Render.buildPotBreakdown(pots, [], nameFor);
assert.deepEqual(textOf(wrap.children[0]), ["Main pot", "$300", "all 4"]);
} finally { undo(); }
});
test("and an uncalled bet still reads as money going back", () => {
// Not an eligibility question — nobody is competing for it — so it keeps its
// own wording and gets no title to expand.
const undo = fakeDom();
try {
const wrap = Render.buildPotBreakdown(pots, [{ id: "a", amount: 25 }], nameFor);
const row = wrap.children[2];
assert.deepEqual(textOf(row), ["Uncalled", "$25", "back to Pa"]);
assert.equal(row.children[2].title, "");
} finally { undo(); }
});
});