|
|
|
@@ -0,0 +1,154 @@
|
|
|
|
|
// 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");
|
|
|
|
|
});
|
|
|
|
|
});
|