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 61 additions and 144 deletions
+4 -90
View File
@@ -1962,75 +1962,20 @@ body.hasActionBar #toastHost {
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 {
display: flex;
align-items: center;
gap: 0.7rem;
min-height: 56px;
gap: 0.5rem;
background: #141414;
border: 1px solid #333;
border-radius: 10px;
padding: 0.6rem 0.8rem;
border-radius: 6px;
padding: 0.5rem 0.7rem;
cursor: pointer;
font-size: 0.95rem;
transition: border-color 0.12s ease, background 0.12s ease;
font-size: 0.85rem;
}
.potWizardChoice:hover {
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 {
@@ -2051,37 +1996,6 @@ body.hasActionBar #toastHost {
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) {
#holdemLayout {
grid-template-columns: 1fr;
@@ -184,9 +184,19 @@ const Betting = {
const nextStreetIndex = this.STREETS.indexOf(state.street || "preflop") + 1;
state.street = this.STREETS[nextStreetIndex] || "river";
state.actingSeatIndex = this.liveSeats(state).length > 1
? this.nextLiveIndex(state, state.dealerSeatIndex)
: null;
if (this.liveSeats(state).length <= 1) {
state.actingSeatIndex = null;
return;
}
// After the flop the first live player left of the button acts — which heads-up
// 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) {
@@ -207,15 +217,23 @@ const Betting = {
const sb = Number(state.smallBlind) || 0;
const bb = Number(state.bigBlind) || 0;
const sbIndex = this.seatAt(state, state.dealerSeatIndex, 1);
const bbIndex = this.seatAt(state, state.dealerSeatIndex, 2);
const { sbIndex, bbIndex, headsUp } = HoldemState.getBlindSeats(state);
if (sb > 0 && state.seats[sbIndex].inHand) this.putIn(state, sbIndex, sb);
if (bb > 0 && state.seats[bbIndex].inHand) this.putIn(state, bbIndex, bb);
if (sbIndex === null || bbIndex === null) {
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.lastRaiseSize = bb || 0;
state.actingSeatIndex = this.nextLiveIndex(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.
@@ -18,14 +18,6 @@ const PotWizard = {
this.playerLabelById = new Map(
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 => ({
id: p.id,
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
@@ -113,46 +105,13 @@ const PotWizard = {
contenders.forEach(c => {
const label = document.createElement("label");
label.className = "potWizardChoice";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
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");
text.className = "potWizardChoiceText";
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);
text.textContent = `${this.playerLabelById.get(c.id)} — up to ${Money.format(c.remaining)}`;
label.appendChild(checkbox);
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);
});
round.appendChild(choices);
@@ -163,10 +163,36 @@ const HoldemState = {
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) {
const sbIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1);
const bbIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 2);
return { dealerIndex: state.dealerSeatIndex, sbIndex, bbIndex };
const live = this.liveOrderFromButton(state);
const dealerIndex = state.dealerSeatIndex;
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) {