diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml
index 7b6ada2..ae5cbb5 100644
--- a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml
+++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml
@@ -127,6 +127,10 @@
Chips in play
+
+ Taken off the table
+
+
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
index d2a1da3..13c732a 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
@@ -387,6 +387,12 @@ document.addEventListener("DOMContentLoaded", () => {
: "";
runUndoable(`Next hand — ${where}. Seat the table, then deal.${discarded}`, () => {
+ // Written down as well as announced. The toast is a moment; the bank check
+ // is asked at cash-out, and until now it saw only that the chips were gone
+ // and reported them as missing.
+ if (unresolved > Betting.EPSILON_CHIPS) {
+ state.discarded = Money.round((Number(state.discarded) || 0) + unresolved);
+ }
HoldemState.startNextHand(state);
});
});
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
index 87838cf..0f03a67 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
@@ -876,8 +876,13 @@ const Render = {
note.textContent =
`${Money.format(plan.shortBy)} of winnings has nothing to pay it — there are more chips on the table than cash was taken for. Fix that above before anyone cashes out.`;
} else {
- note.textContent =
- `${Money.format(plan.overBy)} is owed with nobody to pay it to — more cash was taken than there are chips in play. Fix that above before anyone cashes out.`;
+ // If a pot was thrown away on purpose, that's where the gap went, and the
+ // dealer already answered a dialog about it. Telling them to go and fix it
+ // would be sending them after their own decision.
+ const discarded = Math.max(0, Number(this.state.discarded) || 0);
+ note.textContent = discarded > 0
+ ? `${Money.format(plan.overBy)} is owed with nobody to pay it to. ${Money.format(discarded)} was taken off the table when a hand was abandoned, which accounts for ${discarded >= plan.overBy ? "it" : "some of it"}.`
+ : `${Money.format(plan.overBy)} is owed with nobody to pay it to — more cash was taken than there are chips in play. Fix that above before anyone cashes out.`;
}
note.classList.toggle("settleUpOff", !plan.balanced);
},
@@ -896,9 +901,22 @@ const Render = {
document.getElementById("bankCashIn").textContent = Money.format(bank.cashIn);
document.getElementById("bankChipsInPlay").textContent = Money.format(bank.chipsInPlay);
+ // An abandoned pot the dealer confirmed throwing away is money that left the
+ // table on purpose. It gets its own line so the two columns still add up in
+ // front of them, rather than looking like a shortfall they have to explain.
+ const discardedRow = document.getElementById("bankDiscardedRow");
+ if (discardedRow) {
+ discardedRow.hidden = !(bank.discarded > 0);
+ if (bank.discarded > 0) {
+ document.getElementById("bankDiscarded").textContent = Money.format(bank.discarded);
+ }
+ }
+
const verdict = document.getElementById("bankReconcileVerdict");
if (bank.balanced) {
- verdict.textContent = "Balanced — the cash covers every chip on the table.";
+ verdict.textContent = bank.discarded > 0
+ ? `Balanced — the cash covers every chip on the table, once the ${Money.format(bank.discarded)} taken off it is counted.`
+ : "Balanced — the cash covers every chip on the table.";
} else if (bank.difference > 0) {
verdict.textContent =
`${Money.format(bank.difference)} more in chips than cash taken. Someone's rebuy probably wasn't recorded — the box will be short at cash-out.`;
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
index 4228ba4..72df7cd 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
@@ -51,6 +51,9 @@ const HoldemState = {
notes: "",
handNumber: 1,
handHistory: [],
+ // Chips taken off the table on purpose, so the bank check can tell them
+ // apart from chips that have gone missing.
+ discarded: 0,
smallBlind: 1,
bigBlind: 2,
defaultStack: null,
@@ -146,6 +149,7 @@ const HoldemState = {
if (!parsed || !Array.isArray(parsed.seats)) return this.failedLoad(raw);
if (typeof parsed.handNumber !== "number" || parsed.handNumber < 1) parsed.handNumber = 1;
if (!Array.isArray(parsed.handHistory)) parsed.handHistory = [];
+ if (typeof parsed.discarded !== "number" || !(parsed.discarded >= 0)) parsed.discarded = 0;
// Tables saved before live betting existed are missing its fields; fill
// them in rather than making the dealer reset a table mid-session.
@@ -590,10 +594,19 @@ const HoldemState = {
0
);
- const difference = Money.round(chipsInPlay - cashIn);
+ // Chips the dealer deliberately took off the table — an abandoned pot they
+ // were asked about and confirmed. They are gone, so they can't be counted as
+ // in play, but they aren't missing either: the box knows exactly where they
+ // went. Without this the check blamed the tool's own confirmed action on
+ // "someone has paid for chips they haven't been given", which sends a dealer
+ // hunting for a miscount that never happened.
+ const discarded = Math.max(0, Money.round(Number(state.discarded) || 0));
+
+ const difference = Money.round(chipsInPlay + discarded - cashIn);
return {
cashIn: Money.round(cashIn),
chipsInPlay: Money.round(chipsInPlay),
+ discarded,
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/discardedPot.test.js b/tests/holdem/discardedPot.test.js
new file mode 100644
index 0000000..425b52e
--- /dev/null
+++ b/tests/holdem/discardedPot.test.js
@@ -0,0 +1,94 @@
+// Chips the dealer took off the table on purpose.
+//
+// Starting the next hand with money still in the middle destroys it — the tool asks
+// first and says so afterwards. What it didn't do was remember, so the bank check saw
+// only that chips were gone and reported them as missing.
+const test = require("node:test");
+const assert = require("node:assert");
+const { HoldemState, Money, makeTable } = require("./load");
+
+const table = () => {
+ const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 200 });
+ state.seats.slice(0, 4).forEach(player => { player.buyIns = 200; });
+ return state;
+};
+
+test("a table with nothing thrown away balances as before", () => {
+ const bank = HoldemState.bankReconciliation(table());
+
+ assert.equal(bank.balanced, true);
+ assert.equal(bank.discarded, 0);
+});
+
+test("chips that went missing are still reported", () => {
+ // The check exists for this: cash in the box with no chips to match it.
+ const state = table();
+ state.seats[0].stack = 150;
+
+ const bank = HoldemState.bankReconciliation(state);
+ assert.equal(bank.balanced, false);
+ assert.equal(bank.difference, -50);
+});
+
+test("an abandoned pot is accounted for rather than counted as missing", () => {
+ const state = table();
+ state.seats[1].stack = 199;
+ state.seats[2].stack = 198;
+ state.seats[3].stack = 198;
+ state.discarded = 5;
+
+ const bank = HoldemState.bankReconciliation(state);
+ assert.equal(bank.chipsInPlay, 795);
+ assert.equal(bank.discarded, 5);
+ assert.equal(bank.balanced, true, "the box knows exactly where that $5 went");
+});
+
+test("throwing a pot away doesn't hide a real shortfall underneath it", () => {
+ const state = table();
+ state.seats[1].stack = 199;
+ state.seats[2].stack = 198;
+ state.seats[3].stack = 148; // $50 genuinely unaccounted for as well
+ state.discarded = 5;
+
+ const bank = HoldemState.bankReconciliation(state);
+ assert.equal(bank.balanced, false);
+ assert.equal(bank.difference, -50);
+});
+
+test("a nonsense discarded value can't move the answer", () => {
+ const state = table();
+ state.discarded = -100;
+ assert.equal(HoldemState.bankReconciliation(state).discarded, 0);
+
+ state.discarded = "not a number";
+ assert.equal(HoldemState.bankReconciliation(state).discarded, 0);
+});
+
+test("a table saved before this existed reads as nothing thrown away", () => {
+ const saved = JSON.stringify(table());
+ const parsed = JSON.parse(saved);
+ delete parsed.discarded;
+
+ const store = new Map([[HoldemState.STORAGE_KEY, JSON.stringify(parsed)]]);
+ globalThis.localStorage = {
+ getItem: key => (store.has(key) ? store.get(key) : null),
+ setItem: (key, value) => store.set(key, value),
+ removeItem: key => store.delete(key),
+ };
+
+ const loaded = HoldemState.load();
+ assert.equal(loaded.discarded, 0);
+ assert.equal(HoldemState.bankReconciliation(loaded).balanced, true);
+ delete globalThis.localStorage;
+});
+
+test("the amounts still add up the way the panel shows them", () => {
+ const state = table();
+ state.seats[1].stack = 199;
+ state.seats[2].stack = 198;
+ state.seats[3].stack = 198;
+ state.discarded = 5;
+
+ const bank = HoldemState.bankReconciliation(state);
+ assert.equal(Money.round(bank.chipsInPlay + bank.discarded), bank.cashIn);
+});