// 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`); }); }); });