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:
2026-09-01 12:34:53 -06:00
co-authored by Claude Opus 5
parent 4c1ac222a8
commit 29188d6049
3 changed files with 120 additions and 1 deletions
@@ -117,6 +117,20 @@ const Betting = {
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
// 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.
@@ -279,11 +279,18 @@ const Render = {
totalEl.textContent = total > 0 ? Money.format(total) : "";
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 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);
node.classList.toggle("seatAllIn", allIn);
node.classList.toggle("seatActing", index === this.state.actingSeatIndex);
node.classList.toggle("seatEmpty", empty);
node.classList.toggle("seatInactive", !player.inHand && !empty);
+98
View File
@@ -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);
});