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 +

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); +});