Hold'em dealer: 55 polish rounds, one merge #101
@@ -127,6 +127,10 @@
|
||||
<span>Chips in play</span>
|
||||
<span id="bankChipsInPlay"></span>
|
||||
</div>
|
||||
<div class="bankReconcileRow" id="bankDiscardedRow" hidden>
|
||||
<span>Taken off the table</span>
|
||||
<span id="bankDiscarded"></span>
|
||||
</div>
|
||||
<p id="bankReconcileVerdict" class="bankReconcileVerdict"></p>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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.`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user