Buying chips puts you back in the game
Handing over cash and being dealt in are one event at a real table. Here they were two, and the second one was easy to miss. Last change deals a busted player around until they rebuy. So the dealer takes their money, taps Add chips, and: Ben: inHand false -> false stack 0 -> 100 buyIns 100 -> 200 toast: "Ben added $100." He has chips, he is not in the game, and nothing on screen says so. He gets skipped hand after hand until somebody notices and finds "Deal in" in the seat menu. Now the buy-in does both, and says which: mid-hand: "Ben added $100 - in from the next hand." inHand true, folded true between hands: "Ben added $100 - dealt in." inHand true, folded false Folding him for the hand in progress is the rule "Deal in" already follows -- you can't buy into a pot you have not been paying for -- so the two doors into the game behave the same way. It is one undoable step: undo takes back the chips, the cash record and the seat. Not for a seat that was sat out while holding chips. That is a dealer who sat someone out on purpose and is topping them up, and dealing them back in would be the tool guessing at something it was not told. The test reads the seat's own recorded stack rather than stackOf(), which substitutes the table's starting stack and would make a seat with nothing recorded look like it was holding chips. busted, sat out -> back in new chair, no stack yet -> in sat out holding $100 -> stays out already in the hand -> nothing to do, including an all-in at zero chips 8 tests (214 total). Driven through the UI: the rebuy, the toast, the next hand actually dealing him in (4 players, live 4), and undo putting all three fields back. Found on the way, not fixed here Buying in on a seat with no stack of its own credits the table's starting stack *plus* the buy-in. A new player handed $100 on a table with a $100 default ends up with $200 of chips against $100 of recorded cash. I am leaving it because the right answer is genuinely unclear and it is not mine to pick. A blank stack means "no chips recorded" to the bank check, which counts it as zero, and "the table's starting stack" to the betting code and the felt, which show it as $100. Both are defensible for their own job, and which one a buy-in should follow decides whether a casual dealer who never types stacks gets $200 or $100 after a rebuy. Worth saying out loud rather than quietly picking one. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -90,6 +90,28 @@ const Betting = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Does handing this seat chips also put them back in the game?
|
||||||
|
//
|
||||||
|
// At a real table those are one event, and here they were two: a busted player got
|
||||||
|
// their chips and stayed sat out, silently skipped hand after hand until somebody
|
||||||
|
// noticed and found "Deal in" in the seat menu. Nothing said so — the toast just
|
||||||
|
// read "Ben added $100".
|
||||||
|
//
|
||||||
|
// Not for a seat that was sat out while *holding* chips. That's a dealer who sat
|
||||||
|
// somebody out on purpose and is topping them up, and dealing them back in would be
|
||||||
|
// the tool guessing at something it wasn't told. Only a seat with no stack of its
|
||||||
|
// own: busted at zero, or a chair being bought into for the first time.
|
||||||
|
//
|
||||||
|
// Deliberately reads player.stack rather than stackOf(), which substitutes the
|
||||||
|
// table's starting stack for a seat that has none — the question here is what this
|
||||||
|
// seat was actually recorded as holding.
|
||||||
|
buyInDealsThemIn(player) {
|
||||||
|
if (!player || player.inHand) return false;
|
||||||
|
const own = player.stack;
|
||||||
|
const recorded = own !== null && own !== undefined && String(own).trim() !== "";
|
||||||
|
return !recorded || Number(own) <= this.EPSILON_CHIPS;
|
||||||
|
},
|
||||||
|
|
||||||
// "bet" or "raise" — what putting more in is actually called right now.
|
// "bet" or "raise" — what putting more in is actually called right now.
|
||||||
//
|
//
|
||||||
// A wager into a pot nobody has wagered into is a bet; everything after that is a
|
// A wager into a pot nobody has wagered into is a bet; everything after that is a
|
||||||
|
|||||||
@@ -140,11 +140,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
editSeat: index => Render.toggleSeatEditor(index),
|
editSeat: index => Render.toggleSeatEditor(index),
|
||||||
|
|
||||||
addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
|
addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
|
||||||
seatAction(seatIndex, `${nameOf(seatIndex)} added ${Money.format(amount)}.`, player => {
|
// Buying chips puts you back in the game — see Betting.buyInDealsThemIn.
|
||||||
|
const dealBackIn = Betting.buyInDealsThemIn(state.seats[seatIndex]);
|
||||||
|
const handRunning = !Betting.onlyBlindsIn(state) || Betting.potSoFar(state) > 0;
|
||||||
|
|
||||||
|
const message = `${nameOf(seatIndex)} added ${Money.format(amount)}` +
|
||||||
|
(dealBackIn
|
||||||
|
? handRunning ? " — in from the next hand." : " — dealt in."
|
||||||
|
: ".");
|
||||||
|
|
||||||
|
seatAction(seatIndex, message, p => {
|
||||||
// Chips in front and cash in are different questions — the stack moves
|
// Chips in front and cash in are different questions — the stack moves
|
||||||
// every hand, what they've handed over only moves when they buy.
|
// every hand, what they've handed over only moves when they buy.
|
||||||
player.stack = Money.round(Betting.stackOf(state, player) + amount);
|
p.stack = Money.round(Betting.stackOf(state, p) + amount);
|
||||||
player.buyIns = Money.round((Number(player.buyIns) || 0) + amount);
|
p.buyIns = Money.round((Number(p.buyIns) || 0) + amount);
|
||||||
|
|
||||||
|
if (dealBackIn) {
|
||||||
|
p.inHand = true;
|
||||||
|
// Same rule "Deal in" follows: you can't buy into a pot you haven't
|
||||||
|
// been paying for, and `folded` is exactly that state for one hand.
|
||||||
|
p.folded = handRunning;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Whether handing a seat chips also puts them back in the game.
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Betting, makeTable } = require("./load");
|
||||||
|
|
||||||
|
test("a busted player who has been sat out comes back in", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[1].stack = 0;
|
||||||
|
Betting.beginHand(state); // the deal sits them out
|
||||||
|
|
||||||
|
assert.equal(state.seats[1].inHand, false);
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[1]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a chair being bought into for the first time comes in", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
state.seats[4].name = "Eve";
|
||||||
|
state.seats[4].stack = null;
|
||||||
|
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[4]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a seat sat out while holding chips stays out", () => {
|
||||||
|
// A dealer sat them out on purpose and is topping them up. Dealing them back in
|
||||||
|
// would be the tool guessing at something it wasn't told.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].inHand = false;
|
||||||
|
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[2]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("somebody already in the hand is left exactly where they are", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[0]), false);
|
||||||
|
|
||||||
|
// Including a player who is in but out of chips — they're all-in, not sat out.
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
assert.equal(state.seats[2].inHand, true);
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[2]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a blank stack field reads as nothing recorded, not as a number", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
state.seats[3].name = "Dee";
|
||||||
|
state.seats[3].stack = "";
|
||||||
|
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[3]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it doesn't ask the table what a missing stack would have been", () => {
|
||||||
|
// stackOf substitutes the table's starting stack for a seat that has none, which
|
||||||
|
// would make a seat with nothing recorded look like it was holding chips.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
state.defaultStack = 500;
|
||||||
|
state.seats[4].name = "Eve";
|
||||||
|
state.seats[4].stack = null;
|
||||||
|
|
||||||
|
assert.equal(Betting.stackOf(state, state.seats[4]), 500);
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[4]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("asking doesn't change anything", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
state.seats[2].inHand = false;
|
||||||
|
const before = JSON.stringify(state);
|
||||||
|
|
||||||
|
Betting.buyInDealsThemIn(state.seats[2]);
|
||||||
|
|
||||||
|
assert.equal(JSON.stringify(state), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nothing to decide about a seat that isn't there", () => {
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(null), false);
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(undefined), false);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user