diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml
index 5db9c99..40dbd8c 100644
--- a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml
+++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml
@@ -162,6 +162,10 @@
House take
+
+ Paid out
+
+
diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css
index 0f0df21..ebabf22 100644
--- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css
+++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css
@@ -2294,6 +2294,24 @@ body.hasActionBar #holdemSidebar {
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 ---------- */
.potBreakdown {
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/handHistoryExport.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/handHistoryExport.js
index 1d0bf38..582f8ba 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/handHistoryExport.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/handHistoryExport.js
@@ -54,7 +54,10 @@ const HandHistoryExport = {
rows.push(["Player", "Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down", "Notes"]);
standings.forEach(row => {
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.won),
Money.forSheet(row.net),
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
index 327cc9a..8f48301 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
@@ -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
// swap is easy to aim wrong and the seats look alike at a glance.
swapSeats: (from, to) => {
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
index 19bf744..f9c38f5 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
@@ -1012,7 +1012,14 @@ const Render = {
const template = document.getElementById("standingsRowTemplate");
standings.forEach(row => {
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(".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");
// What has left the table on purpose, whichever way it went.
const accountedFor = [
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.paidOut > 0 ? `${Money.format(bank.paidOut)} paid out to players who have left` : null,
].filter(Boolean);
if (bank.balanced) {
verdict.textContent = accountedFor.length > 0
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js
index 6feb934..a90c5b2 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js
@@ -47,6 +47,13 @@ const SeatMenu = {
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) {
actions.push({ label: "Give the button", run: () => this.hooks.makeDealer(index) });
}
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
index 34cfa20..c8ae787 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
@@ -110,6 +110,11 @@ const HoldemState = {
// 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.
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
// felt can badge them. Read off the deal rather than worked out again
// later, the same way the posted blinds are.
@@ -228,6 +233,17 @@ const HoldemState = {
noFlopNoDrop: rake.noFlopNoDrop !== false,
};
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 = [];
// 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
// 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]));
+ // 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
// 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())
.map(t => {
const seat = seatById.get(t.id);
- const boughtIn = seat ? Money.round(Number(seat.buyIns) || 0) : 0;
- const chipsNow = seat ? Money.round(Number(seat.stack) || 0) : 0;
+ const gone = goneById.get(t.id);
+ 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
// 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 —
@@ -806,6 +839,10 @@ const HoldemState = {
// a $340 stack read as "$140" at the exact moment cash changed
// hands.
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),
notes,
};
@@ -838,6 +875,84 @@ const HoldemState = {
//
// 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.
+ // ---- 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 ----
//
// 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) {
- 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(
(sum, p) =>
sum +
@@ -922,12 +1042,17 @@ const HoldemState = {
// for a better reason: the box is supposed to have them.
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 {
cashIn: Money.round(cashIn),
chipsInPlay: Money.round(chipsInPlay),
discarded,
raked,
+ paidOut,
difference,
// Half a cent of float dust isn't a discrepancy worth alarming anyone over.
balanced: Math.abs(difference) <= Money.EPSILON,
diff --git a/tests/holdem/cashOut.test.js b/tests/holdem/cashOut.test.js
new file mode 100644
index 0000000..56aa2b4
--- /dev/null
+++ b/tests/holdem/cashOut.test.js
@@ -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);
+ });
+});