Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60d192a9ec | ||
|
|
cc6a1902bc |
@@ -110,10 +110,23 @@
|
||||
<span>In</span>
|
||||
<span>Won</span>
|
||||
<span>Net</span>
|
||||
<span title="Chips in front of them, less the cash they handed over">Cash out</span>
|
||||
</div>
|
||||
<div id="standingsList"></div>
|
||||
</div>
|
||||
<p id="standingsEmptyHint" class="fieldHint">Once you resolve a pot, everyone's running total for the night shows up here.</p>
|
||||
<p id="standingsEmptyHint" class="fieldHint">Everyone who has bought in shows up here, with what they'd be owed if the night ended now.</p>
|
||||
|
||||
<div id="bankReconcile" class="bankReconcile" hidden>
|
||||
<div class="bankReconcileRow">
|
||||
<span>Cash taken</span>
|
||||
<span id="bankCashIn"></span>
|
||||
</div>
|
||||
<div class="bankReconcileRow">
|
||||
<span>Chips in play</span>
|
||||
<span id="bankChipsInPlay"></span>
|
||||
</div>
|
||||
<p id="bankReconcileVerdict" class="bankReconcileVerdict"></p>
|
||||
</div>
|
||||
|
||||
<h3 class="panelSubhead">Hands</h3>
|
||||
<div id="handHistoryList"></div>
|
||||
@@ -225,6 +238,7 @@
|
||||
<span class="standingsIn"></span>
|
||||
<span class="standingsWon"></span>
|
||||
<span class="standingsNet"></span>
|
||||
<span class="standingsCashOut"></span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1467,7 +1467,7 @@ button,
|
||||
|
||||
.standingsRow {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto auto auto;
|
||||
gap: 0.5rem;
|
||||
align-items: baseline;
|
||||
padding: 0.3rem 0.1rem;
|
||||
@@ -1481,6 +1481,48 @@ button,
|
||||
min-width: 4.5rem;
|
||||
}
|
||||
|
||||
/* The settle-up column is the one a banker reads while counting out cash, so it
|
||||
carries the weight the per-hand columns don't. */
|
||||
.standingsCashOut {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ---------- Bank reconciliation ----------
|
||||
Cash taken versus chips on the table. Every chip is a promise to pay cash for it
|
||||
later, and if the chips outrun the box that promise can't be kept — which a dealer
|
||||
otherwise discovers when the last two players cash out and there's nothing left. */
|
||||
.bankReconcile {
|
||||
margin-top: 0.6rem;
|
||||
padding: 0.55rem 0.6rem;
|
||||
border: 1px solid #2f2f2f;
|
||||
border-radius: 8px;
|
||||
background: #151515;
|
||||
}
|
||||
|
||||
.bankReconcileRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
font-size: 0.78rem;
|
||||
color: #b8b8b8;
|
||||
font-variant-numeric: tabular-nums;
|
||||
padding: 0.12rem 0;
|
||||
}
|
||||
|
||||
.bankReconcileVerdict {
|
||||
margin: 0.4rem 0 0;
|
||||
font-size: 0.74rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.bankReconcileVerdict.bankBalanced {
|
||||
color: #7fc99a;
|
||||
}
|
||||
|
||||
.bankReconcileVerdict.bankOff {
|
||||
color: #f0b46a;
|
||||
}
|
||||
|
||||
.standingsHeader {
|
||||
color: #8a8a8a;
|
||||
font-size: 0.7rem;
|
||||
|
||||
@@ -318,15 +318,34 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
Render.renderAll();
|
||||
});
|
||||
|
||||
document.getElementById("nextHandBtn").addEventListener("click", () => {
|
||||
document.getElementById("nextHandBtn").addEventListener("click", async () => {
|
||||
// Moving on wipes every seat's contribution, so an unresolved pot is simply
|
||||
// destroyed — measured at a four-handed table, pressing this with $8 in the
|
||||
// middle took the table from 800 chips to 792, and the toast said nothing
|
||||
// about it. Chips only ever leave this tool because a dealer meant them to.
|
||||
const unresolved = HoldemState.unresolvedContributions(state);
|
||||
|
||||
if (unresolved > Betting.EPSILON_CHIPS) {
|
||||
const ok = await Dialog.confirm({
|
||||
title: `Throw away ${Money.format(unresolved)}?`,
|
||||
message: "There's still money in the middle from this hand. Starting the next one takes it off the table without paying anybody — use Resolve Pot if somebody won it.",
|
||||
confirmLabel: "Throw it away",
|
||||
danger: true,
|
||||
});
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
// startNextHand moves the button on by one; say where it landed so the
|
||||
// dealer can confirm the rotation without hunting for the badge.
|
||||
const nextDealer = HoldemState.nextSeatIndex(state, state.dealerSeatIndex, 1);
|
||||
const where = state.dedicatedDealer
|
||||
? `small blind on seat ${HoldemState.nextSeatIndex(state, nextDealer, 1) + 1}`
|
||||
: `button on seat ${nextDealer + 1}`;
|
||||
const discarded = unresolved > Betting.EPSILON_CHIPS
|
||||
? ` ${Money.format(unresolved)} taken off the table.`
|
||||
: "";
|
||||
|
||||
runUndoable(`Next hand — ${where}. Blinds posted.`, () => {
|
||||
runUndoable(`Next hand — ${where}. Blinds posted.${discarded}`, () => {
|
||||
HoldemState.startNextHand(state);
|
||||
Betting.beginHand(state);
|
||||
});
|
||||
|
||||
@@ -272,11 +272,25 @@ const PotWizard = {
|
||||
});
|
||||
actions.appendChild(applyBtn);
|
||||
|
||||
const closeBtn = document.createElement("button");
|
||||
closeBtn.className = "holdemBtn holdemBtnGhost";
|
||||
closeBtn.textContent = "Close";
|
||||
closeBtn.addEventListener("click", () => this.close());
|
||||
actions.appendChild(closeBtn);
|
||||
// This button cancels — it does not pay anybody. Under a heading reading
|
||||
// "Final Payouts" with an amount beside a name, "Close" reads like dismissing
|
||||
// something already done, and a dealer who taps it walks away believing the
|
||||
// pot has been pushed. It hasn't: the chips are still in the middle and the
|
||||
// hand isn't logged. So it says what it does.
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.className = "holdemBtn holdemBtnGhost";
|
||||
cancelBtn.textContent = "Cancel — don't pay yet";
|
||||
cancelBtn.addEventListener("click", () => this.close());
|
||||
actions.appendChild(cancelBtn);
|
||||
|
||||
if (this.totals.size > 0) {
|
||||
const note = document.createElement("p");
|
||||
note.className = "fieldHint potWizardPendingNote";
|
||||
note.textContent = "Nothing has been paid out yet. Start Next Hand pushes the pot and logs the hand.";
|
||||
wrap.appendChild(actions);
|
||||
wrap.appendChild(note);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
wrap.appendChild(actions);
|
||||
return wrap;
|
||||
|
||||
@@ -701,8 +701,46 @@ const Render = {
|
||||
netEl.classList.toggle("netUp", row.net > 0);
|
||||
netEl.classList.toggle("netDown", row.net < 0);
|
||||
|
||||
// What they'd actually be handed if the night ended now: chips in front of
|
||||
// them less the cash they gave the banker. This was computed and written to
|
||||
// the CSV all along, but never shown to the person doing the paying out.
|
||||
const cashEl = node.querySelector(".standingsCashOut");
|
||||
cashEl.textContent = Money.formatSigned(row.cashPosition);
|
||||
cashEl.classList.toggle("netUp", row.cashPosition > 0);
|
||||
cashEl.classList.toggle("netDown", row.cashPosition < 0);
|
||||
|
||||
list.appendChild(node);
|
||||
});
|
||||
|
||||
this.renderBankReconciliation();
|
||||
},
|
||||
|
||||
// Does the cash box cover the chips on the table? Nothing asked this before, and
|
||||
// it's the question that decides whether the last two players can be paid.
|
||||
renderBankReconciliation() {
|
||||
const host = document.getElementById("bankReconcile");
|
||||
if (!host) return;
|
||||
|
||||
const bank = HoldemState.bankReconciliation(this.state);
|
||||
// Nothing to reconcile before anybody has handed over money or taken a chip.
|
||||
host.hidden = bank.cashIn === 0 && bank.chipsInPlay === 0;
|
||||
if (host.hidden) return;
|
||||
|
||||
document.getElementById("bankCashIn").textContent = Money.format(bank.cashIn);
|
||||
document.getElementById("bankChipsInPlay").textContent = Money.format(bank.chipsInPlay);
|
||||
|
||||
const verdict = document.getElementById("bankReconcileVerdict");
|
||||
if (bank.balanced) {
|
||||
verdict.textContent = "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.`;
|
||||
} else {
|
||||
verdict.textContent =
|
||||
`${Money.format(Math.abs(bank.difference))} more cash than chips in play. Someone has paid for chips they haven't been given.`;
|
||||
}
|
||||
verdict.classList.toggle("bankBalanced", bank.balanced);
|
||||
verdict.classList.toggle("bankOff", !bank.balanced);
|
||||
},
|
||||
|
||||
// ---- Hand counter + history ----
|
||||
|
||||
@@ -167,6 +167,38 @@ const HoldemState = {
|
||||
return !player.inHand;
|
||||
},
|
||||
|
||||
// What starting the next hand would take off the table for good: exactly the
|
||||
// fields startNextHand clears.
|
||||
//
|
||||
// Deliberately not totalPot(). The table-wide base pot is a standing ante — a
|
||||
// stand-in for "everyone puts the same in every hand", used by a dealer who
|
||||
// isn't tracking stacks — so it re-applies to the next hand rather than being
|
||||
// lost with this one. Counting it would mean warning about money that isn't
|
||||
// going anywhere, on every single hand, for exactly the dealers who chose the
|
||||
// simplest way to run the table.
|
||||
unresolvedContributions(state) {
|
||||
const total = state.seats.reduce((sum, player) => {
|
||||
const override = player.amountOverride;
|
||||
let banked = 0;
|
||||
|
||||
if (override !== null && override !== undefined && String(override).trim() !== "") {
|
||||
const parsed = Number(override);
|
||||
if (!Number.isNaN(parsed)) banked = Math.max(0, parsed);
|
||||
}
|
||||
|
||||
if (banked === 0) {
|
||||
banked = (state.chipLegend || []).reduce((chips, chip) => {
|
||||
const count = Number(player.chipCounts[chip.id]) || 0;
|
||||
return chips + count * Number(chip.value || 0);
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return sum + banked + Math.max(0, Number(player.committed) || 0);
|
||||
}, 0);
|
||||
|
||||
return Money.round(total);
|
||||
},
|
||||
|
||||
totalPot(state) {
|
||||
return state.seats.reduce(
|
||||
(sum, p) => sum + this.computeContribution(p, state.chipLegend, state.basePotAmount),
|
||||
@@ -352,6 +384,23 @@ const HoldemState = {
|
||||
// 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]));
|
||||
|
||||
// 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
|
||||
// left a player who bought in and then sat through a few hands folding —
|
||||
// or who just sat down — missing entirely, so the banker's own record didn't
|
||||
// mention the cash they'd taken. They turn up at cash-out holding chips the
|
||||
// list never accounted for.
|
||||
state.seats.forEach((player, index) => {
|
||||
if (totals.has(player.id)) return;
|
||||
const hasMoneyInPlay =
|
||||
(Number(player.buyIns) || 0) > 0 ||
|
||||
(Number(player.stack) || 0) > 0 ||
|
||||
(Number(player.amountOverride) || 0) > 0 ||
|
||||
(Number(player.committed) || 0) > 0;
|
||||
if (!hasMoneyInPlay) return;
|
||||
totals.set(player.id, { id: player.id, name: this.displayName(player, index), in: 0, won: 0 });
|
||||
});
|
||||
|
||||
return Array.from(totals.values())
|
||||
.map(t => {
|
||||
const seat = seatById.get(t.id);
|
||||
@@ -376,6 +425,37 @@ const HoldemState = {
|
||||
})
|
||||
.sort((a, b) => b.net - a.net);
|
||||
},
|
||||
|
||||
// The one check a banker actually needs, and the one nothing here did.
|
||||
//
|
||||
// Every chip on the table is a promise to pay cash for it later. If the chips in
|
||||
// play add up to more than the cash that came in, that promise can't be kept —
|
||||
// and the way you find out is the last two players cashing out and the box being
|
||||
// empty. It's always someone taking chips without paying, a rebuy that never got
|
||||
// recorded, or a miscount, and it's silent until it's too late to work out which.
|
||||
//
|
||||
// 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.
|
||||
bankReconciliation(state) {
|
||||
const cashIn = state.seats.reduce((sum, p) => sum + Math.max(0, Number(p.buyIns) || 0), 0);
|
||||
const chipsInPlay = state.seats.reduce(
|
||||
(sum, p) =>
|
||||
sum +
|
||||
Math.max(0, Number(p.stack) || 0) +
|
||||
Math.max(0, Number(p.committed) || 0) +
|
||||
Math.max(0, Number(p.amountOverride) || 0),
|
||||
0
|
||||
);
|
||||
|
||||
const difference = Money.round(chipsInPlay - cashIn);
|
||||
return {
|
||||
cashIn: Money.round(cashIn),
|
||||
chipsInPlay: Money.round(chipsInPlay),
|
||||
difference,
|
||||
// Half a cent of float dust isn't a discrepancy worth alarming anyone over.
|
||||
balanced: Math.abs(difference) <= Money.EPSILON,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
// The banker's job, which is a different job from the dealer's: every chip on the
|
||||
// table is a promise to pay cash for it later, and somebody has to be able to check
|
||||
// that the promise can be kept before the last two players try to cash out.
|
||||
const { test, describe } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { Betting, HoldemState, Money, makeTable } = require("./load");
|
||||
|
||||
const seat = (state, index, name, cash, chips) => {
|
||||
const p = state.seats[index];
|
||||
p.name = name; p.inHand = true; p.buyIns = cash; p.stack = chips;
|
||||
return p;
|
||||
};
|
||||
|
||||
describe("everyone with money in the game is accounted for", () => {
|
||||
test("a player who has bought in but played no logged hand still appears", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
seat(state, 1, "Ben", 100, 100);
|
||||
seat(state, 2, "Cal", 200, 200); // just sat down
|
||||
|
||||
state.seats[0].amountOverride = 10;
|
||||
state.seats[1].amountOverride = 10;
|
||||
HoldemState.recordHand(state, [{ id: state.seats[0].id, label: "Ann", amount: 20 }]);
|
||||
state.seats[0].amountOverride = null;
|
||||
state.seats[1].amountOverride = null;
|
||||
|
||||
const names = HoldemState.sessionStandings(state).map(r => r.name);
|
||||
assert.ok(names.includes("Cal"), `Cal handed over $200 and is missing: ${names.join(", ")}`);
|
||||
});
|
||||
|
||||
test("their row reads as no hands played, not as a loss", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
seat(state, 1, "Ben", 200, 200);
|
||||
|
||||
const cal = HoldemState.sessionStandings(state).find(r => r.name === "Ben");
|
||||
assert.equal(cal.in, 0);
|
||||
assert.equal(cal.won, 0);
|
||||
assert.equal(cal.net, 0);
|
||||
assert.equal(cal.boughtIn, 200);
|
||||
assert.equal(cal.cashPosition, 0, "level: they've bought chips and still hold them");
|
||||
});
|
||||
|
||||
test("an empty chair is not dragged into the settle-up", () => {
|
||||
const state = makeTable({ seatCount: 9, seated: [0], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
const rows = HoldemState.sessionStandings(state);
|
||||
assert.equal(rows.length, 1);
|
||||
});
|
||||
|
||||
test("somebody who has cashed out completely still shows what they took", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 0); // handed every chip back
|
||||
seat(state, 1, "Ben", 100, 200);
|
||||
const ann = HoldemState.sessionStandings(state).find(r => r.name === "Ann");
|
||||
assert.equal(ann.cashPosition, -100, "down the whole buy-in");
|
||||
});
|
||||
});
|
||||
|
||||
describe("does the cash box cover the chips on the table", () => {
|
||||
test("a straight game balances", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 60);
|
||||
seat(state, 1, "Ben", 100, 140);
|
||||
seat(state, 2, "Cal", 100, 100);
|
||||
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.cashIn, 300);
|
||||
assert.equal(bank.chipsInPlay, 300);
|
||||
assert.equal(bank.difference, 0);
|
||||
assert.equal(bank.balanced, true);
|
||||
});
|
||||
|
||||
test("chips outrunning the cash box is caught before cash-out", () => {
|
||||
// The 2am failure: the last players out can't be paid.
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
seat(state, 1, "Ben", 100, 100);
|
||||
seat(state, 2, "Cal", 100, 260); // took chips for a rebuy nobody wrote down
|
||||
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.cashIn, 300);
|
||||
assert.equal(bank.chipsInPlay, 460);
|
||||
assert.equal(bank.difference, 160);
|
||||
assert.equal(bank.balanced, false);
|
||||
});
|
||||
|
||||
test("cash taken for chips never handed over is caught too", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 200, 100); // paid 200, only got 100 in chips
|
||||
seat(state, 1, "Ben", 100, 100);
|
||||
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.difference, -100);
|
||||
assert.equal(bank.balanced, false);
|
||||
});
|
||||
|
||||
test("chips in the middle still count as chips in play", () => {
|
||||
// Money in the pot is somebody's; it just hasn't been won yet. Leaving it out
|
||||
// would report a shortfall every time a hand was open — which is nearly always.
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||
state.seats.forEach(p => { if (p.inHand) p.buyIns = 100; });
|
||||
|
||||
const before = HoldemState.bankReconciliation(state);
|
||||
assert.equal(before.balanced, true);
|
||||
|
||||
Betting.beginHand(state); // blinds go in
|
||||
Betting.raiseTo(state, 0, 25); // and a raise
|
||||
Betting.sweepToHandTotals(state); // banked into the pot
|
||||
|
||||
const during = HoldemState.bankReconciliation(state);
|
||||
assert.equal(during.balanced, true, "an open pot is not a discrepancy");
|
||||
assert.equal(during.chipsInPlay, before.chipsInPlay);
|
||||
});
|
||||
|
||||
test("a whole session of play never knocks the books out", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
state.seats.forEach(p => { if (p.inHand) p.buyIns = 100; });
|
||||
|
||||
for (let hand = 0; hand < 6; hand++) {
|
||||
Betting.beginHand(state);
|
||||
Betting.liveSeats(state).forEach(s => {
|
||||
if (Betting.toCall(state, s.player) > 0) Betting.call(state, s.index);
|
||||
});
|
||||
Betting.sweepToHandTotals(state);
|
||||
|
||||
const pot = HoldemState.totalPot(state);
|
||||
const winner = Betting.liveSeats(state)[0];
|
||||
const payouts = [{ id: winner.player.id, label: "w", amount: pot }];
|
||||
HoldemState.recordHand(state, payouts);
|
||||
Betting.creditWinnings(state, payouts);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true, `books off after hand ${hand + 1}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("a rebuy keeps the books straight, a chip grab doesn't", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 100, 100);
|
||||
seat(state, 1, "Ben", 100, 100);
|
||||
|
||||
// A proper rebuy: cash to the banker, chips to the player.
|
||||
state.seats[0].buyIns += 100;
|
||||
state.seats[0].stack += 100;
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
|
||||
// Chips taken without paying.
|
||||
state.seats[1].stack += 50;
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.balanced, false);
|
||||
assert.equal(bank.difference, 50);
|
||||
});
|
||||
|
||||
test("nothing to reconcile before anybody has bought in", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [], stack: 0 });
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.cashIn, 0);
|
||||
assert.equal(bank.chipsInPlay, 0);
|
||||
assert.equal(bank.balanced, true);
|
||||
});
|
||||
|
||||
test("float dust is not a discrepancy", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1], stack: 0 });
|
||||
seat(state, 0, "Ann", 0.1 + 0.2, 0.3);
|
||||
seat(state, 1, "Ben", 10, 10);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
// Chips only ever leave this tool because a dealer meant them to. Starting the next
|
||||
// hand clears every seat's contribution, so an unresolved pot is simply destroyed —
|
||||
// the UI now asks first, and the number it quotes has to be exactly what goes.
|
||||
const { test, describe } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
const { Betting, HoldemState, Money, makeTable, chipsOnTable } = require("./load");
|
||||
|
||||
// The same measure the confirmation uses.
|
||||
const unresolvedPot = state => HoldemState.unresolvedContributions(state);
|
||||
|
||||
describe("what starting the next hand destroys", () => {
|
||||
test("the quoted amount is exactly what leaves the table", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.call(state, 3); Betting.call(state, 0); Betting.call(state, 1); Betting.check(state, 2);
|
||||
Betting.sweepToHandTotals(state);
|
||||
|
||||
const before = chipsOnTable(state);
|
||||
const quoted = unresolvedPot(state);
|
||||
assert.ok(quoted > 0, "there is a pot to warn about");
|
||||
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(
|
||||
Money.toCents(chipsOnTable(state)),
|
||||
Money.toCents(before - quoted),
|
||||
"the dealer must be told the real number, not an approximation"
|
||||
);
|
||||
});
|
||||
|
||||
test("chips still in front of players on the current street are counted too", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state); // blinds are `committed`, not yet banked
|
||||
Betting.raiseTo(state, 0, 25); // and so is a raise mid-street
|
||||
|
||||
const before = chipsOnTable(state);
|
||||
const quoted = unresolvedPot(state);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(Money.toCents(chipsOnTable(state)), Money.toCents(before - quoted));
|
||||
assert.ok(quoted >= 25, `a live raise has to be included; quoted ${quoted}`);
|
||||
});
|
||||
|
||||
test("a hand that was paid out properly leaves nothing to warn about", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.call(state, 0); Betting.call(state, 1); Betting.check(state, 2);
|
||||
Betting.sweepToHandTotals(state);
|
||||
|
||||
const pot = HoldemState.totalPot(state);
|
||||
const payouts = [{ id: state.seats[0].id, label: "Ann", amount: pot }];
|
||||
HoldemState.recordHand(state, payouts);
|
||||
Betting.creditWinnings(state, payouts);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(unresolvedPot(state), 0, "nothing left over after a settled hand");
|
||||
});
|
||||
|
||||
test("settling then dealing on conserves every chip", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
const start = chipsOnTable(state);
|
||||
|
||||
for (let hand = 0; hand < 5; hand++) {
|
||||
Betting.beginHand(state);
|
||||
const live = Betting.liveSeats(state);
|
||||
live.forEach(s => { if (Betting.toCall(state, s.player) > 0) Betting.call(state, s.index); });
|
||||
Betting.sweepToHandTotals(state);
|
||||
|
||||
const pot = HoldemState.totalPot(state);
|
||||
const winner = Betting.liveSeats(state)[0];
|
||||
const payouts = [{ id: winner.player.id, label: "w", amount: pot }];
|
||||
HoldemState.recordHand(state, payouts);
|
||||
Betting.creditWinnings(state, payouts);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(chipsOnTable(state), start, `chips drifted on hand ${hand + 1}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("an empty table has no phantom pot to warn about", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [] });
|
||||
assert.equal(unresolvedPot(state), 0);
|
||||
});
|
||||
|
||||
test("a standing ante is never warned about, because it isn't lost", () => {
|
||||
// The table default is a stand-in for "everyone puts the same in every hand",
|
||||
// used by a dealer not tracking stacks at all. It re-applies to the next hand
|
||||
// rather than going anywhere, so counting it would nag this dealer before
|
||||
// every single hand — which is why the warning measures the fields
|
||||
// startNextHand actually clears rather than the pot badge.
|
||||
const state = makeTable({ seatCount: 9, seated: [0, 1] });
|
||||
state.basePotAmount = 25;
|
||||
state.seats.forEach(p => { p.amountOverride = null; });
|
||||
|
||||
// The base pot carries into the next hand, so nothing is being thrown away
|
||||
// and the dealer must not be asked to confirm anything.
|
||||
assert.equal(HoldemState.totalPot(state), 50, "the badge still shows a pot");
|
||||
assert.equal(unresolvedPot(state), 0, "but none of it is at risk, so no warning");
|
||||
|
||||
HoldemState.startNextHand(state);
|
||||
assert.equal(HoldemState.totalPot(state), 50, "the standing ante applies to the new hand too");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user