Compare commits

..
Author SHA1 Message Date
jheapsandClaude Sonnet 5 cc1b5011a1 Give the odd chip to the player the rule says gets it
Quick Split tells the dealer a leftover chip goes "to the first player left of
the button". The pot wizard, on the same tool, handed it to whoever the dealer
happened to tick first. Two screens, same split, different answer.

The wizard now orders the winners around the table from the button before the
calculator shares the pot out, so the leftover lands where the instruction says
it does. The calculator itself is unchanged — it already gave leftovers to the
front of the list, it was just being handed the list in the order the checkboxes
were clicked.

It's pennies, but it's pennies in front of the person who didn't get them, and a
dealer who has just read the other screen's instruction out loud looks wrong.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-01 02:05:57 -06:00
3 changed files with 27 additions and 56 deletions
@@ -184,19 +184,9 @@ const Betting = {
const nextStreetIndex = this.STREETS.indexOf(state.street || "preflop") + 1;
state.street = this.STREETS[nextStreetIndex] || "river";
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);
state.actingSeatIndex = this.liveSeats(state).length > 1
? this.nextLiveIndex(state, state.dealerSeatIndex)
: null;
},
isHandOver(state) {
@@ -217,23 +207,15 @@ const Betting = {
const sb = Number(state.smallBlind) || 0;
const bb = Number(state.bigBlind) || 0;
const { sbIndex, bbIndex, headsUp } = HoldemState.getBlindSeats(state);
const sbIndex = this.seatAt(state, state.dealerSeatIndex, 1);
const bbIndex = this.seatAt(state, state.dealerSeatIndex, 2);
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);
if (sb > 0 && state.seats[sbIndex].inHand) this.putIn(state, sbIndex, sb);
if (bb > 0 && state.seats[bbIndex].inHand) 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;
// 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);
state.actingSeatIndex = this.nextLiveIndex(state, bbIndex);
},
// Winners get their share put back in front of them.
@@ -18,6 +18,7 @@ const PotWizard = {
this.playerLabelById = new Map(
players.map(p => [p.id, this.labelFor(p, state.seats.indexOf(p))])
);
this.seatIndexById = new Map(players.map(p => [p.id, state.seats.indexOf(p)]));
const contributions = players.map(p => ({
id: p.id,
contribution: HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount),
@@ -49,6 +50,20 @@ const PotWizard = {
this.totals.set(id, (this.totals.get(id) || 0) + amount);
},
// A chopped pot rarely divides exactly, and the odd chip goes to the first player
// left of the button — not to whoever the dealer happened to tick first. The
// calculator hands leftovers out in the order it's given the winners, so ordering
// them here is what makes the payout match the rule.
orderFromButton(ids) {
const distance = new Map();
for (let step = 1; step <= this.state.seatCount; step++) {
distance.set(HoldemState.nextSeatIndex(this.state, this.state.dealerSeatIndex, step), step);
}
return [...ids].sort((a, b) =>
(distance.get(this.seatIndexById.get(a)) ?? 0) - (distance.get(this.seatIndexById.get(b)) ?? 0)
);
},
renderStep() {
// Auto-resolve any round where only one player is left standing — nothing to ask.
let contenders = PotCalculator.getContenders(this.entries);
@@ -127,7 +142,7 @@ const PotWizard = {
confirmBtn.addEventListener("click", () => {
const selected = Array.from(choices.querySelectorAll("input:checked")).map(i => i.value);
if (selected.length === 0) return;
const result = PotCalculator.resolveRound(this.entries, selected);
const result = PotCalculator.resolveRound(this.entries, this.orderFromButton(selected));
this.recordRound(result);
this.entries = result.updatedEntries;
this.renderStep();
@@ -163,36 +163,10 @@ 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 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 };
const sbIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 1);
const bbIndex = this.nextSeatIndex(state, state.dealerSeatIndex, 2);
return { dealerIndex: state.dealerSeatIndex, sbIndex, bbIndex };
},
startNextHand(state) {