Hold'em: every seat on the ring is the same seat, and the ring is measured before it is believed #122
@@ -24,6 +24,32 @@ const PotCalculator = {
|
|||||||
return entries.filter(e => !e.folded && e.remaining > this.EPSILON);
|
return entries.filter(e => !e.folded && e.remaining > this.EPSILON);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The most this player can be handed if they win the round outright.
|
||||||
|
//
|
||||||
|
// resolveRound caps the pool at the winner's own remaining contribution and then
|
||||||
|
// takes min(remaining, cap) from every entry, dead money included — so the ceiling
|
||||||
|
// is that sum. It is not the player's own contribution, which is what the wizard
|
||||||
|
// used to print beside each name under the words "up to": with three players in
|
||||||
|
// for $250 each the rows all read "up to $250" while the prompt above them said
|
||||||
|
// $765 was in play, and $765 is what the winner was actually paid. A dealer who
|
||||||
|
// trusted the row would have short-paid by two thirds.
|
||||||
|
ceilingFor(entries, id) {
|
||||||
|
const winner = entries.find(e => e.id === id);
|
||||||
|
if (!winner || winner.remaining <= this.EPSILON) return 0;
|
||||||
|
return Money.round(entries.reduce(
|
||||||
|
(sum, e) => sum + Math.min(e.remaining, winner.remaining), 0));
|
||||||
|
},
|
||||||
|
|
||||||
|
// The same number, but only when it is worth saying: a ceiling equal to everything
|
||||||
|
// on the table is the prompt's own figure repeated once per row, which is ink that
|
||||||
|
// carries no data. It appears exactly when somebody is in for less than the rest —
|
||||||
|
// which is the one time a dealer needs to be told, because it is the hand where
|
||||||
|
// picking that winner leaves a side pot still to award.
|
||||||
|
bindingCeilingFor(entries, id) {
|
||||||
|
const ceiling = this.ceilingFor(entries, id);
|
||||||
|
return ceiling < this.totalPot(entries) - this.EPSILON ? ceiling : null;
|
||||||
|
},
|
||||||
|
|
||||||
// Splits the money on the table into the main pot and any side pots, with who can
|
// Splits the money on the table into the main pot and any side pots, with who can
|
||||||
// win each. Read-only — this is what a dealer says out loud before the showdown
|
// win each. Read-only — this is what a dealer says out loud before the showdown
|
||||||
// ("main pot four-eighty, side pot ninety between seats 3 and 5"), which is the
|
// ("main pot four-eighty, side pot ninety between seats 3 and 5"), which is the
|
||||||
|
|||||||
@@ -218,10 +218,18 @@ const PotWizard = {
|
|||||||
}
|
}
|
||||||
label.appendChild(text);
|
label.appendChild(text);
|
||||||
|
|
||||||
const upTo = document.createElement("span");
|
// What this row costs if it is the one ticked — but only on the rows where
|
||||||
upTo.className = "potWizardChoiceUpTo";
|
// that is less than the figure in the prompt above. On a pot everybody
|
||||||
upTo.textContent = `up to ${Money.format(c.remaining)}`;
|
// covered it was the same number three times, and it was the wrong number:
|
||||||
label.appendChild(upTo);
|
// each player's own stake rather than what winning pays them. See
|
||||||
|
// PotCalculator.bindingCeilingFor.
|
||||||
|
const ceiling = PotCalculator.bindingCeilingFor(this.entries, c.id);
|
||||||
|
if (ceiling !== null) {
|
||||||
|
const upTo = document.createElement("span");
|
||||||
|
upTo.className = "potWizardChoiceUpTo";
|
||||||
|
upTo.textContent = `up to ${Money.format(ceiling)}`;
|
||||||
|
label.appendChild(upTo);
|
||||||
|
}
|
||||||
|
|
||||||
// Cards sit apart from the name rather than inside it, so a dealer reading
|
// Cards sit apart from the name rather than inside it, so a dealer reading
|
||||||
// a row across the table sees the hand, not a sentence.
|
// a row across the table sees the hand, not a sentence.
|
||||||
|
|||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user