The standings had a column headed "Cash out" and it did not hold an amount to pay.
It held the difference against the player's buy-in. Measured on a table where Ann
bought in for $200 and sits behind $340:
PLAYER NET CASH OUT
Ann $0 +$140
She is handed $340. The column called "Cash out" says $140, under a heading that
invites reading it as the payout, at the exact moment somebody is counting money onto
the table. The number a banker's hands actually follow — what the player is holding —
was computed all along and written to the CSV, and never shown on screen.
Three columns now, and they say what they are: Player, Chips, Up/down. Chips is a
plain amount and never signed, because it is a quantity of money to hand over. Up/down
keeps the sign and the colour, because it is a result. The two differ by exactly the
buy-in, which is asserted rather than assumed.
Chips counts what is out in the middle for the hand in progress as still theirs, for
the same reason the up/down figure does: the panel is read mid-hand constantly and a
stack that drops by the size of a bet looks like money going missing.
Net — won less paid in, from the hand log — comes off the screen to make room. It is
the weakest of the three: it only moves when a pot is resolved through the wizard, so
at a table where the dealer settles pots by hand it reads $0 all night beside two
columns that don't, and up/down already answers "how are they doing". It stays in the
CSV export, which loses nothing.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
225 lines
10 KiB
JavaScript
225 lines
10 KiB
JavaScript
// The banker's job, which is a different job from the dealer's: every chip on the
|
|
// table is a promise to pay cash for it later, and somebody has to be able to check
|
|
// that the promise can be kept before the last two players try to cash out.
|
|
const { test, describe } = require("node:test");
|
|
const assert = require("node:assert/strict");
|
|
const { Betting, HoldemState, Money, makeTable } = require("./load");
|
|
|
|
const seat = (state, index, name, cash, chips) => {
|
|
const p = state.seats[index];
|
|
p.name = name; p.inHand = true; p.buyIns = cash; p.stack = chips;
|
|
return p;
|
|
};
|
|
|
|
describe("everyone with money in the game is accounted for", () => {
|
|
test("a player who has bought in but played no logged hand still appears", () => {
|
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 0 });
|
|
seat(state, 0, "Ann", 100, 100);
|
|
seat(state, 1, "Ben", 100, 100);
|
|
seat(state, 2, "Cal", 200, 200); // just sat down
|
|
|
|
state.seats[0].amountOverride = 10;
|
|
state.seats[1].amountOverride = 10;
|
|
HoldemState.recordHand(state, [{ id: state.seats[0].id, label: "Ann", amount: 20 }]);
|
|
state.seats[0].amountOverride = null;
|
|
state.seats[1].amountOverride = null;
|
|
|
|
const names = HoldemState.sessionStandings(state).map(r => r.name);
|
|
assert.ok(names.includes("Cal"), `Cal handed over $200 and is missing: ${names.join(", ")}`);
|
|
});
|
|
|
|
test("their row reads as no hands played, not as a loss", () => {
|
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
|
seat(state, 0, "Ann", 100, 100);
|
|
seat(state, 1, "Ben", 200, 200);
|
|
|
|
const cal = HoldemState.sessionStandings(state).find(r => r.name === "Ben");
|
|
assert.equal(cal.in, 0);
|
|
assert.equal(cal.won, 0);
|
|
assert.equal(cal.net, 0);
|
|
assert.equal(cal.boughtIn, 200);
|
|
assert.equal(cal.cashPosition, 0, "level: they've bought chips and still hold them");
|
|
});
|
|
|
|
test("an empty chair is not dragged into the settle-up", () => {
|
|
const state = makeTable({ seatCount: 9, seated: [0], stack: 0 });
|
|
seat(state, 0, "Ann", 100, 100);
|
|
const rows = HoldemState.sessionStandings(state);
|
|
assert.equal(rows.length, 1);
|
|
});
|
|
|
|
test("somebody who has cashed out completely still shows what they took", () => {
|
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
|
seat(state, 0, "Ann", 100, 0); // handed every chip back
|
|
seat(state, 1, "Ben", 100, 200);
|
|
const ann = HoldemState.sessionStandings(state).find(r => r.name === "Ann");
|
|
assert.equal(ann.cashPosition, -100, "down the whole buy-in");
|
|
});
|
|
});
|
|
|
|
describe("does the cash box cover the chips on the table", () => {
|
|
test("a straight game balances", () => {
|
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 0 });
|
|
seat(state, 0, "Ann", 100, 60);
|
|
seat(state, 1, "Ben", 100, 140);
|
|
seat(state, 2, "Cal", 100, 100);
|
|
|
|
const bank = HoldemState.bankReconciliation(state);
|
|
assert.equal(bank.cashIn, 300);
|
|
assert.equal(bank.chipsInPlay, 300);
|
|
assert.equal(bank.difference, 0);
|
|
assert.equal(bank.balanced, true);
|
|
});
|
|
|
|
test("chips outrunning the cash box is caught before cash-out", () => {
|
|
// The 2am failure: the last players out can't be paid.
|
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 0 });
|
|
seat(state, 0, "Ann", 100, 100);
|
|
seat(state, 1, "Ben", 100, 100);
|
|
seat(state, 2, "Cal", 100, 260); // took chips for a rebuy nobody wrote down
|
|
|
|
const bank = HoldemState.bankReconciliation(state);
|
|
assert.equal(bank.cashIn, 300);
|
|
assert.equal(bank.chipsInPlay, 460);
|
|
assert.equal(bank.difference, 160);
|
|
assert.equal(bank.balanced, false);
|
|
});
|
|
|
|
test("cash taken for chips never handed over is caught too", () => {
|
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
|
seat(state, 0, "Ann", 200, 100); // paid 200, only got 100 in chips
|
|
seat(state, 1, "Ben", 100, 100);
|
|
|
|
const bank = HoldemState.bankReconciliation(state);
|
|
assert.equal(bank.difference, -100);
|
|
assert.equal(bank.balanced, false);
|
|
});
|
|
|
|
test("chips in the middle still count as chips in play", () => {
|
|
// Money in the pot is somebody's; it just hasn't been won yet. Leaving it out
|
|
// would report a shortfall every time a hand was open — which is nearly always.
|
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
|
state.seats.forEach(p => { if (p.inHand) p.buyIns = 100; });
|
|
|
|
const before = HoldemState.bankReconciliation(state);
|
|
assert.equal(before.balanced, true);
|
|
|
|
Betting.beginHand(state); // blinds go in
|
|
Betting.raiseTo(state, 0, 25); // and a raise
|
|
Betting.sweepToHandTotals(state); // banked into the pot
|
|
|
|
const during = HoldemState.bankReconciliation(state);
|
|
assert.equal(during.balanced, true, "an open pot is not a discrepancy");
|
|
assert.equal(during.chipsInPlay, before.chipsInPlay);
|
|
});
|
|
|
|
test("a whole session of play never knocks the books out", () => {
|
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
|
state.seats.forEach(p => { if (p.inHand) p.buyIns = 100; });
|
|
|
|
for (let hand = 0; hand < 6; hand++) {
|
|
Betting.beginHand(state);
|
|
Betting.liveSeats(state).forEach(s => {
|
|
if (Betting.toCall(state, s.player) > 0) Betting.call(state, s.index);
|
|
});
|
|
Betting.sweepToHandTotals(state);
|
|
|
|
const pot = HoldemState.totalPot(state);
|
|
const winner = Betting.liveSeats(state)[0];
|
|
const payouts = [{ id: winner.player.id, label: "w", amount: pot }];
|
|
HoldemState.recordHand(state, payouts);
|
|
Betting.creditWinnings(state, payouts);
|
|
HoldemState.startNextHand(state);
|
|
|
|
assert.equal(HoldemState.bankReconciliation(state).balanced, true, `books off after hand ${hand + 1}`);
|
|
}
|
|
});
|
|
|
|
test("a rebuy keeps the books straight, a chip grab doesn't", () => {
|
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
|
seat(state, 0, "Ann", 100, 100);
|
|
seat(state, 1, "Ben", 100, 100);
|
|
|
|
// A proper rebuy: cash to the banker, chips to the player.
|
|
state.seats[0].buyIns += 100;
|
|
state.seats[0].stack += 100;
|
|
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
|
|
|
// Chips taken without paying.
|
|
state.seats[1].stack += 50;
|
|
const bank = HoldemState.bankReconciliation(state);
|
|
assert.equal(bank.balanced, false);
|
|
assert.equal(bank.difference, 50);
|
|
});
|
|
|
|
test("nothing to reconcile before anybody has bought in", () => {
|
|
const state = makeTable({ seatCount: 6, seated: [], stack: 0 });
|
|
const bank = HoldemState.bankReconciliation(state);
|
|
assert.equal(bank.cashIn, 0);
|
|
assert.equal(bank.chipsInPlay, 0);
|
|
assert.equal(bank.balanced, true);
|
|
});
|
|
|
|
test("float dust is not a discrepancy", () => {
|
|
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
|
seat(state, 0, "Ann", 0.1 + 0.2, 0.3);
|
|
seat(state, 1, "Ben", 10, 10);
|
|
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
|
});
|
|
});
|
|
|
|
describe("the number the banker counts out", () => {
|
|
// The standings had two signed columns and neither was an amount to pay. A player
|
|
// up $140 on a $340 stack read "$140" under a heading saying "Cash out", at the
|
|
// exact moment somebody was counting money onto the table.
|
|
test("chips in front is what they are holding, not what they are up", () => {
|
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 340 });
|
|
state.seats[0].name = "Ann";
|
|
state.seats[0].buyIns = 200;
|
|
state.seats[1].buyIns = 200;
|
|
state.seats[1].stack = 60;
|
|
|
|
const rows = HoldemState.sessionStandings(state);
|
|
const ann = rows.find(r => r.name === "Ann");
|
|
assert.equal(ann.chipsInFront, 340, "the cash to count out");
|
|
assert.equal(ann.cashPosition, 140, "and separately, that they are up $140");
|
|
});
|
|
|
|
test("a player who is down still has chips to be handed", () => {
|
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 60 });
|
|
state.seats[0].name = "Cal";
|
|
state.seats.forEach(p => { p.buyIns = 200; });
|
|
|
|
const cal = HoldemState.sessionStandings(state).find(r => r.name === "Cal");
|
|
assert.equal(cal.chipsInFront, 60, "they leave with $60 of chips");
|
|
assert.equal(cal.cashPosition, -140, "having lost $140 of the $200 they brought");
|
|
});
|
|
|
|
test("chips still out in the middle are counted as theirs", () => {
|
|
// Cashing out mid-hand isn't a thing, but the panel is read mid-hand constantly
|
|
// and a stack that drops by the size of a bet looks like money going missing.
|
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 200 });
|
|
state.seats[0].name = "Dee";
|
|
state.seats.forEach(p => { p.buyIns = 200; });
|
|
Betting.beginHand(state);
|
|
Betting.raiseTo(state, 0, 50);
|
|
|
|
const dee = HoldemState.sessionStandings(state).find(r => r.name === "Dee");
|
|
assert.equal(dee.chipsInFront, 200, "stack plus what's in the middle for them");
|
|
assert.equal(dee.cashPosition, 0, "so they are still level");
|
|
});
|
|
|
|
test("chips and up/down always differ by exactly the buy-in", () => {
|
|
const state = makeTable({ seatCount: 9, seated: [0, 1, 2, 3], stack: 100 });
|
|
[340, 150, 60, 250].forEach((chips, i) => {
|
|
state.seats[i].stack = chips;
|
|
state.seats[i].buyIns = 200 + i * 50;
|
|
});
|
|
|
|
HoldemState.sessionStandings(state).forEach(row => {
|
|
assert.equal(Money.round(row.chipsInFront - row.boughtIn), row.cashPosition,
|
|
`${row.name}'s two columns should tell one story`);
|
|
});
|
|
});
|
|
});
|