Compare commits

..
Author SHA1 Message Date
jheapsandClaude Sonnet 5 18bb23ad6d Pass the button to the next player, not the next chair
The button moved one seat per hand regardless of whether anyone was sitting
there. At a ten-seat table with four players — an ordinary home game — it spent
five hands out of eight on an empty chair.

That reads as untidy, but the real problem is fairness. Position and the blinds
are meant to come round to each player at the same rate. Rotating past empty
chairs breaks that: a player with three empty seats to their right takes the
button far less often than one sitting in a cluster, while both pay blinds every
time the rotation reaches them.

The button now skips to the next seat that's actually dealt in, so with four
players it visits those four in turn and each takes it once every four hands.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 01:50:16 -06:00
4 changed files with 18 additions and 56 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 ----
@@ -196,13 +184,8 @@ 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";
state.actingSeatIndex = this.liveSeats(state).length > 1
// It takes two players with chips to have a bet. Once everyone left is all-in ? this.nextLiveIndex(state, state.dealerSeatIndex)
// — or only one player still has anything behind — the rest of the board is
// just dealt out, and asking someone to check when they have nothing to check
// with only slows the dealer down.
state.actingSeatIndex = this.canStillBet(state)
? this.nextToAct(state, state.dealerSeatIndex)
: null; : null;
}, },
@@ -210,24 +193,6 @@ const Betting = {
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) {
@@ -250,7 +215,7 @@ const Betting = {
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); state.actingSeatIndex = this.nextLiveIndex(state, bbIndex);
}, },
// Winners get their share put back in front of them. // Winners get their share put back in front of them.
@@ -159,7 +159,20 @@ const HoldemState = {
return ((fromIndex + delta) % count + count) % count; return ((fromIndex + delta) % count + count) % count;
}, },
// The button passes to the next player, not the next chair. Moving it one seat at
// a time parks it on empty chairs — with four players spread around ten seats it
// sits on nobody most hands — and that isn't only untidy: position and blinds are
// supposed to come round to each player at the same rate, and they don't if the
// button is rotating past chairs nobody is sitting in.
advanceButton(state) { advanceButton(state) {
for (let step = 1; step <= state.seatCount; step++) {
const index = this.nextSeatIndex(state, state.dealerSeatIndex, step);
if (state.seats[index].inHand) {
state.dealerSeatIndex = index;
return;
}
}
// Nobody is dealt in; move it along anyway so it doesn't look stuck.
state.dealerSeatIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1); state.dealerSeatIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1);
}, },