From 1c2e71650836eddf5fffcc8cb90d338171edb067 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Wed, 2 Sep 2026 09:05:32 -0600 Subject: [PATCH] Ask the same two questions the same way everywhere MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The round before this found the bank check and the standings reading a seat's own stack where the felt reads the table's starting stack — two parts of the tool disagreeing about how much money was on the table. That was worth a sweep, and the sweep found more. The colour-up panel had the same fault. It is the panel that says "count out 2×$100 for this player", and on a table opened the ordinary way — a starting stack and some names — it showed nothing at all. The one table where every stack is a round number made of the largest chips there are was the table it had nothing to say about. The other question, "is anybody in this chair", existed in four spellings: in the seat count, in the seat menu twice, in the waiting list, and twice more in the state module. Three of them looked at name, chips and cash; one also looked at a hand being dealt. A chair that counts as occupied for one and empty for another is how somebody's chips get given away — measured, a seat marked in hand with no name and no stack yet was on offer to the next person in the waiting queue, on top of whoever was already playing it. Both questions now have one implementation and one name: "What is this seat holding?" HoldemState.chipsHeldBy "Is anybody in this chair?" HoldemState.seatInPlay 10 new tests, including one that pins the bank check, the standings and the settle-up plan to the same total, and one that pins the seat count and the open seats to the same set of empty chairs. The occupancy test was cut back to its old three-part form on purpose to confirm the waiting-list case fails. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- .../wwwroot/js/casino/holdem/main.js | 13 +- .../wwwroot/js/casino/holdem/render.js | 16 ++- .../wwwroot/js/casino/holdem/seatMenu.js | 5 +- .../wwwroot/js/casino/holdem/state.js | 5 +- tests/holdem/oneStackRule.test.js | 133 ++++++++++++++++++ 5 files changed, 154 insertions(+), 18 deletions(-) create mode 100644 tests/holdem/oneStackRule.test.js diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js index 59881ed..667a39a 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js @@ -69,7 +69,7 @@ document.addEventListener("DOMContentLoaded", () => { hooks.rerender(); return; } - const occupied = (seat.name || "").trim() || Number(seat.buyIns) > 0 || Number(seat.stack) > 0; + const occupied = HoldemState.seatInPlay(seat); if (occupied) { Toast.show(`${HoldemState.displayName(seat, index)} is already in that seat.`, { duration: 3500 }); hooks.rerender(); @@ -366,12 +366,11 @@ document.addEventListener("DOMContentLoaded", () => { // in progress. Shrinking the table throws these away permanently, and it also // orphans them in the session standings, where they keep their winnings but lose // the stack and buy-in the settle-up is worked out from. - const seatHasSomeoneIn = player => - !!(player.name && player.name.trim()) || - player.inHand || - (Number(player.stack) || 0) > 0 || - (Number(player.buyIns) || 0) > 0 || - (Number(player.amountOverride) || 0) > 0; + // One question, asked in one place. This predicate existed here, in the seat menu, + // in the action bar and twice in the state module, in four slightly different + // spellings — and a chair that counts as occupied for the waiting list but empty + // for the seat count is how somebody's chips get thrown away. + const seatHasSomeoneIn = player => HoldemState.seatInPlay(player); document.getElementById("seatCountInput").addEventListener("change", async e => { const requested = Math.max(2, Math.min(24, Math.floor(Number(e.target.value)) || state.seatCount)); diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js index 12eb7fb..25607fd 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js @@ -936,14 +936,20 @@ const Render = { const list = document.getElementById("colourUpList"); if (!host || !list) return; + // What each player is holding, asked the same way the felt asks it — which + // includes the table's starting stack standing in for a seat that has never + // been given one. Reading `player.stack` here meant that a table opened the + // ordinary way, with "everyone starts with 200", showed an empty colour-up + // panel: nobody to count out, at the table where every single stack is a + // round number made of the largest chips there are. const legend = this.state.chipLegend || []; const rows = this.state.seats - .map((player, index) => ({ player, index })) - .filter(({ player }) => Betting.round(Number(player.stack) || 0) > 0) - .map(({ player, index }) => ({ + .map((player, index) => ({ player, index, held: HoldemState.chipsHeldBy(this.state, player) })) + .filter(({ held }) => held > 0) + .map(({ player, index, held }) => ({ name: HoldemState.displayName(player, index), - stack: Betting.round(Number(player.stack) || 0), - chips: Money.formatChipBreakdown(Number(player.stack) || 0, legend), + stack: held, + chips: Money.formatChipBreakdown(held, legend), })) .filter(row => row.chips); diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js index 8655f7a..c86b436 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js @@ -52,8 +52,7 @@ const SeatMenu = { // the queue is offered, for the same reason the panel only offers them — a list // that isn't kept in order is just a pile of names. const waiting = (this.state.waitingList || [])[0]; - const emptyChair = !(player.name || "").trim() && - !(Number(player.buyIns) > 0) && !(Number(player.stack) > 0); + const emptyChair = !HoldemState.seatInPlay(player); if (waiting && emptyChair) { actions.push({ label: `Seat ${waiting.name}`, @@ -65,7 +64,7 @@ const SeatMenu = { // Leaving for the night, which is a different thing from sitting out a hand: // the chips go back over the rail and the chair opens. Offered only for a seat // that has somebody in it, since an empty chair has nobody to pay. - if ((player.name || "").trim() || Number(player.buyIns) > 0 || Number(player.stack) > 0) { + if (HoldemState.seatInPlay(player)) { actions.push({ label: "Cash out…", run: () => this.hooks.cashOut(index) }); } diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js index 420ab27..7f1d75c 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js @@ -917,8 +917,7 @@ const HoldemState = { openSeats(state) { return state.seats .map((player, index) => ({ player, index })) - .filter(({ player }) => !(player.name || "").trim() && - !(Number(player.buyIns) > 0) && !(Number(player.stack) > 0)) + .filter(({ player }) => !this.seatInPlay(player)) .map(({ index }) => index); }, @@ -929,7 +928,7 @@ const HoldemState = { seatFromWaitingList(state, id, index) { const seat = state.seats[index]; if (!seat) return { ok: false, reason: "There's no such seat." }; - if ((seat.name || "").trim() || Number(seat.buyIns) > 0 || Number(seat.stack) > 0) { + if (this.seatInPlay(seat)) { return { ok: false, reason: `${this.displayName(seat, index)} is already in that seat.` }; } const entry = this.removeFromWaitingList(state, id); diff --git a/tests/holdem/oneStackRule.test.js b/tests/holdem/oneStackRule.test.js new file mode 100644 index 0000000..1184831 --- /dev/null +++ b/tests/holdem/oneStackRule.test.js @@ -0,0 +1,133 @@ +// One question, one answer. +// +// The round before this found the bank check and the standings reading `seat.stack` +// where the felt reads the table's starting stack — two parts of the tool disagreeing +// about how much money is on the table. This is the sweep that followed: everywhere +// else the same two questions get asked, asked the same way. +// +// "What is this seat holding?" -> HoldemState.chipsHeldBy +// "Is anybody in this chair?" -> HoldemState.seatInPlay +const test = require("node:test"); +const { describe } = require("node:test"); +const assert = require("node:assert"); +const { HoldemState, Betting, SettleUp } = require("./load"); + +const openedOnTheDefault = (stack = 200) => { + const state = HoldemState.createDefault(); + HoldemState.resizeSeats(state, 6); + state.defaultStack = stack; + ["Ann", "Ben", "Cal"].forEach((name, i) => { + state.seats[i].name = name; + state.seats[i].inHand = true; + state.seats[i].buyIns = stack; + }); + return state; +}; + +describe("what a seat is holding", () => { + test("is the same number the felt plays with", () => { + const state = openedOnTheDefault(); + state.seats.forEach((player, i) => { + if (i > 2) return; + assert.equal(HoldemState.chipsHeldBy(state, player), Betting.stackOf(state, player)); + }); + }); + + test("and is nothing at all for an empty chair", () => { + const state = openedOnTheDefault(); + // Betting's own answer for an empty chair is the table default, because it is + // only ever asked about seats that are in a hand. The money side has to ask + // about all ten chairs, so it checks whether anybody is in them first. + assert.equal(Betting.stackOf(state, state.seats[5]), 200); + assert.equal(HoldemState.chipsHeldBy(state, state.seats[5]), 0); + }); + + test("counts a stack the dealer typed over the table's default", () => { + const state = openedOnTheDefault(); + state.seats[0].stack = 75; + assert.equal(HoldemState.chipsHeldBy(state, state.seats[0]), 75); + }); +}); + +describe("is anybody in this chair", () => { + const empty = () => HoldemState.createDefault().seats[0]; + + test("an untouched chair is empty", () => { + assert.equal(HoldemState.seatInPlay(empty()), false); + assert.equal(HoldemState.seatInPlay(null), false); + }); + + test("a name, chips, cash, a bet or a dealt hand all count", () => { + [ + ["a name", p => { p.name = "Ann"; }], + ["chips", p => { p.stack = 50; }], + ["cash taken", p => { p.buyIns = 100; }], + ["a bet in front of them", p => { p.committed = 5; }], + ["money already swept in", p => { p.amountOverride = 20; }], + ["a hand dealt to them", p => { p.inHand = true; }], + ].forEach(([what, mark]) => { + const seat = empty(); + mark(seat); + assert.equal(HoldemState.seatInPlay(seat), true, what); + }); + }); + + test("whitespace is not a name", () => { + const seat = empty(); + seat.name = " "; + assert.equal(HoldemState.seatInPlay(seat), false); + }); +}); + +describe("the parts that used to disagree", () => { + test("the open seats and the seat count see the same empty chairs", () => { + const state = openedOnTheDefault(); + // Three players, six chairs. + assert.deepEqual(HoldemState.openSeats(state), [3, 4, 5]); + const occupied = state.seats.filter(p => HoldemState.seatInPlay(p)).length; + assert.equal(occupied + HoldemState.openSeats(state).length, state.seats.length); + }); + + test("a chair holding only a dealt hand is not offered to the waiting list", () => { + // The old test looked at name, buy-ins and stack. A seat marked in-hand with + // none of those would have been handed to the next person in the queue, on top + // of whoever was already playing it. + const state = openedOnTheDefault(); + state.seats[4].inHand = true; + assert.deepEqual(HoldemState.openSeats(state), [3, 5]); + + HoldemState.addToWaitingList(state, "Priya"); + const result = HoldemState.seatFromWaitingList(state, state.waitingList[0].id, 4); + assert.equal(result.ok, false); + }); + + test("the bank, the standings and the settle-up all price the night the same way", () => { + const state = openedOnTheDefault(); + state.seats[0].stack = 320; // Ann is up + state.seats[1].stack = 80; // Ben is down + + const bank = HoldemState.bankReconciliation(state); + const standings = HoldemState.sessionStandings(state); + const chipsFromStandings = standings + .filter(row => !row.cashedOut) + .reduce((sum, row) => sum + row.chipsInFront, 0); + + assert.equal(bank.chipsInPlay, chipsFromStandings, "same chips on the table"); + assert.equal(bank.balanced, true); + + // And the payments the dealer is told to make add up to the same money. + const plan = SettleUp.plan(standings); + const owed = standings.reduce((sum, row) => sum + Math.max(0, row.cashPosition), 0); + const paid = plan.payments.reduce((sum, row) => sum + row.amount, 0); + assert.equal(paid + (plan.shortBy || 0) - (plan.overBy || 0), owed); + }); + + test("a player still holding the table's starting stack is in all three", () => { + const state = openedOnTheDefault(); + const standings = HoldemState.sessionStandings(state); + assert.equal(standings.length, 3); + assert.deepEqual(standings.map(row => row.chipsInFront), [200, 200, 200]); + assert.equal(HoldemState.bankReconciliation(state).chipsInPlay, 600); + assert.deepEqual(SettleUp.plan(standings).payments, [], "everyone is even, so nobody pays anybody"); + }); +}); -- 2.43.0