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
jheaps e8d6098d39 Merge pull request 'Add chips to a seat without leaving the table' (#32) from feature/casino-holdem-rebuys into master
Build and Deploy / build-deploy (push) Successful in 1m7s
Reviewed-on: #32
2026-08-31 23:57:01 -06:00
jheaps 5d1a6b7399 Merge branch 'master' into feature/casino-holdem-rebuys 2026-08-31 23:56:53 -06:00
jheaps a816b32426 Merge pull request 'A rulings reference for the calls that come up mid-hand' (#30) from feature/casino-holdem-rulings into master
Build and Deploy / build-deploy (push) Canceled after 31s
Reviewed-on: #30
2026-08-31 23:56:40 -06:00
jheaps 5271d5fbad Merge branch 'master' into feature/casino-holdem-rulings 2026-08-31 23:56:32 -06:00
jheapsandClaude Sonnet 5 552ec1a542 Add chips to a seat without leaving the table
Somebody rebuying is the most common interruption in a cash game, and the only
way to record it was to open the seat editor and retype a stack from memory.

Press and hold a seat and there's now "Add chips…": quick amounts off the
table's starting stack — half a buy-in, a buy-in, two — with a keypad for the
times it isn't one of those. It's undoable like every other seat action, and it
records cash handed over as well as chips added, because those answer different
questions. The stack moves every hand; what a player has paid in only moves
when they buy.

The export gains bought in, chips now, and up/down, so settling up at the end of
the night is reading a row rather than reconstructing it.

Getting that last column right needed care. Chips in front of a player disagree
with the hand log for as long as a hand is open — and since blinds post the
instant the previous hand settles, that window is almost always. Counting what a
player already has in the current pot as still theirs makes the cash position
agree with the hand log at any moment, including across a rebuy.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-08-31 23:53:15 -06:00
jheapsandClaude Sonnet 5 9a5b974fda Add a rulings reference for the calls that come up mid-hand
The tool handles the money; the other half of dealing is the calls. A home-game
dealer who isn't a pro has to rule on a misdeal or a prematurely turned board
card with a table watching, and the usual outcome is an argument settled by
whoever is loudest.

A "Common rulings" link under the table opens a reference for exactly the
situations that come up: what counts as a misdeal, what to do when the fourth
board card gets turned before the betting is finished, and how acting out of
turn is normally treated. It follows the Tournament Directors Association
rules, and says plainly that house rules win and that a dispute gets settled
between hands rather than during one.

It's a sheet rather than a sixth sidebar panel — a dealer consults this rarely
and needs it immediately, which is the opposite of what a panel is for.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-08-31 23:35:59 -06:00
11 changed files with 555 additions and 14 deletions
@@ -35,6 +35,9 @@
<div id="seatsLayer"></div>
</div>
<p id="tableHint" class="tableHint"></p>
<p class="tableHintActions">
<button type="button" id="openRulingsBtn" class="linkBtn">Common rulings</button>
</p>
</section>
<aside id="holdemSidebar">
@@ -179,6 +182,7 @@
<span>Stack</span>
<input type="number" class="stackInput" min="0" step="1" placeholder="table default" />
</label>
<div class="seatBuyIns" hidden></div>
<div class="seatContribution">
<div class="chipCounts"></div>
<label class="fieldRow amountOverrideRow">
@@ -235,6 +239,73 @@
</div>
</template>
<div id="addChipsOverlay" hidden>
<div id="addChipsSheet" role="dialog" aria-labelledby="addChipsTitle">
<div class="seatSheetGrip" aria-hidden="true"></div>
<div class="addChipsHeader">
<h2 id="addChipsTitle"></h2>
<span id="addChipsSubtitle"></span>
</div>
<div id="addChipsQuick"></div>
<div class="addChipsEntry">
<span class="addChipsCurrency">$</span>
<input type="text" id="addChipsInput" inputmode="decimal" autocomplete="off" aria-label="Amount to add" />
</div>
<div class="addChipsActions">
<button id="addChipsConfirmBtn" class="holdemBtn holdemBtnAccent" type="button">Add to stack</button>
<button id="addChipsCancelBtn" class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
</div>
</div>
</div>
<div id="rulingsOverlay" hidden>
<div id="rulingsSheet" role="dialog" aria-labelledby="rulingsTitle">
<div class="seatSheetGrip" aria-hidden="true"></div>
<div class="rulingsHeader">
<h2 id="rulingsTitle">Common rulings</h2>
<button id="closeRulingsBtn" class="holdemBtn holdemBtnSmall holdemBtnGhost" type="button">Done</button>
</div>
<p class="rulingsIntro">
The conventions most rooms follow, for the calls that come up mid-hand.
Your house rules always win — and settle a dispute before the next hand,
not during it.
</p>
<section class="ruling">
<h3>Redeal it — misdeal</h3>
<ul>
<li>The first card goes to the wrong seat.</li>
<li>A seat entitled to a hand gets dealt out, or a seat that isn't entitled gets dealt in.</li>
<li>Two or more boxed cards turn up on the initial deal.</li>
<li>Either of the first two cards is exposed by dealer error.</li>
</ul>
</section>
<section class="ruling">
<h3>A board card turned too early</h3>
<p>
If the fourth board card gets turned before the betting round is finished,
that card is out of play for the round. Finish the betting, then burn and
turn what would have been the fifth card in the fourth card's place.
</p>
</section>
<section class="ruling">
<h3>Acting out of turn</h3>
<p>
A one-off is usually just backed up if no one has acted behind them.
Doing it repeatedly is what earns a penalty.
</p>
</section>
<p class="rulingsSource">
Follows the Tournament Directors Association rules, which most rooms and
home games take their lead from.
</p>
</div>
</div>
<div id="seatMenuOverlay" hidden>
<div id="seatMenuSheet" role="dialog" aria-labelledby="seatMenuTitle">
<div class="seatSheetGrip" aria-hidden="true"></div>
@@ -297,6 +368,7 @@
<script src="~/js/casino/holdem/render.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/cardPicker.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/seatMenu.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/addChips.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/betting.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/actionBar.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/shortcuts.js?v=@ViewData["cssVersion"]"></script>
+260
View File
@@ -585,6 +585,260 @@ body {
font-weight: 400;
}
/* ---------- Add chips ---------- */
.seatBuyIns {
margin-top: -0.3rem;
color: #9a9a9a;
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
#addChipsOverlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 98;
}
#addChipsOverlay[hidden] {
display: none;
}
#addChipsSheet {
width: 100%;
max-width: 420px;
background: linear-gradient(180deg, #242424, #1a1a1a);
border: 1px solid #3a3a3a;
border-bottom: none;
border-radius: 16px 16px 0 0;
padding: 0.35rem 1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
}
.addChipsHeader {
margin-bottom: 0.8rem;
}
.addChipsHeader h2 {
margin: 0;
font-size: 1rem;
font-weight: 500;
color: #f5f5f5;
}
#addChipsSubtitle {
color: #9a9a9a;
font-size: 0.78rem;
font-variant-numeric: tabular-nums;
}
#addChipsQuick {
display: flex;
gap: 0.45rem;
margin-bottom: 0.7rem;
}
.addChipsQuickBtn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.1rem;
min-height: 52px;
border-radius: 11px;
border: 1px solid #3a3a3a;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #ddd;
font-family: inherit;
cursor: pointer;
}
.addChipsQuickBtn.selected {
border-color: #d8b46a;
background: linear-gradient(180deg, #3a2f18, #241d0f);
color: #f0d9a8;
}
.addChipsQuickLabel {
font-size: 0.78rem;
font-weight: 600;
}
.addChipsQuickAmount {
font-size: 0.7rem;
color: #a8a8a8;
font-variant-numeric: tabular-nums;
}
.addChipsQuickBtn.selected .addChipsQuickAmount {
color: #d8b46a;
}
.addChipsEntry {
display: flex;
align-items: center;
gap: 0.4rem;
background: #141414;
border: 1px solid #3a3a3a;
border-radius: 11px;
padding: 0.2rem 0.8rem;
margin-bottom: 0.8rem;
}
.addChipsEntry:focus-within {
border-color: #d8b46a;
}
.addChipsCurrency {
color: #777;
font-size: 1.3rem;
}
#addChipsInput {
flex: 1;
min-width: 0;
background: none;
border: none;
outline: none;
color: #f2f2f2;
font-family: inherit;
font-size: 1.5rem;
font-weight: 500;
font-variant-numeric: tabular-nums;
padding: 0.4rem 0;
}
.addChipsActions {
display: flex;
flex-direction: column-reverse;
gap: 0.5rem;
}
.addChipsActions .holdemBtn {
width: 100%;
justify-content: center;
min-height: 48px;
}
/* ---------- Rulings reference ---------- */
.tableHintActions {
margin: 0.35rem 0 0;
text-align: center;
}
.linkBtn {
background: none;
border: none;
padding: 0;
color: #a9782f;
font-family: inherit;
font-size: 0.78rem;
text-decoration: underline;
text-underline-offset: 2px;
cursor: pointer;
}
.linkBtn:hover {
color: #d8b46a;
}
#rulingsOverlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
display: flex;
align-items: flex-end;
justify-content: center;
z-index: 97;
}
#rulingsOverlay[hidden] {
display: none;
}
#rulingsSheet {
width: 100%;
max-width: 460px;
max-height: 86vh;
overflow-y: auto;
background: linear-gradient(180deg, #242424, #1a1a1a);
border: 1px solid #3a3a3a;
border-bottom: none;
border-radius: 16px 16px 0 0;
padding: 0.35rem 1.1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
}
.rulingsHeader {
display: flex;
align-items: center;
gap: 0.6rem;
margin-bottom: 0.5rem;
}
.rulingsHeader h2 {
flex: 1;
margin: 0;
font-size: 1.05rem;
font-weight: 500;
color: #f5f5f5;
}
.rulingsIntro {
margin: 0 0 1rem;
color: #9a9a9a;
font-size: 0.8rem;
line-height: 1.5;
}
.ruling {
margin-bottom: 1rem;
padding-left: 0.7rem;
border-left: 2px solid rgba(216, 180, 106, 0.4);
}
.ruling h3 {
margin: 0 0 0.35rem;
color: #d8b46a;
font-size: 0.82rem;
font-weight: 600;
letter-spacing: 0.03em;
}
.ruling p,
.ruling li {
color: #c4c4c4;
font-size: 0.82rem;
line-height: 1.5;
}
.ruling p {
margin: 0;
}
.ruling ul {
margin: 0;
padding-left: 1.1rem;
}
.ruling li + li {
margin-top: 0.25rem;
}
.rulingsSource {
margin: 0;
padding-top: 0.6rem;
border-top: 1px solid #2c2c2c;
color: #7a7a7a;
font-size: 0.72rem;
line-height: 1.45;
}
/* ---------- Seat long-press menu ---------- */
/* Held down, before the menu opens — the only feedback that a press is being
@@ -1226,6 +1480,12 @@ body.hasActionBar #holdemSidebar {
font-variant-numeric: tabular-nums;
}
.actionRunout {
margin-left: auto;
color: #d8b46a;
font-size: 0.74rem;
}
.actionWho {
margin-left: auto;
color: #9c9c9c;
@@ -74,6 +74,16 @@ const ActionBar = {
players.textContent = `${remaining} ${remaining === 1 ? "player" : "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;
if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) {
const player = this.state.seats[index];
@@ -0,0 +1,92 @@
// "Add chips" — a rebuy, a top-up, or the first buy-in of the night.
//
// Quick amounts come off the table's starting stack, because a rebuy is almost
// always one buy-in, sometimes half or two. The keypad is there for the times it
// isn't. Recorded as cash in as well as chips, so at the end of the night the
// dealer can say what each player actually handed over.
const AddChips = {
state: null,
onConfirm: null,
seatIndex: null,
draft: "",
open(state, seatIndex, onConfirm) {
this.state = state;
this.seatIndex = seatIndex;
this.onConfirm = onConfirm;
this.draft = String(this.baseAmount());
this.render();
document.getElementById("addChipsOverlay").hidden = false;
},
close() {
document.getElementById("addChipsOverlay").hidden = true;
},
// The table's starting stack if there is one, otherwise something sane to nudge
// rather than an empty box.
baseAmount() {
const configured = Number(this.state.defaultStack);
if (!Number.isNaN(configured) && configured > 0) return configured;
const bb = Number(this.state.bigBlind) || 0;
return bb > 0 ? bb * 100 : 100;
},
quickAmounts() {
const base = this.baseAmount();
return [
{ label: "½ buy-in", value: Math.round(base / 2 * 100) / 100 },
{ label: "Buy-in", value: base },
{ label: "2 buy-ins", value: base * 2 },
];
},
render() {
const player = this.state.seats[this.seatIndex];
document.getElementById("addChipsTitle").textContent =
`Add chips — ${HoldemState.displayName(player, this.seatIndex)}`;
const stack = Betting.stackOf(this.state, player);
const boughtIn = Number(player.buyIns) || 0;
document.getElementById("addChipsSubtitle").textContent = boughtIn > 0
? `${Money.format(stack)} in front · ${Money.format(boughtIn)} in so far`
: `${Money.format(stack)} in front`;
const quick = document.getElementById("addChipsQuick");
quick.innerHTML = "";
this.quickAmounts().forEach(option => {
const btn = document.createElement("button");
btn.type = "button";
btn.className = "addChipsQuickBtn";
btn.classList.toggle("selected", Number(this.draft) === option.value);
const label = document.createElement("span");
label.className = "addChipsQuickLabel";
label.textContent = option.label;
const amount = document.createElement("span");
amount.className = "addChipsQuickAmount";
amount.textContent = Money.format(option.value);
btn.appendChild(label);
btn.appendChild(amount);
btn.addEventListener("click", () => {
this.draft = String(option.value);
this.render();
});
quick.appendChild(btn);
});
const input = document.getElementById("addChipsInput");
input.value = this.draft;
},
confirm() {
const amount = Math.max(0, Number(document.getElementById("addChipsInput").value) || 0);
this.close();
if (amount > 0) this.onConfirm(this.seatIndex, Math.round(amount * 100) / 100);
},
};
if (typeof module !== "undefined" && module.exports) {
module.exports = AddChips;
}
@@ -70,7 +70,19 @@ const Betting = {
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) >= 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;
},
@@ -156,7 +168,7 @@ const Betting = {
},
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 ----
@@ -184,8 +196,13 @@ 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)
// 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;
},
@@ -193,6 +210,24 @@ const Betting = {
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
// blind, which is where a dealer expects to be looking when the cards go out.
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.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.
@@ -35,9 +35,19 @@ const HandHistoryExport = {
if (standings.length > 0) {
rows.push([]);
rows.push(["Session Standings"]);
rows.push(["Player", "Paid In", "Won", "Net"]);
// Bought in and chips now are what a cash-out is settled from; paid in and
// won describe the hands. Both belong in the file the dealer hands over.
rows.push(["Player", "Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down"]);
standings.forEach(row => {
rows.push([row.name, Money.format(row.in), Money.format(row.won), Money.formatSigned(row.net)]);
rows.push([
row.name,
Money.format(row.in),
Money.format(row.won),
Money.formatSigned(row.net),
Money.format(row.boughtIn || 0),
Money.format(row.chipsNow || 0),
Money.formatSigned(row.cashPosition || 0),
]);
});
}
@@ -57,6 +57,15 @@ document.addEventListener("DOMContentLoaded", () => {
},
makeDealer: index => seatAction(index, `Button on ${nameOf(index)}.`, () => { state.dealerSeatIndex = index; }),
editSeat: index => Render.toggleSeatEditor(index),
addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
seatAction(seatIndex, `${nameOf(seatIndex)} added ${Money.format(amount)}.`, player => {
// Chips in front and cash in are different questions — the stack moves
// every hand, what they've handed over only moves when they buy.
player.stack = Math.round((Betting.stackOf(state, player) + amount) * 100) / 100;
player.buyIns = Math.round(((Number(player.buyIns) || 0) + amount) * 100) / 100;
});
}),
};
Render.mount(state, hooks);
@@ -260,6 +269,26 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("closeSeatEditorBtn").addEventListener("click", () => Render.closeSeatEditor());
document.getElementById("addChipsConfirmBtn").addEventListener("click", () => AddChips.confirm());
document.getElementById("addChipsCancelBtn").addEventListener("click", () => AddChips.close());
document.getElementById("addChipsOverlay").addEventListener("click", e => {
if (e.target.id === "addChipsOverlay") AddChips.close();
});
document.getElementById("addChipsInput").addEventListener("keydown", e => {
if (e.key === "Enter") AddChips.confirm();
});
const rulingsOverlay = document.getElementById("rulingsOverlay");
const closeRulings = () => { rulingsOverlay.hidden = true; };
document.getElementById("openRulingsBtn").addEventListener("click", () => { rulingsOverlay.hidden = false; });
document.getElementById("closeRulingsBtn").addEventListener("click", closeRulings);
rulingsOverlay.addEventListener("click", e => {
if (e.target === rulingsOverlay) closeRulings();
});
document.addEventListener("keydown", e => {
if (e.key === "Escape" && !rulingsOverlay.hidden) closeRulings();
});
document.getElementById("seatMenuCancelBtn").addEventListener("click", () => SeatMenu.close());
document.getElementById("seatMenuOverlay").addEventListener("click", e => {
if (e.target.id === "seatMenuOverlay") SeatMenu.close();
@@ -491,6 +491,13 @@ const Render = {
});
});
// What they've handed over tonight, shown next to what's in front of them so a
// cash-out is one subtraction rather than a memory test.
const boughtIn = Number(player.buyIns) || 0;
const boughtInEl = form.querySelector(".seatBuyIns");
boughtInEl.textContent = boughtIn > 0 ? `Bought in ${Money.format(boughtIn)}` : "";
boughtInEl.hidden = boughtIn <= 0;
const stackInput = form.querySelector(".stackInput");
stackInput.value = player.stack ?? "";
stackInput.addEventListener("input", () => {
@@ -32,6 +32,10 @@ const SeatMenu = {
actions.push({ label: "Un-fold", run: () => this.hooks.unfold(index) });
}
// The most common interruption in a cash game, and until now it meant opening
// the editor and retyping a stack from memory.
actions.push({ label: "Add chips…", run: () => this.hooks.addChips(index) });
actions.push({
label: dealtIn ? "Sit out" : "Deal in",
run: () => this.hooks.toggleDealtIn(index),
@@ -35,7 +35,7 @@ const Shortcuts = {
// An overlay owns the keyboard while it's up: the confirm dialog already binds
// Enter and Escape, and the card picker and seat menu are modal.
overlayOpen() {
return ["confirmOverlay", "potWizardOverlay", "cardPickerOverlay", "seatMenuOverlay"]
return ["confirmOverlay", "potWizardOverlay", "cardPickerOverlay", "seatMenuOverlay", "rulingsOverlay", "addChipsOverlay"]
.some(id => {
const el = document.getElementById(id);
return el && !el.hidden;
@@ -31,6 +31,8 @@ const HoldemState = {
stack: null,
committed: 0,
hasActed: false,
// Cash handed over tonight, which only moves when they buy chips.
buyIns: 0,
};
},
@@ -79,6 +81,7 @@ const HoldemState = {
if (player.stack === undefined) player.stack = null;
if (player.committed === undefined) player.committed = 0;
if (player.hasActed === undefined) player.hasActed = false;
if (player.buyIns === undefined) player.buyIns = 0;
});
return parsed;
@@ -236,13 +239,32 @@ const HoldemState = {
});
});
// Cash bought in and chips currently in front don't come from the hand log at
// all, so they're read off the seat rather than summed — a rebuy isn't a hand.
const seatById = new Map(state.seats.map(p => [p.id, p]));
return Array.from(totals.values())
.map(t => ({
...t,
in: Math.round(t.in * 100) / 100,
won: Math.round(t.won * 100) / 100,
net: Math.round((t.won - t.in) * 100) / 100,
}))
.map(t => {
const seat = seatById.get(t.id);
const boughtIn = seat ? Math.round((Number(seat.buyIns) || 0) * 100) / 100 : 0;
const chipsNow = seat ? Math.round((Number(seat.stack) || 0) * 100) / 100 : 0;
// Chips already pushed into the hand in progress are still this
// player's money until it's won. Leaving them out makes the cash
// position disagree with the hand log for as long as a hand is open —
// and blinds are posted the instant the previous one settles, so that
// window is almost always.
const inFrontOfThem = seat ? (Number(seat.amountOverride) || 0) + (Number(seat.committed) || 0) : 0;
return {
...t,
in: Math.round(t.in * 100) / 100,
won: Math.round(t.won * 100) / 100,
net: Math.round((t.won - t.in) * 100) / 100,
boughtIn,
chipsNow,
cashPosition: Math.round((chipsNow + inFrontOfThem - boughtIn) * 100) / 100,
};
})
.sort((a, b) => b.net - a.net);
},
};