Add the starting-stack accounting round to the release
This commit is contained in:
@@ -176,6 +176,7 @@
|
||||
<span id="bankPaidOut"></span>
|
||||
</div>
|
||||
<p id="bankReconcileVerdict" class="bankReconcileVerdict"></p>
|
||||
<button type="button" id="bankRecordBuyInsBtn" class="holdemBtn holdemBtnSmall" hidden></button>
|
||||
</div>
|
||||
|
||||
<div id="settleUp" hidden>
|
||||
|
||||
@@ -432,6 +432,24 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
// A misdeal. Everything in the middle goes back to whoever put it there, and the
|
||||
// same button deals the same hand again.
|
||||
// The seats holding the table's starting stack, written down as cash taken.
|
||||
document.getElementById("bankRecordBuyInsBtn").addEventListener("click", async () => {
|
||||
const starting = HoldemState.seatsOwingCash(state).filter(row => row.fromStartingStack);
|
||||
if (starting.length === 0) return;
|
||||
const total = Money.round(starting.reduce((sum, row) => sum + row.chips, 0));
|
||||
|
||||
const ok = await Dialog.confirm({
|
||||
title: `Write down ${Money.format(total)} taken?`,
|
||||
message: `${starting.map(row => `${row.name} ${Money.format(row.chips)}`).join(", ")} — the starting stack ` +
|
||||
`each of them is playing with. Only do this if the cash is actually in the box.`,
|
||||
confirmLabel: "Write it down",
|
||||
});
|
||||
if (!ok) return;
|
||||
|
||||
runUndoable(`${Money.format(total)} recorded from ${starting.length} ${starting.length === 1 ? "player" : "players"}.`,
|
||||
() => HoldemState.recordStartingBuyIns(state));
|
||||
});
|
||||
|
||||
document.getElementById("waitingListForm").addEventListener("submit", e => {
|
||||
e.preventDefault();
|
||||
const field = document.getElementById("waitingNameInput");
|
||||
|
||||
@@ -1264,14 +1264,43 @@ const Render = {
|
||||
? `Balanced — the cash covers every chip on the table, once the ${accountedFor.join(" and the ")} 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.`;
|
||||
// Name them. "Somebody's rebuy wasn't recorded" is true and useless; the
|
||||
// dealer's next move is to go to a particular seat, and the tool knows
|
||||
// which. The commonest cause by far is the table's own starting stack:
|
||||
// seats holding chips that were never bought, because "everyone starts
|
||||
// with 200" is how a night opens.
|
||||
const owing = HoldemState.seatsOwingCash(this.state);
|
||||
const names = owing.map(row => row.name);
|
||||
const who = names.length === 1
|
||||
? names[0]
|
||||
: `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]}`;
|
||||
verdict.textContent = owing.length > 0
|
||||
? `${Money.format(bank.difference)} more in chips than cash taken. ${who} ` +
|
||||
`${owing.length === 1 ? "is" : "are"} holding chips with no buy-in written down.`
|
||||
: `${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);
|
||||
|
||||
// One tap for the seats that are only holding the table's starting stack —
|
||||
// which is the dealer's own instruction, said once in the setup panel, rather
|
||||
// than the tool inventing cash. Anybody else's unrecorded rebuy is named in the
|
||||
// sentence above and left to them, because only they know what it was.
|
||||
const record = document.getElementById("bankRecordBuyInsBtn");
|
||||
if (record) {
|
||||
const starting = HoldemState.seatsOwingCash(this.state).filter(row => row.fromStartingStack);
|
||||
const total = Money.round(starting.reduce((sum, row) => sum + row.chips, 0));
|
||||
record.hidden = starting.length === 0;
|
||||
if (starting.length > 0) {
|
||||
record.textContent = `Take ${Money.format(total)} from ${starting.length} ${starting.length === 1 ? "player" : "players"}`;
|
||||
record.title = starting.length === 1
|
||||
? `Write down ${Money.format(total)} taken from ${starting[0].name}`
|
||||
: `Write down the starting stack as cash taken from ${starting.map(row => row.name).join(", ")}`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ---- Hand counter + history ----
|
||||
|
||||
@@ -803,7 +803,7 @@ const HoldemState = {
|
||||
if (totals.has(player.id)) return;
|
||||
const hasMoneyInPlay =
|
||||
(Number(player.buyIns) || 0) > 0 ||
|
||||
(Number(player.stack) || 0) > 0 ||
|
||||
this.chipsHeldBy(state, player) > 0 ||
|
||||
(Number(player.amountOverride) || 0) > 0 ||
|
||||
(Number(player.committed) || 0) > 0;
|
||||
if (!hasMoneyInPlay) return;
|
||||
@@ -821,7 +821,7 @@ const HoldemState = {
|
||||
// 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)
|
||||
? this.chipsHeldBy(state, seat)
|
||||
: (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
|
||||
@@ -1081,6 +1081,71 @@ const HoldemState = {
|
||||
};
|
||||
},
|
||||
|
||||
// Is anybody actually in this chair? A seat with a name on it, chips in front of
|
||||
// it, cash against it or a hand dealt to it. Empty chairs are not players, which
|
||||
// matters the moment the table's starting stack starts standing in for a seat that
|
||||
// has never been given one — every empty chair would otherwise look like somebody
|
||||
// holding two hundred dollars.
|
||||
seatInPlay(player) {
|
||||
if (!player) return false;
|
||||
return !!(player.name || "").trim() ||
|
||||
(Number(player.buyIns) || 0) > 0 ||
|
||||
(Number(player.stack) || 0) > 0 ||
|
||||
(Number(player.committed) || 0) > 0 ||
|
||||
(Number(player.amountOverride) || 0) > 0 ||
|
||||
player.inHand === true;
|
||||
},
|
||||
|
||||
// What a seat is holding, the way the felt and the betting see it — which includes
|
||||
// the table's starting stack standing in for a seat that has never been given one.
|
||||
//
|
||||
// This is the number the money side of the tool was missing. "Starting stack" is
|
||||
// offered in the setup panel as the ordinary way to open a table, and everything a
|
||||
// dealer looks at — the puck, the bar, what a player can bet — reads it. The bank
|
||||
// check and the standings read `seat.stack` directly instead, which is null for
|
||||
// exactly those seats: measured on a three-handed table with a $200 starting stack
|
||||
// and nobody's own stack set, the felt played for $600 while the bank check said
|
||||
// "no chips in play, balanced" and the standings listed nobody at all.
|
||||
chipsHeldBy(state, player) {
|
||||
if (!this.seatInPlay(player)) return 0;
|
||||
return Math.max(0, Money.round(Betting.stackOf(state, player)));
|
||||
},
|
||||
|
||||
// Seats playing with chips the box has no cash against.
|
||||
//
|
||||
// Two kinds, and they are not the same problem. A seat holding the table's starting
|
||||
// stack has never been given chips of its own — the dealer set "everyone starts
|
||||
// with 200" and sat people down, which is the ordinary way to open a table and the
|
||||
// reason this whole gap existed. A seat with its own stack and no buy-in is a
|
||||
// rebuy somebody forgot to write down, and only the dealer knows what it was.
|
||||
seatsOwingCash(state) {
|
||||
return state.seats
|
||||
.map((player, index) => ({ player, index }))
|
||||
.filter(({ player }) => this.chipsHeldBy(state, player) > 0 &&
|
||||
!((Number(player.buyIns) || 0) > 0))
|
||||
.map(({ player, index }) => ({
|
||||
index,
|
||||
name: this.displayName(player, index),
|
||||
chips: this.chipsHeldBy(state, player),
|
||||
// True when they are holding the table's starting stack rather than one
|
||||
// the dealer typed for them.
|
||||
fromStartingStack: player.stack === null || player.stack === undefined ||
|
||||
String(player.stack).trim() === "",
|
||||
}));
|
||||
},
|
||||
|
||||
// Write down the cash for the seats that are holding the table's starting stack.
|
||||
// Only those: what somebody else's unrecorded rebuy was is a question only the
|
||||
// dealer can answer, and guessing it would be the tool inventing money.
|
||||
recordStartingBuyIns(state) {
|
||||
const owed = this.seatsOwingCash(state).filter(row => row.fromStartingStack);
|
||||
owed.forEach(row => { state.seats[row.index].buyIns = row.chips; });
|
||||
return {
|
||||
seats: owed.length,
|
||||
total: Money.round(owed.reduce((sum, row) => sum + row.chips, 0)),
|
||||
};
|
||||
},
|
||||
|
||||
bankReconciliation(state) {
|
||||
const gone = Array.isArray(state.cashedOut) ? state.cashedOut : [];
|
||||
// Cash the box took from people who have since left counts as taken: it is in
|
||||
@@ -1091,7 +1156,7 @@ const HoldemState = {
|
||||
const chipsInPlay = state.seats.reduce(
|
||||
(sum, p) =>
|
||||
sum +
|
||||
Math.max(0, Number(p.stack) || 0) +
|
||||
this.chipsHeldBy(state, p) +
|
||||
Math.max(0, Number(p.committed) || 0) +
|
||||
Math.max(0, Number(p.amountOverride) || 0),
|
||||
0
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
// The chips the felt plays with are the chips the box is checked against.
|
||||
//
|
||||
// "Starting stack" is offered in the setup panel as the ordinary way to open a table —
|
||||
// "seats without their own stack start here" — and everything a dealer looks at reads
|
||||
// it: the puck, the action bar, what a player is allowed to bet. The money side did
|
||||
// not. It read `seat.stack` directly, which is null for exactly those seats, so a
|
||||
// three-handed table playing for $600 reported no chips in play, a balanced bank and a
|
||||
// standings list with nobody on it.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Money, HoldemState, Betting } = require("./load");
|
||||
|
||||
const openedWithStartingStack = (stack = 200, names = ["Ann", "Ben", "Cal"]) => {
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 6);
|
||||
state.defaultStack = stack;
|
||||
names.forEach((name, i) => {
|
||||
state.seats[i].name = name;
|
||||
state.seats[i].inHand = true;
|
||||
});
|
||||
return state;
|
||||
};
|
||||
|
||||
describe("what the table is playing for", () => {
|
||||
test("is what the bank counts", () => {
|
||||
const state = openedWithStartingStack();
|
||||
// What the felt says each of them has.
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => Betting.stackOf(state, p)), [200, 200, 200]);
|
||||
assert.equal(HoldemState.bankReconciliation(state).chipsInPlay, 600);
|
||||
});
|
||||
|
||||
test("and the box being empty is a problem it now reports", () => {
|
||||
const bank = HoldemState.bankReconciliation(openedWithStartingStack());
|
||||
assert.equal(bank.cashIn, 0);
|
||||
assert.equal(bank.difference, 600);
|
||||
assert.equal(bank.balanced, false, "six hundred dollars of chips nobody paid for");
|
||||
});
|
||||
|
||||
test("empty chairs are not people holding two hundred dollars", () => {
|
||||
// The starting stack stands in for a seat that hasn't been given one, and every
|
||||
// empty chair is such a seat. Counting them would invent $600 of chips at an
|
||||
// empty table.
|
||||
const state = openedWithStartingStack();
|
||||
assert.equal(HoldemState.bankReconciliation(state).chipsInPlay, 600, "three players, not six chairs");
|
||||
|
||||
const empty = HoldemState.createDefault();
|
||||
empty.defaultStack = 200;
|
||||
assert.equal(HoldemState.bankReconciliation(empty).chipsInPlay, 0);
|
||||
assert.equal(HoldemState.bankReconciliation(empty).balanced, true);
|
||||
});
|
||||
|
||||
test("once the cash is written down it balances", () => {
|
||||
const state = openedWithStartingStack();
|
||||
state.seats.slice(0, 3).forEach(p => { p.buyIns = 200; });
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
|
||||
test("a table where everyone has their own stack is unchanged", () => {
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 4);
|
||||
["Ann", "Ben"].forEach((name, i) => {
|
||||
state.seats[i].name = name;
|
||||
state.seats[i].stack = 150;
|
||||
state.seats[i].buyIns = 150;
|
||||
});
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.chipsInPlay, 300);
|
||||
assert.equal(bank.balanced, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("who is on the settle-up list", () => {
|
||||
test("everyone holding chips, including the ones holding the table's own stack", () => {
|
||||
const rows = HoldemState.sessionStandings(openedWithStartingStack());
|
||||
assert.deepEqual(rows.map(r => r.name), ["Ann", "Ben", "Cal"]);
|
||||
assert.deepEqual(rows.map(r => r.chipsInFront), [200, 200, 200]);
|
||||
});
|
||||
|
||||
test("with nothing bought in yet, which is the thing to notice", () => {
|
||||
const rows = HoldemState.sessionStandings(openedWithStartingStack());
|
||||
assert.deepEqual(rows.map(r => r.boughtIn), [0, 0, 0]);
|
||||
assert.deepEqual(rows.map(r => r.cashPosition), [200, 200, 200]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("naming the seats the cash is missing for", () => {
|
||||
test("says who, and which kind of missing it is", () => {
|
||||
const state = openedWithStartingStack();
|
||||
state.seats[2].stack = 320; // Cal typed his own: an unrecorded rebuy
|
||||
|
||||
const owing = HoldemState.seatsOwingCash(state);
|
||||
assert.deepEqual(owing.map(row => row.name), ["Ann", "Ben", "Cal"]);
|
||||
assert.deepEqual(owing.map(row => row.fromStartingStack), [true, true, false]);
|
||||
assert.deepEqual(owing.map(row => row.chips), [200, 200, 320]);
|
||||
});
|
||||
|
||||
test("a seat whose buy-in is written down isn't on the list", () => {
|
||||
const state = openedWithStartingStack();
|
||||
state.seats[0].buyIns = 200;
|
||||
assert.deepEqual(HoldemState.seatsOwingCash(state).map(row => row.name), ["Ben", "Cal"]);
|
||||
});
|
||||
|
||||
test("and a table where nothing is owed has nobody on it", () => {
|
||||
const state = openedWithStartingStack();
|
||||
state.seats.slice(0, 3).forEach(p => { p.buyIns = 200; });
|
||||
assert.deepEqual(HoldemState.seatsOwingCash(state), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("writing the starting stacks down", () => {
|
||||
test("records exactly what those seats are holding", () => {
|
||||
const state = openedWithStartingStack();
|
||||
const done = HoldemState.recordStartingBuyIns(state);
|
||||
assert.equal(done.seats, 3);
|
||||
assert.equal(done.total, 600);
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.buyIns), [200, 200, 200]);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
|
||||
test("and leaves somebody else's forgotten rebuy alone", () => {
|
||||
// Only the dealer knows what that was. Guessing would be the tool inventing
|
||||
// money, which is the fault this whole round is about.
|
||||
const state = openedWithStartingStack();
|
||||
state.seats[2].stack = 320;
|
||||
HoldemState.recordStartingBuyIns(state);
|
||||
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.buyIns), [200, 200, 0]);
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.balanced, false);
|
||||
assert.equal(bank.difference, 320, "still exactly Cal's chips, and still reported");
|
||||
});
|
||||
|
||||
test("does nothing on a table that doesn't need it", () => {
|
||||
const state = openedWithStartingStack();
|
||||
state.seats.slice(0, 3).forEach(p => { p.buyIns = 200; });
|
||||
const done = HoldemState.recordStartingBuyIns(state);
|
||||
assert.equal(done.seats, 0);
|
||||
assert.equal(done.total, 0);
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.buyIns), [200, 200, 200]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("through a hand", () => {
|
||||
test("the money adds up from the deal to the payout", () => {
|
||||
const state = openedWithStartingStack(200, ["Ann", "Ben", "Cal", "Dee"]);
|
||||
HoldemState.recordStartingBuyIns(state);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
|
||||
Betting.beginHand(state);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true, "blinds are still on the table");
|
||||
|
||||
[3, 0, 1].forEach(() => { if (state.actingSeatIndex !== null) Betting.call(state, state.actingSeatIndex); });
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
|
||||
const pot = Betting.potSoFar(state);
|
||||
Betting.sweepToHandTotals(state);
|
||||
Betting.creditWinnings(state, [{ id: state.seats[0].id, label: "Ann", amount: pot }]);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
const bank = HoldemState.bankReconciliation(state);
|
||||
assert.equal(bank.balanced, true);
|
||||
assert.equal(bank.chipsInPlay, 800, "four buy-ins of two hundred, wherever they have moved to");
|
||||
assert.ok(state.seats[0].stack > 200, "and the winner is holding more than they sat down with");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user