Compare commits

..
Author SHA1 Message Date
jheapsandClaude Sonnet 5 ae591e4212 Stop asking all-in players to act
A player who is all-in is still in the hand and still wins pots, but has nothing
left to decide. The engine didn't know that: it picked the next player to act by
walking to whoever was still in the hand, chips or no chips.

So a dealer with three players all-in pre-flop was asked to check for each of
them on the flop, the turn and the river — nine taps to say nothing — and the
action could land mid-street on someone who'd already pushed their last chip in.

Now the action passes over players who are all-in, and once fewer than two
people have chips left the betting is simply over: the bar runs Deal Flop, Turn,
River, Showdown straight through, and says "All-in — running out the board" so
the absence of a question reads as deliberate rather than as a stuck screen.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 01:37:19 -06:00
4 changed files with 62 additions and 138 deletions
+10 -90
View File
@@ -1480,6 +1480,12 @@ 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;
@@ -1962,75 +1968,20 @@ body.hasActionBar #toastHost {
margin-bottom: 0.6rem; margin-bottom: 0.6rem;
} }
/* This is the screen where the money gets decided, so it holds the same 44px+
targets as the rest of the tool rather than the smallest ones in it. Picking the
wrong winner is the one mistake here that costs somebody real money. */
.potWizardChoice { .potWizardChoice {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.7rem; gap: 0.5rem;
min-height: 56px;
background: #141414; background: #141414;
border: 1px solid #333; border: 1px solid #333;
border-radius: 10px; border-radius: 6px;
padding: 0.6rem 0.8rem; padding: 0.5rem 0.7rem;
cursor: pointer; cursor: pointer;
font-size: 0.95rem; font-size: 0.85rem;
transition: border-color 0.12s ease, background 0.12s ease;
} }
.potWizardChoice:hover { .potWizardChoice:hover {
border-color: #555; border-color: #555;
background: #191919;
}
.potWizardChoice input {
flex: none;
width: 22px;
height: 22px;
accent-color: #1e8449;
}
/* Selected has to be obvious at a glance — a dealer confirms this with players
watching, and a checkbox alone is easy to misread across a table. */
.potWizardChoice.chosen {
border-color: #1e8449;
background: linear-gradient(180deg, #16301f, #12241a);
}
.potWizardChoice.chosen .potWizardChoiceName {
color: #d6ffe0;
}
.potWizardChoiceText {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 0.1rem;
}
.potWizardChoiceName {
color: #f0f0f0;
font-weight: 600;
}
.potWizardChoiceUpTo {
color: #9a9a9a;
font-size: 0.76rem;
font-variant-numeric: tabular-nums;
}
.potWizardChoiceCards {
flex: none;
font-family: var(--font-mono, monospace);
font-size: 1rem;
font-weight: 700;
color: #ddd;
}
.potWizardChoiceCards .cardRed {
color: #ff6b6b;
} }
.potWizardSummary ul { .potWizardSummary ul {
@@ -2051,37 +2002,6 @@ body.hasActionBar #toastHost {
gap: 0.6rem; gap: 0.6rem;
} }
/* Every button in this modal is one a dealer presses with players waiting. */
#potWizardBody .holdemBtn {
min-height: 48px;
justify-content: center;
}
/* The confirm is the whole point of the step, so it gets the whole width rather
than being sized to the length of its own label. */
.potWizardRound > .holdemBtn {
width: 100%;
}
.potWizardActions .holdemBtn {
flex: 1;
}
.potWizardSummary li {
font-variant-numeric: tabular-nums;
}
@media (max-width: 700px) {
/* Stacked, with the action that ends the hand furthest from a stray thumb. */
.potWizardActions {
flex-direction: column-reverse;
}
.potWizardChoice {
min-height: 60px;
}
}
@media (max-width: 900px) { @media (max-width: 900px) {
#holdemLayout { #holdemLayout {
grid-template-columns: 1fr; grid-template-columns: 1fr;
@@ -74,6 +74,16 @@ 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,7 +70,19 @@ 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 && this.stackOf(state, player) >= 0) return index; if (player.inHand && !player.folded) 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;
}, },
@@ -156,7 +168,7 @@ const Betting = {
}, },
passAction(state, index) { passAction(state, index) {
state.actingSeatIndex = this.isStreetComplete(state) ? null : this.nextLiveIndex(state, index); state.actingSeatIndex = this.isStreetComplete(state) ? null : this.nextToAct(state, index);
}, },
// ---- Street and hand boundaries ---- // ---- Street and hand boundaries ----
@@ -184,8 +196,13 @@ 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
? this.nextLiveIndex(state, state.dealerSeatIndex) // It takes two players with chips to have a bet. Once everyone left is all-in
// — 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;
}, },
@@ -193,6 +210,24 @@ 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) {
@@ -215,7 +250,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.nextLiveIndex(state, bbIndex); state.actingSeatIndex = this.nextToAct(state, bbIndex);
}, },
// Winners get their share put back in front of them. // Winners get their share put back in front of them.
@@ -18,14 +18,6 @@ const PotWizard = {
this.playerLabelById = new Map( this.playerLabelById = new Map(
players.map(p => [p.id, this.labelFor(p, state.seats.indexOf(p))]) players.map(p => [p.id, this.labelFor(p, state.seats.indexOf(p))])
); );
// The label carries name and cards together for the log and the payout lines;
// the choice rows want them separately so each can be styled and read on its own.
this.nameById = new Map(
players.map(p => [p.id, HoldemState.displayName(p, state.seats.indexOf(p))])
);
this.cardsById = new Map(
players.map(p => [p.id, (p.cards || []).filter(Boolean)])
);
const contributions = players.map(p => ({ const contributions = players.map(p => ({
id: p.id, id: p.id,
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount), contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
@@ -113,46 +105,13 @@ const PotWizard = {
contenders.forEach(c => { contenders.forEach(c => {
const label = document.createElement("label"); const label = document.createElement("label");
label.className = "potWizardChoice"; label.className = "potWizardChoice";
const checkbox = document.createElement("input"); const checkbox = document.createElement("input");
checkbox.type = "checkbox"; checkbox.type = "checkbox";
checkbox.value = c.id; checkbox.value = c.id;
// The row is the target, not the box — so the row has to look chosen too.
checkbox.addEventListener("change", () => {
label.classList.toggle("chosen", checkbox.checked);
});
label.appendChild(checkbox);
const text = document.createElement("span"); const text = document.createElement("span");
text.className = "potWizardChoiceText"; text.textContent = `${this.playerLabelById.get(c.id)} — up to ${Money.format(c.remaining)}`;
label.appendChild(checkbox);
const name = document.createElement("span");
name.className = "potWizardChoiceName";
name.textContent = this.nameById.get(c.id);
text.appendChild(name);
const upTo = document.createElement("span");
upTo.className = "potWizardChoiceUpTo";
upTo.textContent = `up to ${Money.format(c.remaining)}`;
text.appendChild(upTo);
label.appendChild(text); label.appendChild(text);
// Cards sit apart from the name rather than inside it, so a dealer reading
// a row across the table sees the hand, not a sentence.
const cards = this.cardsById.get(c.id) || [];
if (cards.length > 0) {
const cardsEl = document.createElement("span");
cardsEl.className = "potWizardChoiceCards";
cards.forEach((card, i) => {
if (i > 0) cardsEl.appendChild(document.createTextNode(" "));
const span = document.createElement("span");
if (/[HD]$/i.test(card)) span.className = "cardRed";
span.textContent = Render.formatCard(card);
cardsEl.appendChild(span);
});
label.appendChild(cardsEl);
}
choices.appendChild(label); choices.appendChild(label);
}); });
round.appendChild(choices); round.appendChild(choices);