Pay somebody out and keep the chair, the cash and the record straight

The most ordinary thing that happens at a home game after midnight — somebody
counts up and goes home — and the tool had nothing for it. Freeing the chair
meant wiping the seat by hand, and the bank check then declared their whole stack
missing: "someone has paid for chips they haven't been given", for the rest of
the night. Measured on a balanced table, cashing a player out with $350 in front
of them left the check $350 short. Wiping their buy-in as well silenced it, at
the price of losing their entire night from the standings and from the file the
dealer hands over at the end.

Cashing out is two facts, and both have to survive the empty chair: the box took
their cash, and the box handed their chips back. Both are kept now, so the chair
opens and the money still adds up.

Cash out… is in the seat menu. It says what to count — "$350 — 3×$100, 2×$25" —
what they bought in for, and whether they leave up or down, because that is the
sentence the dealer says while their hands are moving. The bank check gains a
Paid out line and reads "balanced ... once the $350 paid out to players who have
left is counted", alongside the discards and the house's cut. The standings keep
them with a stripe and the words "· paid out", so nobody counts money out to them
twice at 2am, and the export marks them the same way.

It refuses to pay anybody with money still in the middle: taking chips out of a
pot other people can win, or handing the same chips over twice, are both worse
than waiting for the hand to finish. The chair gets a fresh id, because the
person who had it has gone and the next player to sit down is not them.

Two things this turned up. The first draft worked out what a seat was holding by
itself, read `Number(null)` as a perfectly good zero, and offered to pay a player
with no stack set nothing at all — it asks Betting now, which owns that rule.
The second was mine and the audit caught it: marking the departed row with
opacity took the In and Won cells to 3.83:1, under AA, on the very list a dealer
settles from. It is a stripe instead.

