// The number the pot wizard prints beside a name under the words "up to". // // It was the player's own remaining contribution. That is not what winning pays them, // and on the screen that moves the money the gap was large and in the wrong direction: // three players in for $250 each showed "up to $250" on all three rows while the prompt // directly above said "$765 is in play" — and $765 is what the winner was actually // handed. A dealer who trusted the row would have short-paid by two thirds. With a real // side pot it was worse in a more confusing way: the breakdown box at the top of the // same dialog announced "Main pot $765 · all 3", and Bob's row three lines under it // said "up to $250". // // The ceiling is now what resolveRound will actually pay, and the test that matters is // that it stays that: every case below checks the label against the function that moves // the chips rather than against a number written out by hand. // // It is also printed only where it binds. A ceiling equal to everything on the table is // the prompt's own figure repeated once per row — redundant data-ink, in Tufte's sense // — and the rows it does appear on are exactly the hands where picking that winner // leaves a side pot still to award, which is the one time a dealer needs telling. const { test, describe } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("fs"); const path = require("path"); const { PotCalculator, Money } = require("./load"); const p = (id, contribution, folded = false) => ({ id, contribution, folded }); const entriesOf = (...players) => PotCalculator.buildEntries(players); // What the wizard would hand this player if their row were the one ticked. const paysOut = (entries, id) => PotCalculator.resolveRound(entries, [id]).payoutPool; // --------------------------------------------------------------------------- describe("what a row promises is what the round pays", () => { test("a pot everybody covered pays the whole thing, dead blinds included", () => { // Three in for $250, two blinds folded before the raise. const entries = entriesOf(p("chris", 250), p("bob", 250), p("dee", 250), p("sb", 5, true), p("bb", 10, true)); for (const id of ["chris", "bob", "dee"]) { assert.equal(PotCalculator.ceilingFor(entries, id), 765); assert.equal(PotCalculator.ceilingFor(entries, id), paysOut(entries, id)); } }); test("the short all-in's ceiling is the main pot, not their own stake", () => { // Bob is all in for 250; Christopher and Dee go on to 600. const entries = entriesOf(p("chris", 600), p("bob", 250), p("dee", 600), p("sb", 5, true), p("bb", 10, true)); assert.equal(PotCalculator.ceilingFor(entries, "bob"), 765); assert.equal(PotCalculator.ceilingFor(entries, "chris"), 1465); assert.equal(PotCalculator.ceilingFor(entries, "dee"), 1465); }); // The property, over every shape the wizard can reach: a ceiling that disagreed // with the payout would be the same defect again under a different number. test("across every shape of pot, the ceiling is the payout", () => { const shapes = [ [p("a", 10), p("b", 10)], [p("a", 10), p("b", 25), p("c", 25)], [p("a", 5), p("b", 50), p("c", 120), p("d", 120)], [p("a", 33.33), p("b", 66.67), p("c", 100)], [p("a", 20), p("b", 20), p("folder", 20, true)], [p("a", 1), p("b", 2), p("c", 3), p("d", 4), p("e", 5)], [p("short", 0.05), p("deep", 1000)], ]; shapes.forEach((shape, n) => { const entries = entriesOf(...shape); PotCalculator.getContenders(entries).forEach(c => { assert.equal(PotCalculator.ceilingFor(entries, c.id), paysOut(entries, c.id), `shape ${n}: ${c.id}`); }); }); }); test("a second round asks about what is left, not what was there", () => { const entries = entriesOf(p("chris", 600), p("bob", 250), p("dee", 600)); const after = PotCalculator.resolveRound(entries, ["bob"]).updatedEntries; // Bob took $750 of the $1,450; $700 is left between the two deep stacks. assert.equal(PotCalculator.totalPot(after), 700); assert.equal(PotCalculator.ceilingFor(after, "chris"), 700); assert.equal(PotCalculator.ceilingFor(after, "dee"), 700); }); test("somebody already paid out has nothing left to be owed", () => { const entries = entriesOf(p("a", 100), p("b", 100)); const after = PotCalculator.resolveRound(entries, ["a"]).updatedEntries; assert.equal(PotCalculator.ceilingFor(after, "a"), 0); }); test("a name nobody put chips in for is worth nothing rather than an error", () => { assert.equal(PotCalculator.ceilingFor(entriesOf(p("a", 10)), "ghost"), 0); }); }); describe("and it is only said where it binds", () => { test("a pot everybody covered says its figure once, in the prompt", () => { const entries = entriesOf(p("chris", 250), p("bob", 250), p("dee", 250), p("sb", 5, true), p("bb", 10, true)); ["chris", "bob", "dee"].forEach(id => assert.equal(PotCalculator.bindingCeilingFor(entries, id), null)); }); test("only the player who is in for less carries a number", () => { const entries = entriesOf(p("chris", 600), p("bob", 250), p("dee", 600), p("sb", 5, true), p("bb", 10, true)); assert.equal(PotCalculator.bindingCeilingFor(entries, "bob"), 765); assert.equal(PotCalculator.bindingCeilingFor(entries, "chris"), null); assert.equal(PotCalculator.bindingCeilingFor(entries, "dee"), null); }); // Two short stacks at different heights: each is capped by the one above, so both // rows have something to say and they do not say the same thing. test("two all-ins at different heights each carry their own", () => { const entries = entriesOf(p("small", 50), p("middle", 120), p("big", 400)); assert.equal(PotCalculator.bindingCeilingFor(entries, "small"), 150); assert.equal(PotCalculator.bindingCeilingFor(entries, "middle"), 290); assert.equal(PotCalculator.bindingCeilingFor(entries, "big"), null); }); test("the deepest player is never the one capped", () => { const entries = entriesOf(p("a", 10), p("b", 50), p("c", 900)); assert.equal(PotCalculator.bindingCeilingFor(entries, "c"), null); assert.equal(PotCalculator.ceilingFor(entries, "c"), PotCalculator.totalPot(entries)); }); // Dead money is owed to whoever wins the layer it fell in, so it lifts every // ceiling — including the short stack's, which is the whole reason Bob's row reads // $765 rather than $750. test("a folded player's chips lift the ceilings rather than sitting outside them", () => { const live = entriesOf(p("a", 100), p("b", 100)); const withDead = entriesOf(p("a", 100), p("b", 100), p("folder", 40, true)); assert.equal(PotCalculator.ceilingFor(live, "a"), 200); assert.equal(PotCalculator.ceilingFor(withDead, "a"), 240); }); test("money in cents does not drift", () => { const entries = entriesOf(p("a", 0.05), p("b", 0.05), p("c", 0.05)); assert.equal(PotCalculator.ceilingFor(entries, "a"), Money.round(0.15)); }); }); describe("the wizard asks the calculator rather than reaching for the stake", () => { const SRC = fs.readFileSync( path.join(__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "js", "casino", "holdem", "potWizard.js"), "utf8"); test("the row's number comes from bindingCeilingFor", () => { assert.match(SRC, /PotCalculator\.bindingCeilingFor\(this\.entries, c\.id\)/); }); test("and nothing prints a contender's own stake under those words any more", () => { assert.ok(!/up to \$\{Money\.format\(c\.remaining\)\}/.test(SRC), "the wizard still prints the player's own contribution as their ceiling"); }); });