Say ALL IN on the felt
A player with nothing behind had a blank where their stack goes. So did a seat whose stack nobody had entered. On a table with side pots that is the one distinction the dealer is working from -- who can still be bet at, and who is already in for everything they have -- and the felt drew the two states identically. Ann $40 $160 has chips Ben $40 all-in for 40 Cal $40 $160 has chips Dee $12 all-in for 12 The action bar knew: it had already built "Main pot $48, Ann/Ben/Cal/Dee" and "Side pot 1 $84, Ann/Ben/Cal". The table it sits under didn't say a word. Betting.isAllIn is the same stack test nextToAct uses to pass over a player, which is the point: the seat the felt marks is exactly the seat the dealer will never be asked to act on again. There's a test asserting that equivalence rather than restating the condition. The second half of the condition matters. A seat with an empty stack that has put nothing in is a busted player who shouldn't have been dealt in, not a player who moved all-in, so it isn't marked. "ALL IN" in gold where the stack was, and a warmer border on the seat -- deliberately quieter than the acting-seat ring, because all-in is a standing fact about a player rather than a request for attention. 9 tests (172 total). They fail on a naive condition: dropping the committed-chips and folded checks fails 4 of them. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -117,6 +117,20 @@ const Betting = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// A player still in the hand with chips in the pot and nothing left behind. This is
|
||||||
|
// the same stack test nextToAct uses to pass over them, which is the point: the
|
||||||
|
// seat the felt marks all-in is exactly the seat the dealer will never be asked to
|
||||||
|
// act on again.
|
||||||
|
//
|
||||||
|
// The "chips in the pot" half matters. A seat with an empty stack who hasn't put
|
||||||
|
// anything in is a busted player who shouldn't have been dealt in, not a player
|
||||||
|
// who moved all-in, and calling them all-in on the felt would be a lie.
|
||||||
|
isAllIn(state, player) {
|
||||||
|
if (!player || !player.inHand || player.folded) return false;
|
||||||
|
return this.handTotal(player) > this.EPSILON_CHIPS &&
|
||||||
|
this.stackOf(state, player) <= this.EPSILON_CHIPS;
|
||||||
|
},
|
||||||
|
|
||||||
// Which chair sits `step` places along in the direction of play. The table owns
|
// Which chair sits `step` places along in the direction of play. The table owns
|
||||||
// this — walking the seats is exactly where the button, the blinds and the action
|
// this — walking the seats is exactly where the button, the blinds and the action
|
||||||
// used to drift apart from each other, so there is one implementation of it.
|
// used to drift apart from each other, so there is one implementation of it.
|
||||||
|
|||||||
@@ -279,11 +279,18 @@ const Render = {
|
|||||||
totalEl.textContent = total > 0 ? Money.format(total) : "";
|
totalEl.textContent = total > 0 ? Money.format(total) : "";
|
||||||
this.fitAmount(totalEl);
|
this.fitAmount(totalEl);
|
||||||
|
|
||||||
|
// A player with nothing behind used to leave this line blank, so the felt drew
|
||||||
|
// an all-in exactly like a seat whose stack nobody had entered. On a table with
|
||||||
|
// side pots that's the one distinction the dealer is working from — who can
|
||||||
|
// still be bet at, and who is already committed for everything they have.
|
||||||
const stackEl = node.querySelector(".seatPuckStack");
|
const stackEl = node.querySelector(".seatPuckStack");
|
||||||
const stack = Betting.stackOf(this.state, player);
|
const stack = Betting.stackOf(this.state, player);
|
||||||
stackEl.textContent = !empty && stack > 0 ? Money.format(stack) : "";
|
const allIn = Betting.isAllIn(this.state, player);
|
||||||
|
stackEl.textContent = empty ? "" : allIn ? "ALL IN" : stack > 0 ? Money.format(stack) : "";
|
||||||
|
stackEl.classList.toggle("seatPuckAllIn", allIn);
|
||||||
this.fitAmount(stackEl);
|
this.fitAmount(stackEl);
|
||||||
|
|
||||||
|
node.classList.toggle("seatAllIn", allIn);
|
||||||
node.classList.toggle("seatActing", index === this.state.actingSeatIndex);
|
node.classList.toggle("seatActing", index === this.state.actingSeatIndex);
|
||||||
node.classList.toggle("seatEmpty", empty);
|
node.classList.toggle("seatEmpty", empty);
|
||||||
node.classList.toggle("seatInactive", !player.inHand && !empty);
|
node.classList.toggle("seatInactive", !player.inHand && !empty);
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Who the felt marks "ALL IN".
|
||||||
|
//
|
||||||
|
// The mark's whole value is that a dealer can scan the ring and see who can still be
|
||||||
|
// bet at, so the test that matters most is the last one: the seats it marks are
|
||||||
|
// exactly the seats the betting round refuses to ask for another chip.
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load");
|
||||||
|
|
||||||
|
test("a player who moved all-in is marked", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||||
|
state.seats[3].stack = 12;
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.allIn(state, state.actingSeatIndex);
|
||||||
|
|
||||||
|
assert.equal(Betting.isAllIn(state, state.seats[3]), true);
|
||||||
|
assert.equal(state.seats[3].stack, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a player with chips behind is not", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
assert.equal(Betting.isAllIn(state, state.seats[0]), false);
|
||||||
|
assert.equal(Betting.isAllIn(state, state.seats[1]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a busted player who was dealt in but has put nothing in is not", () => {
|
||||||
|
// An empty stack alone isn't all-in — it's someone who shouldn't have been dealt
|
||||||
|
// in. Calling that "ALL IN" on the felt would be a lie about a live pot.
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
assert.equal(Betting.isAllIn(state, state.seats[2]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a folded player with an empty stack is not", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[1].committed = 5;
|
||||||
|
state.seats[1].stack = 0;
|
||||||
|
state.seats[1].folded = true;
|
||||||
|
assert.equal(Betting.isAllIn(state, state.seats[1]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an empty chair is not", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
assert.equal(Betting.isAllIn(state, state.seats[4]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a seat with no stack of its own falls back to the table's and isn't marked", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.defaultStack = 200;
|
||||||
|
state.seats[2].stack = null;
|
||||||
|
state.seats[2].committed = 10;
|
||||||
|
assert.equal(Betting.isAllIn(state, state.seats[2]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the seats marked are exactly the ones the action passes over", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||||
|
state.seats[1].stack = 8;
|
||||||
|
state.seats[3].stack = 30;
|
||||||
|
Betting.beginHand(state);
|
||||||
|
|
||||||
|
// Everyone in, two of them for less than the others can cover.
|
||||||
|
for (let step = 0; step < 4; step++) Betting.allIn(state, state.actingSeatIndex);
|
||||||
|
|
||||||
|
const marked = state.seats
|
||||||
|
.map((player, index) => (Betting.isAllIn(state, player) ? index : null))
|
||||||
|
.filter(index => index !== null);
|
||||||
|
const skipped = state.seats
|
||||||
|
.map((player, index) =>
|
||||||
|
player.inHand && !player.folded && Betting.nextToAct(state, index - 1) !== index ? index : null)
|
||||||
|
.filter(index => index !== null);
|
||||||
|
|
||||||
|
assert.deepEqual(marked, skipped);
|
||||||
|
assert.ok(marked.length > 0, "the scenario should actually produce all-in players");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nobody is marked once the next hand starts", () => {
|
||||||
|
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].stack = 5;
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.allIn(state, state.actingSeatIndex);
|
||||||
|
assert.ok(state.seats.some(player => Betting.isAllIn(state, player)));
|
||||||
|
|
||||||
|
// The short stack got there by winning nothing; give them chips back the way a
|
||||||
|
// dealer would before the next deal.
|
||||||
|
HoldemState.startNextHand(state);
|
||||||
|
state.seats[2].stack = 50;
|
||||||
|
assert.equal(state.seats.filter(player => Betting.isAllIn(state, player)).length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("marking nobody doesn't move a chip", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||||
|
state.seats[3].stack = 12;
|
||||||
|
Betting.beginHand(state);
|
||||||
|
const before = chipsOnTable(state);
|
||||||
|
state.seats.forEach(player => Betting.isAllIn(state, player));
|
||||||
|
assert.equal(chipsOnTable(state), before);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user