19 new tests, with the departed buy-in dropped from the check and the pot guard
disabled on purpose to confirm they fail. Walked through in the browser: menu,
confirm, payout, bank, standings, undo, and the refusal.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
2026-09-02 08:08:46 -06:00
co-authored by Claude Opus 5
parent 0e2821257f
commit f966d8c340
8 changed files with 444 additions and 6 deletions
@@ -162,6 +162,10 @@
<span>House take</span> <span>House take</span>
<span id="bankRaked"></span> <span id="bankRaked"></span>
</div> </div>
<div class="bankReconcileRow" id="bankPaidOutRow" hidden>
<span>Paid out</span>
<span id="bankPaidOut"></span>
</div>
<p id="bankReconcileVerdict" class="bankReconcileVerdict"></p> <p id="bankReconcileVerdict" class="bankReconcileVerdict"></p>
</div> </div>
@@ -2294,6 +2294,24 @@ body.hasActionBar #holdemSidebar {
font-weight: 600; font-weight: 600;
} }
/* A player who has taken their chips and gone. Still on the list, because the box took
their cash and the night's record has to say so, but set apart so nobody counts money
out to them twice at 2am.
Set apart by a stripe rather than by fading the row, which is what this first did:
at 0.72 the contrast check measured the In and Won cells at 3.83:1, under AA, on the
list a dealer settles the night from. The row says "paid out" in words next to the
name — it doesn't need to be hard to read as well. */
.standingsRow.standingsGone {
background: rgba(255, 255, 255, 0.04);
border-left: 2px solid #4a4a4a;
padding-left: 0.35rem;
}
.standingsRow.standingsGone .standingsName {
font-style: italic;
}
/* ---------- Side-pot breakdown ---------- */ /* ---------- Side-pot breakdown ---------- */
.potBreakdown { .potBreakdown {
@@ -54,7 +54,10 @@ const HandHistoryExport = {
rows.push(["Player", "Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down", "Notes"]); rows.push(["Player", "Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down", "Notes"]);
standings.forEach(row => { standings.forEach(row => {
rows.push([ rows.push([
row.name, // Somebody who has gone home is marked in the file the same way the
// panel marks them, because this is the copy that gets read at the
// end of the night when nobody remembers who has already been paid.
row.cashedOut ? `${row.name} (paid out)` : row.name,
Money.forSheet(row.in), Money.forSheet(row.in),
Money.forSheet(row.won), Money.forSheet(row.won),
Money.forSheet(row.net), Money.forSheet(row.net),
@@ -96,6 +96,39 @@ document.addEventListener("DOMContentLoaded", () => {
}); });
}, },
// Somebody is done for the night. The dealer counts their chips out, the chair
// opens, and both halves of what happened stay on the record — the cash the box
// took from them, and the chips it handed back. Wiping the seat by hand did
// neither: it left the bank check declaring their whole stack missing for the
// rest of the night.
cashOut: async index => {
const allowed = HoldemState.canCashOut(state, index);
if (!allowed.ok) {
Toast.show(allowed.reason, { duration: 4000 });
return;
}
const preview = HoldemState.cashOutPreview(state, index);
const result = preview.net === 0
? "even for the night"
: preview.net > 0
? `up ${Money.format(preview.net)}`
: `down ${Money.format(Math.abs(preview.net))}`;
const chips = preview.breakdown ? ` — ${preview.breakdown}` : "";
const ok = await Dialog.confirm({
title: `Cash out ${preview.name}?`,
message: `Count out ${Money.format(preview.chips)}${chips}. They bought in for ` +
`${Money.format(preview.boughtIn)}, so they leave ${result}. The chair opens up, and the ` +
`night's record keeps both their buy-in and this payout so the bank still balances.`,
confirmLabel: `Pay ${Money.format(preview.chips)}`,
});
if (!ok) return;
runUndoable(`${preview.name} cashed out for ${Money.format(preview.chips)}.`,
() => HoldemState.cashOut(state, index));
},
// Two players change chairs. Undoable like every other seat action, because a // Two players change chairs. Undoable like every other seat action, because a
// swap is easy to aim wrong and the seats look alike at a glance. // swap is easy to aim wrong and the seats look alike at a glance.
swapSeats: (from, to) => { swapSeats: (from, to) => {
@@ -1012,7 +1012,14 @@ const Render = {
const template = document.getElementById("standingsRowTemplate"); const template = document.getElementById("standingsRowTemplate");
standings.forEach(row => { standings.forEach(row => {
const node = template.content.firstElementChild.cloneNode(true); const node = template.content.firstElementChild.cloneNode(true);
node.querySelector(".standingsName").textContent = row.name; // Somebody who has gone home is still on the list — they played, and the
// box took their cash — but the number beside them is money already handed
// over, not chips waiting to be counted. Marked, or a dealer settling up at
// 2am pays them twice.
node.classList.toggle("standingsGone", !!row.cashedOut);
node.querySelector(".standingsName").textContent = row.cashedOut
? `${row.name} · paid out`
: row.name;
node.querySelector(".standingsIn").textContent = Money.format(row.in); node.querySelector(".standingsIn").textContent = Money.format(row.in);
node.querySelector(".standingsWon").textContent = Money.format(row.won); node.querySelector(".standingsWon").textContent = Money.format(row.won);
@@ -1169,11 +1176,23 @@ const Render = {
} }
} }
// Chips handed back to people who have gone home. The third way money leaves
// this table on purpose, and the one a dealer is asked about most: "where did
// Ann's four hundred go?"
const paidRow = document.getElementById("bankPaidOutRow");
if (paidRow) {
paidRow.hidden = !(bank.paidOut > 0);
if (bank.paidOut > 0) {
document.getElementById("bankPaidOut").textContent = Money.format(bank.paidOut);
}
}
const verdict = document.getElementById("bankReconcileVerdict"); const verdict = document.getElementById("bankReconcileVerdict");
// What has left the table on purpose, whichever way it went. // What has left the table on purpose, whichever way it went.
const accountedFor = [ const accountedFor = [
bank.discarded > 0 ? `${Money.format(bank.discarded)} taken off it` : null, bank.discarded > 0 ? `${Money.format(bank.discarded)} taken off it` : null,
bank.raked > 0 ? `${Money.format(bank.raked)} in the house's cut` : null, bank.raked > 0 ? `${Money.format(bank.raked)} in the house's cut` : null,
bank.paidOut > 0 ? `${Money.format(bank.paidOut)} paid out to players who have left` : null,
].filter(Boolean); ].filter(Boolean);
if (bank.balanced) { if (bank.balanced) {
verdict.textContent = accountedFor.length > 0 verdict.textContent = accountedFor.length > 0
@@ -47,6 +47,13 @@ const SeatMenu = {
run: () => this.hooks.toggleDealtIn(index), run: () => this.hooks.toggleDealtIn(index),
}); });
// Leaving for the night, which is a different thing from sitting out a hand:
// the chips go back over the rail and the chair opens. Offered only for a seat
// that has somebody in it, since an empty chair has nobody to pay.
if ((player.name || "").trim() || Number(player.buyIns) > 0 || Number(player.stack) > 0) {
actions.push({ label: "Cash out…", run: () => this.hooks.cashOut(index) });
}
if (!this.state.dedicatedDealer && index !== this.state.dealerSeatIndex) { if (!this.state.dedicatedDealer && index !== this.state.dealerSeatIndex) {
actions.push({ label: "Give the button", run: () => this.hooks.makeDealer(index) }); actions.push({ label: "Give the button", run: () => this.hooks.makeDealer(index) });
} }
+129 -4
View File
@@ -110,6 +110,11 @@ const HoldemState = {
// What it has taken so far tonight. A session total, not a per-hand one: // What it has taken so far tonight. A session total, not a per-hand one:
// it is the number the box is checked against at the end. // it is the number the box is checked against at the end.
rakeTaken: 0, rakeTaken: 0,
// People who have taken their chips and gone. Kept for the night rather
// than forgotten with the seat: the box took their cash and paid it back
// out, and both halves have to stay on the record or the bank check reads
// as a shortfall for the rest of the evening.
cashedOut: [],
// Which seats actually put a straddle out when the hand was dealt, so the // Which seats actually put a straddle out when the hand was dealt, so the
// felt can badge them. Read off the deal rather than worked out again // felt can badge them. Read off the deal rather than worked out again
// later, the same way the posted blinds are. // later, the same way the posted blinds are.
@@ -228,6 +233,17 @@ const HoldemState = {
noFlopNoDrop: rake.noFlopNoDrop !== false, noFlopNoDrop: rake.noFlopNoDrop !== false,
}; };
parsed.rakeTaken = Math.max(0, Money.round(Number(parsed.rakeTaken) || 0)); parsed.rakeTaken = Math.max(0, Money.round(Number(parsed.rakeTaken) || 0));
parsed.cashedOut = Array.isArray(parsed.cashedOut)
? parsed.cashedOut
.filter(row => row && row.id)
.map(row => ({
id: row.id,
name: String(row.name || "").slice(0, 40),
buyIns: Math.max(0, Money.round(Number(row.buyIns) || 0)),
chips: Math.max(0, Money.round(Number(row.chips) || 0)),
at: Number(row.at) || 0,
}))
: [];
if (!Array.isArray(parsed.postedStraddles)) parsed.postedStraddles = []; if (!Array.isArray(parsed.postedStraddles)) parsed.postedStraddles = [];
// A table saved before dealing and betting were separate has no flag, and // A table saved before dealing and betting were separate has no flag, and
@@ -753,6 +769,15 @@ const HoldemState = {
// Cash bought in and chips currently in front don't come from the hand log at // Cash bought in and chips currently in front don't come from the hand log at
// all, so they're read off the seat rather than summed — a rebuy isn't a hand. // all, so they're read off the seat rather than summed — a rebuy isn't a hand.
const seatById = new Map(state.seats.map(p => [p.id, p])); const seatById = new Map(state.seats.map(p => [p.id, p]));
// And for anyone who has gone home, off the record their cash-out left behind.
// Without this they read as a player who bought in for nothing and is holding
// nothing, in the very list the dealer settles the night from.
const goneById = new Map((state.cashedOut || []).map(row => [row.id, row]));
goneById.forEach((row, id) => {
if (totals.has(id)) return;
totals.set(id, { id, name: row.name, in: 0, won: 0 });
});
// Anyone who has money in the game belongs in the settle-up, whether or not // Anyone who has money in the game belongs in the settle-up, whether or not
// they've been in a logged hand yet. Building the list from the hand log alone // they've been in a logged hand yet. Building the list from the hand log alone
@@ -774,8 +799,16 @@ const HoldemState = {
return Array.from(totals.values()) return Array.from(totals.values())
.map(t => { .map(t => {
const seat = seatById.get(t.id); const seat = seatById.get(t.id);
const boughtIn = seat ? Money.round(Number(seat.buyIns) || 0) : 0; const gone = goneById.get(t.id);
const chipsNow = seat ? Money.round(Number(seat.stack) || 0) : 0; const boughtIn = seat
? Money.round(Number(seat.buyIns) || 0)
: (gone ? gone.buyIns : 0);
// What a departed player is "holding" is what the box handed them on
// the way out. It is no longer on the table, so it is marked as paid
// rather than counted again at the end of the night.
const chipsNow = seat
? Money.round(Number(seat.stack) || 0)
: (gone ? gone.chips : 0);
// Chips already pushed into the hand in progress are still this // Chips already pushed into the hand in progress are still this
// player's money until it's won. Leaving them out makes the cash // player's money until it's won. Leaving them out makes the cash
// position disagree with the hand log for as long as a hand is open — // position disagree with the hand log for as long as a hand is open —
@@ -806,6 +839,10 @@ const HoldemState = {
// a $340 stack read as "$140" at the exact moment cash changed // a $340 stack read as "$140" at the exact moment cash changed
// hands. // hands.
chipsInFront: Money.round(chipsNow + inFrontOfThem), chipsInFront: Money.round(chipsNow + inFrontOfThem),
// Set once they have taken their chips and left, so the panel and
// the export can say so rather than showing a player still sitting
// there with a stack.
cashedOut: !!gone && !seat,
cashPosition: Money.round(chipsNow + inFrontOfThem - boughtIn), cashPosition: Money.round(chipsNow + inFrontOfThem - boughtIn),
notes, notes,
}; };
@@ -838,6 +875,84 @@ const HoldemState = {
// //
// Chips in play counts everything a player could hand back: what's in front of // Chips in play counts everything a player could hand back: what's in front of
// them and what's in the middle waiting to be won. // them and what's in the middle waiting to be won.
// ---- Somebody leaving with their chips ----
//
// The most ordinary thing that happens at a home game after midnight, and the tool
// had nothing for it. A dealer's only way to free the chair was to wipe the seat by
// hand, and then the bank check — which adds up cash taken against chips on the
// table — declared the departing player's whole stack missing: "someone has paid
// for chips they haven't been given", for the rest of the night. Wiping their
// buy-in as well silenced it, at the price of losing their night from the standings
// and the file the dealer hands over.
//
// Cashing out is two facts, and both have to survive the empty chair: the box took
// their cash, and the box paid their chips back out.
// Can this seat be cashed out right now?
canCashOut(state, index) {
const player = state.seats[index];
if (!player) return { ok: false, reason: "There's nobody in that seat." };
const inPot = Money.round((Number(player.committed) || 0) + (Number(player.amountOverride) || 0));
if (inPot > 0) {
return {
ok: false,
// Paying someone out with money still in the middle would either take
// chips out of a pot other people can win, or hand them chips twice.
reason: `${this.displayName(player, index)} has ${Money.format(inPot)} in the pot. Finish the hand first.`,
};
}
return { ok: true };
},
// What the dealer counts out, and what the seat's night comes to.
cashOutPreview(state, index) {
const player = state.seats[index];
if (!player) return null;
// Betting owns the answer to "what is this seat holding" — including the rule
// that a seat which has never been given a stack is holding the table's
// starting one. Asking it rather than working it out again here: the first
// draft of this did, read `Number(null)` as a perfectly good zero, and offered
// to pay a player nothing.
const chips = Money.round(Math.max(0, Betting.stackOf(state, player)));
const boughtIn = Money.round(Math.max(0, Number(player.buyIns) || 0));
return {
index,
name: this.displayName(player, index),
chips,
boughtIn,
// Their result for the night, in the direction a banker says it: positive
// means the box is paying out more than it took from them.
net: Money.round(chips - boughtIn),
breakdown: Money.formatChipBreakdown(chips, state.chipLegend),
};
},
// Hand them their chips and free the chair, keeping both halves on the record.
cashOut(state, index) {
const allowed = this.canCashOut(state, index);
if (!allowed.ok) return allowed;
const preview = this.cashOutPreview(state, index);
const leavingId = state.seats[index].id;
if (!Array.isArray(state.cashedOut)) state.cashedOut = [];
state.cashedOut.push({
id: leavingId,
name: preview.name,
buyIns: preview.boughtIn,
chips: preview.chips,
at: Date.now(),
});
// The chair opens for the next player. A fresh id, because the person who had
// it has gone and their night is now filed under the old one — reusing it would
// merge two people's results the moment somebody new sits down.
const seat = this.makePlayer();
state.seats[index] = seat;
// Anyone owed a hand by this seat is no longer owed one.
state.postingIn = (state.postingIn || []).filter(id => id !== leavingId);
return { ok: true, ...preview };
},
// ---- The house's cut ---- // ---- The house's cut ----
// //
// A game that takes a rake or a time charge has chips leaving the table every hand, // A game that takes a rake or a time charge has chips leaving the table every hand,
@@ -900,7 +1015,12 @@ const HoldemState = {
}, },
bankReconciliation(state) { bankReconciliation(state) {
const cashIn = state.seats.reduce((sum, p) => sum + Math.max(0, Number(p.buyIns) || 0), 0); const gone = Array.isArray(state.cashedOut) ? state.cashedOut : [];
// Cash the box took from people who have since left counts as taken: it is in
// the box, and forgetting it with the seat is what used to make a cash-out read
// as a shortfall all night.
const cashIn = state.seats.reduce((sum, p) => sum + Math.max(0, Number(p.buyIns) || 0), 0) +
gone.reduce((sum, row) => sum + Math.max(0, Number(row.buyIns) || 0), 0);
const chipsInPlay = state.seats.reduce( const chipsInPlay = state.seats.reduce(
(sum, p) => (sum, p) =>
sum + sum +
@@ -922,12 +1042,17 @@ const HoldemState = {
// for a better reason: the box is supposed to have them. // for a better reason: the box is supposed to have them.
const raked = Math.max(0, Money.round(Number(state.rakeTaken) || 0)); const raked = Math.max(0, Money.round(Number(state.rakeTaken) || 0));
const difference = Money.round(chipsInPlay + discarded + raked - cashIn); // Chips handed back over the rail. Off the table like the other two, and the
// one the dealer can point at when somebody asks where the money went.
const paidOut = Money.round(gone.reduce((sum, row) => sum + Math.max(0, Number(row.chips) || 0), 0));
const difference = Money.round(chipsInPlay + discarded + raked + paidOut - cashIn);
return { return {
cashIn: Money.round(cashIn), cashIn: Money.round(cashIn),
chipsInPlay: Money.round(chipsInPlay), chipsInPlay: Money.round(chipsInPlay),
discarded, discarded,
raked, raked,
paidOut,
difference, difference,
// Half a cent of float dust isn't a discrepancy worth alarming anyone over. // Half a cent of float dust isn't a discrepancy worth alarming anyone over.
balanced: Math.abs(difference) <= Money.EPSILON, balanced: Math.abs(difference) <= Money.EPSILON,
+229
View File
@@ -0,0 +1,229 @@
// Somebody leaving with their chips.
//
// The most ordinary thing that happens at a home game after midnight, and the tool had
// nothing for it. Freeing the chair meant wiping the seat by hand, and then the bank
// check — cash taken against chips on the table — declared the departing player's whole
// stack missing for the rest of the night. Wiping their buy-in too silenced it, at the
// price of losing their night from the standings and the file the dealer hands over.
const test = require("node:test");
const { describe } = require("node:test");
const assert = require("node:assert");
const { Money, HoldemState, Betting, makeTable } = require("./load");
const table = () => {
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 200 });
["Ann", "Ben", "Cal", "Dee"].forEach((name, i) => {
state.seats[i].name = name;
state.seats[i].buyIns = 200;
});
return state;
};
describe("counting them out", () => {
test("the preview is what the dealer's hands follow", () => {
const state = table();
state.seats[0].stack = 350;
const preview = HoldemState.cashOutPreview(state, 0);
assert.equal(preview.name, "Ann");
assert.equal(preview.chips, 350, "what they are holding");
assert.equal(preview.boughtIn, 200);
assert.equal(preview.net, 150, "up for the night");
assert.match(preview.breakdown, /\d+×\$\d+/, "and the chips that make it");
});
test("a player who is down reads as down, not as a smaller stack", () => {
const state = table();
state.seats[1].stack = 60;
assert.equal(HoldemState.cashOutPreview(state, 1).net, -140);
});
test("a seat that never set a stack is holding the table's starting stack", () => {
const state = table();
state.defaultStack = 200;
state.seats[2].stack = null;
assert.equal(HoldemState.cashOutPreview(state, 2).chips, 200);
});
});
describe("what it refuses", () => {
test("paying somebody with money still in the middle", () => {
const state = table();
Betting.beginHand(state);
const bb = state.seats.findIndex(p => (Number(p.committed) || 0) > 0);
const refusal = HoldemState.canCashOut(state, bb);
assert.equal(refusal.ok, false);
assert.match(refusal.reason, /in the pot/);
// And the refusal is the whole story: nothing moved.
assert.equal(HoldemState.cashOut(state, bb).ok, false);
assert.equal((state.cashedOut || []).length, 0);
assert.ok(state.seats[bb].name, "the seat is untouched");
});
test("an empty chair, which has nobody to pay", () => {
const state = table();
const refusal = HoldemState.canCashOut(state, 99);
assert.equal(refusal.ok, false);
});
});
describe("the chair afterwards", () => {
test("is empty and ready for the next player", () => {
const state = table();
state.seats[0].stack = 350;
HoldemState.cashOut(state, 0);
const seat = state.seats[0];
assert.equal(seat.name, "");
assert.equal(seat.stack, null);
assert.equal(seat.buyIns, 0);
assert.equal(seat.inHand, false);
});
test("belongs to somebody new, so two people's nights can't merge", () => {
const state = table();
const wasId = state.seats[0].id;
HoldemState.cashOut(state, 0);
assert.notEqual(state.seats[0].id, wasId);
});
test("and isn't owed the blind the person before them was posting", () => {
const state = table();
state.postingIn = [state.seats[0].id, state.seats[1].id];
HoldemState.cashOut(state, 0);
assert.deepEqual(state.postingIn, [state.seats[1].id]);
});
});
describe("the bank check", () => {
test("balanced before, balanced after — the chips went somewhere known", () => {
const state = table();
state.seats[0].stack = 350;
state.seats[1].stack = 50;
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
HoldemState.cashOut(state, 0);
const bank = HoldemState.bankReconciliation(state);
assert.equal(bank.paidOut, 350);
assert.equal(bank.cashIn, 800, "their buy-in is still cash the box took");
assert.equal(bank.chipsInPlay, 450);
assert.equal(bank.balanced, true);
});
test("this is the fault it fixes: wiping the seat by hand loses $350", () => {
const state = table();
state.seats[0].stack = 350;
state.seats[1].stack = 50; // so the table starts out balanced
// What a dealer had to do before: empty the chair themselves.
state.seats[0].name = "";
state.seats[0].stack = null;
state.seats[0].inHand = false;
const bank = HoldemState.bankReconciliation(state);
assert.equal(bank.balanced, false);
assert.equal(bank.difference, -350, "the tool called it missing money");
});
test("chips that really are missing still show as missing", () => {
// Counting a cash-out must not become a way of swallowing a genuine shortfall.
const state = table();
state.seats[0].stack = 350;
state.seats[1].stack = 50; // balanced to start with
HoldemState.cashOut(state, 0);
state.seats[2].stack = 150; // and now fifty chips have gone astray
const bank = HoldemState.bankReconciliation(state);
assert.equal(bank.balanced, false);
assert.equal(bank.difference, -50);
});
test("a rake and a cash-out are counted apart", () => {
const state = table();
state.seats[0].stack = 350;
state.rakeTaken = 12;
state.seats[1].stack = 38; // the rake came out of Ben's stack
HoldemState.cashOut(state, 0);
const bank = HoldemState.bankReconciliation(state);
assert.equal(bank.raked, 12);
assert.equal(bank.paidOut, 350);
assert.equal(bank.balanced, true);
});
});
describe("the record of their night", () => {
test("they stay in the standings with what they bought and what they left with", () => {
const state = table();
state.seats[0].stack = 350;
HoldemState.recordHand(state, [{ id: state.seats[0].id, label: "Ann", amount: 150 }]);
HoldemState.cashOut(state, 0);
const ann = HoldemState.sessionStandings(state).find(row => row.name === "Ann");
assert.ok(ann, "still on the list");
assert.equal(ann.boughtIn, 200);
assert.equal(ann.chipsInFront, 350, "what the box handed over");
assert.equal(ann.cashPosition, 150);
assert.equal(ann.cashedOut, true, "and marked, so nobody pays them twice");
});
test("somebody still sitting there is not marked", () => {
const state = table();
HoldemState.cashOut(state, 0);
const ben = HoldemState.sessionStandings(state).find(row => row.name === "Ben");
assert.equal(ben.cashedOut, false);
});
test("two people leaving are both kept", () => {
const state = table();
state.seats[0].stack = 350;
state.seats[1].stack = 50;
HoldemState.cashOut(state, 0);
HoldemState.cashOut(state, 1);
assert.equal(state.cashedOut.length, 2);
assert.equal(HoldemState.bankReconciliation(state).paidOut, 400);
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
});
test("a table saved before any of this existed has nobody cashed out", () => {
const parsed = JSON.parse(JSON.stringify(table()));
delete parsed.cashedOut;
const migrated = HoldemState.migrate(parsed);
assert.deepEqual(migrated.cashedOut, []);
assert.equal(HoldemState.bankReconciliation(migrated).paidOut, 0);
});
test("nonsense in a saved record is cleaned up rather than trusted", () => {
const parsed = JSON.parse(JSON.stringify(table()));
parsed.cashedOut = [
{ id: "a", name: "Ann", buyIns: -5, chips: "350", at: "nope" },
{ name: "no id" },
null,
];
const migrated = HoldemState.migrate(parsed);
assert.equal(migrated.cashedOut.length, 1);
assert.equal(migrated.cashedOut[0].buyIns, 0);
assert.equal(migrated.cashedOut[0].chips, 350);
assert.equal(migrated.cashedOut[0].at, 0);
});
});
describe("the table carries on", () => {
test("the next hand deals round the empty chair", () => {
const state = table();
state.seats[0].stack = 350;
HoldemState.cashOut(state, 0);
HoldemState.startNextHand(state);
Betting.beginHand(state);
assert.equal(state.seats[0].inHand, false, "nobody is in the empty chair");
assert.equal(Betting.liveSeats(state).length, 3);
assert.ok(state.actingSeatIndex !== null, "somebody is on the clock");
});
test("and the money still adds up once it has", () => {
const state = table();
state.seats[0].stack = 350;
state.seats[1].stack = 50;
HoldemState.cashOut(state, 0);
HoldemState.startNextHand(state);
Betting.beginHand(state);
const bank = HoldemState.bankReconciliation(state);
assert.equal(Money.round(bank.chipsInPlay + bank.paidOut), bank.cashIn);
assert.equal(bank.balanced, true);
});
});