Compare commits

..
Author SHA1 Message Date
jheapsandClaude Sonnet 5 89d79a0f7d Post the blinds to the players rather than to the chairs
Blinds were assigned by seat offset — always button+1 and button+2 — which is
only right when everyone is dealt in and there are three or more of them.

Heads-up it had the blinds backwards. The button posted the big blind and acted
second before the flop; the rule is the opposite on both counts. That's the end
of every home tournament, dealt wrong.

Worse, the offsets don't care whether anyone is sitting in those chairs. With
two players at a nine-handed table, or with the seats left of the button sitting
out, the blinds landed on empty seats and simply weren't posted — no money in
the middle, and SB/BB badges pointing at nobody.

Blinds now walk the players who are actually dealt in, starting from the button,
with heads-up as its own case: the button is the small blind, acts first
pre-flop, and the big blind acts first afterwards. The badges come from the same
function as the money, so what the table shows and what gets posted can't
disagree.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 01:22:15 -06:00
4 changed files with 56 additions and 63 deletions
@@ -1480,12 +1480,6 @@ body.hasActionBar #holdemSidebar {
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.actionRunout {
margin-left: auto;
color: #d8b46a;
font-size: 0.74rem;
}
.actionWho { .actionWho {
margin-left: auto; margin-left: auto;
color: #9c9c9c; color: #9c9c9c;
@@ -74,16 +74,6 @@ const ActionBar = {
players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`; players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`;
row.appendChild(players); row.appendChild(players);
// Nobody is being asked to act because nobody can — say so, or the bar just
// looks like it's waiting for something.
if (Betting.isRunningOut(this.state) && !Betting.isHandOver(this.state)) {
const runout = document.createElement("span");
runout.className = "actionRunout";
const stillIn = Betting.liveSeats(this.state).length;
runout.textContent = `All-in — running out the board (${stillIn} players)`;
row.appendChild(runout);
}
const index = this.state.actingSeatIndex; const index = this.state.actingSeatIndex;
if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) { if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) {
const player = this.state.seats[index]; const player = this.state.seats[index];
@@ -70,19 +70,7 @@ const Betting = {
for (let step = 1; step <= count; step++) { for (let step = 1; step <= count; step++) {
const index = this.seatAt(state, fromIndex, step); const index = this.seatAt(state, fromIndex, step);
const player = state.seats[index]; const player = state.seats[index];
if (player.inHand && !player.folded) return index; if (player.inHand && !player.folded && this.stackOf(state, player) >= 0) return index;
}
return null;
},
// Who the dealer asks next. A player who is all-in is still in the hand and still
// wins pots, but has no decision left, so the action passes over them.
nextToAct(state, fromIndex) {
const count = state.seatCount;
for (let step = 1; step <= count; step++) {
const index = this.seatAt(state, fromIndex, step);
const player = state.seats[index];
if (player.inHand && !player.folded && this.stackOf(state, player) > this.EPSILON_CHIPS) return index;
} }
return null; return null;
}, },
@@ -168,7 +156,7 @@ const Betting = {
}, },
passAction(state, index) { passAction(state, index) {
state.actingSeatIndex = this.isStreetComplete(state) ? null : this.nextToAct(state, index); state.actingSeatIndex = this.isStreetComplete(state) ? null : this.nextLiveIndex(state, index);
}, },
// ---- Street and hand boundaries ---- // ---- Street and hand boundaries ----
@@ -197,37 +185,24 @@ const Betting = {
const nextStreetIndex = this.STREETS.indexOf(state.street || "preflop") + 1; const nextStreetIndex = this.STREETS.indexOf(state.street || "preflop") + 1;
state.street = this.STREETS[nextStreetIndex] || "river"; state.street = this.STREETS[nextStreetIndex] || "river";
// It takes two players with chips to have a bet. Once everyone left is all-in if (this.liveSeats(state).length <= 1) {
// — or only one player still has anything behind — the rest of the board is state.actingSeatIndex = null;
// just dealt out, and asking someone to check when they have nothing to check return;
// with only slows the dealer down. }
state.actingSeatIndex = this.canStillBet(state)
? this.nextToAct(state, state.dealerSeatIndex) // After the flop the first live player left of the button acts — which heads-up
: null; // is the big blind, since the button is the small blind. Naming the big blind
// directly also covers a button parked on a seat nobody is sitting in.
const { bbIndex, headsUp } = HoldemState.getBlindSeats(state);
state.actingSeatIndex = headsUp && bbIndex !== null && !state.seats[bbIndex].folded
? bbIndex
: this.nextLiveIndex(state, state.dealerSeatIndex);
}, },
isHandOver(state) { isHandOver(state) {
return this.liveSeats(state).length <= 1; return this.liveSeats(state).length <= 1;
}, },
// Players still in the hand who have chips left to put in. Fewer than two of them
// means the betting is finished even though the hand isn't.
seatsWithChips(state) {
return this.liveSeats(state).filter(s => this.stackOf(state, s.player) > this.EPSILON_CHIPS);
},
EPSILON_CHIPS: 0.004,
canStillBet(state) {
return this.liveSeats(state).length > 1 && this.seatsWithChips(state).length > 1;
},
// True when the hand is still live but nobody can bet again — the dealer is just
// running the board out.
isRunningOut(state) {
return this.liveSeats(state).length > 1 && !this.canStillBet(state);
},
// Deals a fresh betting round: blinds posted, action to the seat after the big // Deals a fresh betting round: blinds posted, action to the seat after the big
// blind, which is where a dealer expects to be looking when the cards go out. // blind, which is where a dealer expects to be looking when the cards go out.
beginHand(state) { beginHand(state) {
@@ -242,15 +217,23 @@ const Betting = {
const sb = Number(state.smallBlind) || 0; const sb = Number(state.smallBlind) || 0;
const bb = Number(state.bigBlind) || 0; const bb = Number(state.bigBlind) || 0;
const sbIndex = this.seatAt(state, state.dealerSeatIndex, 1); const { sbIndex, bbIndex, headsUp } = HoldemState.getBlindSeats(state);
const bbIndex = this.seatAt(state, state.dealerSeatIndex, 2);
if (sb > 0 && state.seats[sbIndex].inHand) this.putIn(state, sbIndex, sb); if (sbIndex === null || bbIndex === null) {
if (bb > 0 && state.seats[bbIndex].inHand) this.putIn(state, bbIndex, bb); state.currentBet = 0;
state.actingSeatIndex = null;
return;
}
if (sb > 0) this.putIn(state, sbIndex, sb);
if (bb > 0) this.putIn(state, bbIndex, bb);
state.currentBet = this.round(Math.max(this.committed(state.seats[sbIndex]), this.committed(state.seats[bbIndex]))); state.currentBet = this.round(Math.max(this.committed(state.seats[sbIndex]), this.committed(state.seats[bbIndex])));
state.lastRaiseSize = bb || 0; state.lastRaiseSize = bb || 0;
state.actingSeatIndex = this.nextToAct(state, bbIndex);
// Heads-up the button is the small blind and acts first before the flop; with
// three or more it's the seat after the big blind.
state.actingSeatIndex = headsUp ? sbIndex : this.nextLiveIndex(state, bbIndex);
}, },
// Winners get their share put back in front of them. // Winners get their share put back in front of them.
@@ -163,10 +163,36 @@ const HoldemState = {
state.dealerSeatIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1); state.dealerSeatIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1);
}, },
// Seats dealt into this hand, in blind order starting from the seat after the
// button. Blinds follow the players, not the chairs — the seats immediately left
// of the button are empty often enough that counting chairs posts them to nobody.
liveOrderFromButton(state) {
const order = [];
for (let step = 1; step <= state.seatCount; step++) {
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
if (state.seats[index].inHand) order.push(index);
}
return order;
},
// Who posts what. Heads-up is its own rule: with two players the button posts the
// small blind, which is the opposite of the usual order.
getBlindSeats(state) { getBlindSeats(state) {
const sbIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1); const live = this.liveOrderFromButton(state);
const bbIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 2); const dealerIndex = state.dealerSeatIndex;
return { dealerIndex: state.dealerSeatIndex, sbIndex, bbIndex };
if (live.length < 2) {
return { dealerIndex, sbIndex: null, bbIndex: null, headsUp: false };
}
if (live.length === 2) {
const buttonIsPlaying = state.seats[dealerIndex].inHand;
const sbIndex = buttonIsPlaying ? dealerIndex : live[0];
const bbIndex = live.find(i => i !== sbIndex);
return { dealerIndex, sbIndex, bbIndex, headsUp: true };
}
return { dealerIndex, sbIndex: live[0], bbIndex: live[1], headsUp: false };
}, },
startNextHand(state) { startNextHand(state) {