From 7c647492676e021eb5fe018beff1b54d61c162ca Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 00:07:37 -0600 Subject: [PATCH 1/9] Let Quick Split handle the pot it used to send people away for Quick Split answered the easy half of the question and printed a footnote for the hard half: if somebody was all-in for less, go set up the full table. That's the moment a dealer least wants to be setting up a table. A second mode takes what each player put in and lists the main pot and each side pot with who can win it, plus anything uncalled that goes back. Rows can be marked folded, so dead money lands in the right pot without its owner being able to win it. The running total is shown so the breakdown can be checked against the chips actually on the table. It reuses the same pot maths as the full table rather than growing a second implementation, which means the two screens can't drift apart and Quick Split inherits the uncalled-bet and everyone-folded handling that was fixed there. Also fixes hiding: these sections carry a `display` declaration, which outranks the browser's own [hidden] rule, so toggling `hidden` on them did nothing and both modes' controls showed at once. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- .../Casino/TexasHoldem/QuickSplit.cshtml | 47 ++++- .../wwwroot/css/casino/quicksplit.css | 171 ++++++++++++++++++ .../wwwroot/js/casino/holdem/quickSplit.js | 121 +++++++++++++ 3 files changed, 333 insertions(+), 6 deletions(-) diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml index 0dfc7a6..0fb0660 100644 --- a/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml @@ -11,6 +11,11 @@

Chop a pot without setting up a table.

+
+ + +
+
Each player gets
$0.00
@@ -18,7 +23,18 @@
-
+ + + + +
$ @@ -28,12 +44,12 @@
-
+
Split between
-
+
Smallest chip in play
@@ -43,12 +59,30 @@ Full dealer table → -

- Uneven pots — someone all-in for less — need the side-pot walkthrough on the - full table. +

+ If somebody was all-in for less than the others, switch to + Someone's all-in above. +

+ + + + @section Styles { } @@ -56,5 +90,6 @@ @section Scripts { + } diff --git a/JoshHeaps.Net/wwwroot/css/casino/quicksplit.css b/JoshHeaps.Net/wwwroot/css/casino/quicksplit.css index 0f15fd8..157f9c9 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/quicksplit.css +++ b/JoshHeaps.Net/wwwroot/css/casino/quicksplit.css @@ -45,6 +45,171 @@ body { font-size: 0.85rem; } +/* ---------- Mode toggle ---------- */ + +#modeToggle { + margin-bottom: 1.1rem; +} + +/* ---------- Uneven amounts ---------- */ + +#unevenRows { + display: flex; + flex-direction: column; + gap: 0.45rem; + margin-bottom: 0.6rem; +} + +.unevenRow { + display: flex; + align-items: center; + gap: 0.45rem; +} + +.unevenRowLabel { + flex: none; + width: 2rem; + color: #8f8f8f; + font-size: 0.8rem; + font-weight: 600; +} + +.unevenAmountWrap { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 0.3rem; + background: #141414; + border: 1px solid #3a3a3a; + border-radius: 10px; + padding: 0 0.6rem; +} + +.unevenAmountWrap:focus-within { + border-color: #d8b46a; +} + +.unevenCurrency { + color: #777; +} + +.unevenAmount { + flex: 1; + min-width: 0; + background: none; + border: none; + outline: none; + color: #f2f2f2; + font-family: inherit; + font-size: 16px; + font-variant-numeric: tabular-nums; + padding: 0.6rem 0; +} + +.unevenFoldBtn { + flex: none; + min-height: 44px; + padding: 0 0.6rem; + border-radius: 9px; + border: 1px solid #3a3a3a; + background: #232323; + color: #8f8f8f; + font-family: inherit; + font-size: 0.72rem; + cursor: pointer; +} + +.unevenFoldBtn.selected { + background: linear-gradient(180deg, #3d1f1f, #2c1515); + border-color: #5b2626; + color: #ffd6d6; +} + +.unevenRemoveBtn { + flex: none; + width: 30px; + height: 44px; + border: none; + background: none; + color: #6a6a6a; + font-size: 0.85rem; + cursor: pointer; +} + +.unevenRemoveBtn:disabled { + opacity: 0.3; + cursor: not-allowed; +} + +#unevenResult { + background: linear-gradient(160deg, #1f2a21, #161c17); + border: 1px solid #2c3a2f; + border-radius: 14px; + padding: 0.9rem 1rem; + margin-bottom: 1.5rem; +} + +#unevenResult[hidden] { + display: none; +} + +.unevenPot { + display: flex; + align-items: baseline; + gap: 0.6rem; + padding: 0.3rem 0; + font-size: 0.85rem; +} + +.unevenPot + .unevenPot { + border-top: 1px solid rgba(255, 255, 255, 0.06); +} + +.unevenPotLabel { + flex: none; + color: #d8b46a; + font-weight: 600; +} + +.unevenPotAmount { + flex: none; + color: #f2f2f2; + font-weight: 600; + font-variant-numeric: tabular-nums; +} + +.unevenPotWho { + flex: 1; + min-width: 0; + text-align: right; + color: #9a9a9a; + font-size: 0.78rem; +} + +.unevenPotReturn .unevenPotLabel, +.unevenPotReturn .unevenPotAmount { + color: #9a9a9a; + font-weight: 500; +} + +.unevenPotTotal { + margin-top: 0.15rem; + border-top: 1px solid rgba(255, 255, 255, 0.14) !important; +} + +.unevenPotTotal .unevenPotLabel { + color: #8f9d92; + font-weight: 500; +} + +.unevenEmpty { + margin: 0; + color: #8f9d92; + font-size: 0.82rem; + line-height: 1.45; +} + /* ---------- The answer ---------- */ #splitResult { @@ -104,6 +269,12 @@ body { margin-bottom: 1.35rem; } +/* A `display` declaration outranks the browser's own [hidden] rule, so anything + given one has to opt back out explicitly or toggling `hidden` does nothing. */ +[hidden] { + display: none !important; +} + .splitLabel { display: block; margin-bottom: 0.5rem; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/quickSplit.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/quickSplit.js index aa4c164..2cf84f6 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/quickSplit.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/quickSplit.js @@ -15,12 +15,28 @@ const QuickSplit = { ways: 2, chip: 1, + // "even" is the common case — one pot, everyone matched. "uneven" is the one this + // screen used to send people back to the full table for: somebody all-in for less, + // so the money splits into a main pot and side pots. + mode: "even", + entries: [], + mount() { this.potInput = document.getElementById("potInput"); this.clearPotBtn = document.getElementById("clearPotBtn"); + this.entries = [{ amount: "", folded: false }, { amount: "", folded: false }, { amount: "", folded: false }]; this.renderWayChoices(); this.renderChipChoices(); + this.renderUnevenRows(); + + document.getElementById("modeEvenBtn").addEventListener("click", () => this.setMode("even")); + document.getElementById("modeUnevenBtn").addEventListener("click", () => this.setMode("uneven")); + document.getElementById("addPlayerBtn").addEventListener("click", () => { + this.entries.push({ amount: "", folded: false }); + this.renderUnevenRows(); + this.update(); + }); this.potInput.addEventListener("input", () => { this.sanitizePotInput(); @@ -127,7 +143,112 @@ const QuickSplit = { }); }, + setMode(mode) { + this.mode = mode; + document.getElementById("modeEvenBtn").classList.toggle("selected", mode === "even"); + document.getElementById("modeUnevenBtn").classList.toggle("selected", mode === "uneven"); + + document.querySelectorAll(".evenOnly").forEach(el => { el.hidden = mode !== "even"; }); + document.querySelectorAll(".unevenOnly").forEach(el => { el.hidden = mode !== "uneven"; }); + document.getElementById("splitResult").hidden = mode !== "even"; + document.getElementById("unevenResult").hidden = mode !== "uneven"; + + this.update(); + }, + + renderUnevenRows() { + const host = document.getElementById("unevenRows"); + const template = document.getElementById("unevenRowTemplate"); + host.innerHTML = ""; + + this.entries.forEach((entry, index) => { + const row = template.content.firstElementChild.cloneNode(true); + row.querySelector(".unevenRowLabel").textContent = `P${index + 1}`; + + const amount = row.querySelector(".unevenAmount"); + amount.value = entry.amount; + amount.setAttribute("aria-label", `Player ${index + 1} amount`); + amount.addEventListener("input", () => { + entry.amount = amount.value.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1"); + if (entry.amount !== amount.value) amount.value = entry.amount; + this.update(); + }); + + // Folded money stays in the pot as dead money, but its owner can't win any + // of it — which is exactly the distinction that makes side pots tricky. + const fold = row.querySelector(".unevenFoldBtn"); + fold.classList.toggle("selected", entry.folded); + fold.setAttribute("aria-pressed", String(entry.folded)); + fold.addEventListener("click", () => { + entry.folded = !entry.folded; + this.renderUnevenRows(); + this.update(); + }); + + const remove = row.querySelector(".unevenRemoveBtn"); + remove.disabled = this.entries.length <= 2; + remove.addEventListener("click", () => { + this.entries.splice(index, 1); + this.renderUnevenRows(); + this.update(); + }); + + host.appendChild(row); + }); + }, + + updateUneven() { + const players = this.entries + .map((entry, index) => ({ id: `P${index + 1}`, contribution: Number(entry.amount) || 0, folded: entry.folded })) + .filter(p => p.contribution > 0); + + const pots = PotCalculator.layers(players); + const returns = PotCalculator.uncalledReturns(players); + + const host = document.getElementById("unevenPots"); + const empty = document.getElementById("unevenEmpty"); + host.innerHTML = ""; + + if (pots.length === 0 && returns.length === 0) { + empty.hidden = false; + empty.textContent = players.length === 0 + ? "Enter what each player put in." + : "Nobody left who can win — un-fold whoever should take it."; + return; + } + empty.hidden = true; + + const addRow = (label, amount, who, className) => { + const row = document.createElement("div"); + row.className = `unevenPot ${className || ""}`; + const name = document.createElement("span"); + name.className = "unevenPotLabel"; + name.textContent = label; + const value = document.createElement("span"); + value.className = "unevenPotAmount"; + value.textContent = Money.format(amount); + const eligible = document.createElement("span"); + eligible.className = "unevenPotWho"; + eligible.textContent = who; + row.appendChild(name); + row.appendChild(value); + row.appendChild(eligible); + host.appendChild(row); + }; + + pots.forEach(pot => addRow(pot.label, pot.amount, pot.eligible.join(", "))); + returns.forEach(ret => addRow("Uncalled", ret.amount, `back to ${ret.id}`, "unevenPotReturn")); + + const total = players.reduce((sum, p) => sum + p.contribution, 0); + addRow("On the table", Math.round(total * 100) / 100, "", "unevenPotTotal"); + }, + update() { + if (this.mode === "uneven") { + this.updateUneven(); + return; + } + const potValue = Number(this.potInput.value); const pot = Number.isNaN(potValue) ? 0 : potValue; this.clearPotBtn.hidden = this.potInput.value === ""; -- 2.43.0 From 38488f2978bac81658bc6f7a09158b4e98601c80 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 00:22:14 -0600 Subject: [PATCH 2/9] Give the pot wizard the same tap targets as everything else MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured the screen where the money actually gets decided and found the smallest targets in the tool: 37px winner rows, 13px checkboxes, a 36px confirm. Every other surface has been held to 44px and up for several rounds — the one place a mis-tap pays the wrong player was the one place that hadn't been. Winner rows are now 56px and up, with a 22px box and a green fill when chosen, because a dealer confirms this with players watching and a small checkbox is easy to misread across a table. The row already was the tap target; now it looks like one. Each row also splits into name, what they can win, and their cards as separate pieces rather than one run-on sentence, so the hand can be read at a glance. The confirm takes the full width, and on a phone the summary's buttons stack with the one that ends the hand furthest from a stray thumb. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/wwwroot/css/casino/holdem.css | 94 ++++++++++++++++++- .../wwwroot/js/casino/holdem/potWizard.js | 45 ++++++++- 2 files changed, 133 insertions(+), 6 deletions(-) diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index 8d9ca2e..ea14ea5 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -1962,20 +1962,75 @@ 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.5rem; + gap: 0.7rem; + min-height: 56px; background: #141414; border: 1px solid #333; - border-radius: 6px; - padding: 0.5rem 0.7rem; + border-radius: 10px; + padding: 0.6rem 0.8rem; cursor: pointer; - font-size: 0.85rem; + font-size: 0.95rem; + transition: border-color 0.12s ease, background 0.12s ease; } .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 { @@ -1996,6 +2051,37 @@ 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; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js index ab36c9a..16918d3 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js @@ -18,6 +18,14 @@ 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), @@ -105,13 +113,46 @@ 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; - const text = document.createElement("span"); - text.textContent = `${this.playerLabelById.get(c.id)} — up to ${Money.format(c.remaining)}`; + // 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); 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); -- 2.43.0 From 6b3a7dc09d00ed6453dbb3b55f44f98ea9543a15 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 00:38:29 -0600 Subject: [PATCH 3/9] Bring every phone target up to size, not just the ones I happened to check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Last round I measured one screen and found the smallest targets in the tool were on it. So I measured all of them: sixty-four controls at phone width, flagging anything under 44px. Twenty-three came back. The sidebar had never had the touch pass the seat form got — its inputs, selects and small buttons sat at 26 to 30px, and the buttons that delete a chip colour or a custom field were 28px wide, which is a bad size for something destructive. The rulings link was a 14px-tall underlined word. The seat form itself had drifted: its mobile sizing rule named five fields, and the stack field added two rounds later wasn't one of them, so it sat at 28px beside its 44px neighbours. Both that rule and the new sidebar one are now written against the form and the panel rather than a list of field names, so the next field added inherits the size instead of waiting to be noticed. The dealer button keeps its 22px disc — it's a chip sitting on a seat, and drawing it thumb-sized would swallow the puck — but takes taps through an invisible 44px pad. Verified the seat underneath still gets its own taps. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/wwwroot/css/casino/holdem.css | 80 +++++++++++++++++++-- 1 file changed, 75 insertions(+), 5 deletions(-) diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index 8d9ca2e..f90c42a 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -2140,11 +2140,10 @@ body.hasActionBar #toastHost { whole phone width to work with, so everything grows to thumb size — and every field goes to 16px, below which iOS zooms the page on focus and the dealer has to pinch back out mid-hand. */ - .seatEditorForm .seatNameInput, - .seatEditorForm .amountOverrideInput, - .seatEditorForm .chipCountInput, - .seatEditorForm .customFieldLabel, - .seatEditorForm .customFieldValue { + /* Every text or number field in the form, rather than a list of the ones that + existed when this was written. The stack field was added later and sat at 28px + next to its 44px neighbours precisely because it wasn't on that list. */ + .seatEditorForm input:not([type="checkbox"]):not([type="color"]) { font-size: 16px; min-height: 44px; padding: 0.4rem 0.6rem; @@ -2208,6 +2207,77 @@ body.hasActionBar #toastHost { margin-top: 0.4rem; } + /* The sidebar's own controls never got the touch pass the seat form did — these + are settings a dealer changes at the table, on the same phone. Written against + the panel rather than each field, so a setting added later inherits it. */ + .panelBody input:not([type="checkbox"]):not([type="color"]), + .subPanelBody input:not([type="checkbox"]):not([type="color"]), + .panelBody select { + font-size: 16px; + min-height: 44px; + padding: 0.4rem 0.6rem; + } + + .panelBody .holdemBtnSmall, + .handHistoryActions .holdemBtn { + min-height: 44px; + } + + /* Destructive and tiny is the worst combination: these delete a chip colour or a + custom field, and were 28px wide. */ + .removeChipColorBtn, + .removeFieldBtn { + min-width: 44px; + min-height: 44px; + } + + /* flex-shrink would take a stated width straight back off again in a row this + narrow, which is why it stayed 32px wide with the height applied. */ + .chipColorInput { + flex: none; + width: 44px; + height: 44px; + } + + /* A bare checkbox can't grow into a target, but the row it sits in can — the + label is what a tap actually lands on. */ + .panelBody .fieldRow { + min-height: 44px; + } + + /* A bare checkbox with no label box around it to grow. */ + #dedicatedDealerToggle { + width: 24px; + height: 24px; + } + + /* An underlined word is a 14px-tall target; give it room without making it look + like a button. */ + .linkBtn { + display: inline-flex; + align-items: center; + min-height: 44px; + padding: 0 0.4rem; + } + + /* The dealer button has to stay a 22px disc — it's a chip sitting on a seat, and + drawing it thumb-sized would swallow the puck. So the disc keeps its size and + an invisible pad around it takes the tap. It reaches outward, away from the + puck's own tap target, so pressing the seat still opens the seat. */ + .dealerChip::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 44px; + height: 44px; + transform: translate(-50%, -50%); + } + + .dealerChip { + overflow: visible; + } + .toastAction { min-height: 44px; } -- 2.43.0 From d0f75d0c1453c3b8d2973125c3429297b9448e5d Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 01:06:20 -0600 Subject: [PATCH 4/9] Lift the faint greys to something readable in a dim room MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a dark interface used in poker rooms and kitchens with the lights down, and I'd been generous with grey-on-black. Measured every piece of text against what's actually painted behind it: thirteen declarations came in under 4.5:1. The worst was the empty card slot's "+" at 2.63:1 — an affordance telling the dealer the slot is tappable, and the least legible thing on the screen. Then a run of #777 labels around 3.9 to 4.1: the standings header, the time on a logged hand, the currency mark in the add-chips field. The rulings source line sat at 3.62 and the rulings link itself at 4.39. All of them move up just far enough to clear 4.5 rather than being flattened to white, so the hierarchy between a heading, a value and a hint survives. Worth recording what the measurement can't see: it reads background-color, so text on a gradient — the pot badge, the dealer button, the blind badges — comes back as a false result in both directions, and those were checked by eye instead. The felt watermark is deliberately at 6% and stays that way. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/wwwroot/css/casino/holdem.css | 28 ++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index 8d9ca2e..3574f49 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -146,7 +146,7 @@ body { } .tableHint { - color: #888; + color: #949494; font-size: 0.8rem; text-align: center; margin-top: 1rem; @@ -357,7 +357,7 @@ body { } .seatEmptyLabel { - color: #6d6d6d; + color: #8a8a8a; font-size: 0.6rem; letter-spacing: 0.08em; text-transform: uppercase; @@ -373,7 +373,7 @@ body { top: 2px; left: 6px; font-size: 0.58rem; - color: #777; + color: #8a8a8a; } .seatPuckName { @@ -578,7 +578,7 @@ body { .cardSlot.cardSlotEmpty { background: #191919; - color: #5c5c5c; + color: #8a8a8a; border-style: dashed; border-color: #3f3f3f; box-shadow: none; @@ -694,7 +694,7 @@ body { } .addChipsCurrency { - color: #777; + color: #8a8a8a; font-size: 1.3rem; } @@ -735,7 +735,7 @@ body { background: none; border: none; padding: 0; - color: #a9782f; + color: #d8b46a; font-family: inherit; font-size: 0.78rem; text-decoration: underline; @@ -834,7 +834,7 @@ body { margin: 0; padding-top: 0.6rem; border-top: 1px solid #2c2c2c; - color: #7a7a7a; + color: #8a8a8a; font-size: 0.72rem; line-height: 1.45; } @@ -1213,7 +1213,7 @@ body { .subPanel summary { cursor: pointer; - color: #8f8f8f; + color: #9a9a9a; font-size: 0.78rem; } @@ -1249,7 +1249,7 @@ body { .fieldHint { font-size: 0.75rem; - color: #888; + color: #949494; line-height: 1.4; margin: 0; } @@ -1306,7 +1306,7 @@ body { } .standingsHeader { - color: #777; + color: #8a8a8a; font-size: 0.7rem; letter-spacing: 0.04em; text-transform: uppercase; @@ -1374,7 +1374,7 @@ body { .handHistoryTime { margin-left: auto; - color: #777; + color: #8a8a8a; font-size: 0.72rem; } @@ -1494,7 +1494,7 @@ body.hasActionBar #holdemSidebar { flex: none; min-width: 3.1rem; text-align: right; - color: #7d7d7d; + color: #949494; font-variant-numeric: tabular-nums; transition: color 0.3s ease; } @@ -1732,7 +1732,7 @@ body.hasActionBar #holdemSidebar { .seatPuckStack { font-size: 0.58rem; - color: #8f8f8f; + color: #9a9a9a; font-variant-numeric: tabular-nums; margin-top: 0.1rem; } @@ -1836,7 +1836,7 @@ body.hasActionBar #toastHost { border-radius: 50%; border: none; background: transparent; - color: #888; + color: #949494; font-size: 0.8rem; font-family: inherit; cursor: pointer; -- 2.43.0 From 89d79a0f7d66b47753bceeeb9d6e63a8fe1ce93c Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 01:22:15 -0600 Subject: [PATCH 5/9] Post the blinds to the players rather than to the chairs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- .../wwwroot/js/casino/holdem/betting.js | 34 ++++++++++++++----- .../wwwroot/js/casino/holdem/state.js | 32 +++++++++++++++-- 2 files changed, 55 insertions(+), 11 deletions(-) diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js index bb2f628..c944c2d 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js @@ -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. diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js index 67b4a4e..7bb1aa0 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js @@ -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) { -- 2.43.0 From ae591e42124be192528f1bfc3417df55457284d9 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 01:37:19 -0600 Subject: [PATCH 6/9] Stop asking all-in players to act MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/wwwroot/css/casino/holdem.css | 6 +++ .../wwwroot/js/casino/holdem/actionBar.js | 10 +++++ .../wwwroot/js/casino/holdem/betting.js | 45 ++++++++++++++++--- 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index 8d9ca2e..f7bfe3a 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -1480,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; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js index d440315..3673f21 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js @@ -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]; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js index bb2f628..97b34fd 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/betting.js @@ -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. -- 2.43.0 From 18bb23ad6d000260b2b0ec076ded3f4c71777e55 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 01:49:22 -0600 Subject: [PATCH 7/9] Pass the button to the next player, not the next chair MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/wwwroot/js/casino/holdem/state.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js index 67b4a4e..666af2f 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js @@ -159,7 +159,20 @@ const HoldemState = { 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) { + 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); }, -- 2.43.0 From cc1b5011a1352d1ea8c49386b24144514747eb81 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 02:05:04 -0600 Subject: [PATCH 8/9] Give the odd chip to the player the rule says gets it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- .../wwwroot/js/casino/holdem/potWizard.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js index ab36c9a..d9307b4 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/potWizard.js @@ -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(); -- 2.43.0 From 4f72dda6fa1318f93adddec0a29d0c6ac256ae12 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Tue, 1 Sep 2026 02:33:03 -0600 Subject: [PATCH 9/9] Ask before a smaller table deletes the players sitting in it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Typing a smaller number into "Seats at table" silently threw away every seat past the new count. Nine players down to six removed three of them — names, stacks and buy-ins gone, no confirmation, no way back. It also quietly falsified the settle-up. Those players keep their rows in the session standings, because the hand log remembers who won what, but the seat they were read from no longer exists — so a player who had just won a pot showed as having bought in for nothing and holding nothing. Shrinking now says who it is about to remove and asks, and either way the change is undoable like the rest of the table's actions. Growing the table, and shrinking onto chairs nobody is sitting in, still happen without a word. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- .../wwwroot/js/casino/holdem/main.js | 36 ++++++++++++++++--- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js index f6915ae..30137cc 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js @@ -183,10 +183,38 @@ document.addEventListener("DOMContentLoaded", () => { }); }; - document.getElementById("seatCountInput").addEventListener("change", e => { - HoldemState.resizeSeats(state, Number(e.target.value)); - hooks.save(state); - Render.renderAll(); + // A seat that has anything of a player in it — a name, chips, cash in, or a hand + // in progress. Shrinking the table throws these away permanently, and it also + // orphans them in the session standings, where they keep their winnings but lose + // the stack and buy-in the settle-up is worked out from. + const seatHasSomeoneIn = player => + !!(player.name && player.name.trim()) || + player.inHand || + (Number(player.stack) || 0) > 0 || + (Number(player.buyIns) || 0) > 0 || + (Number(player.amountOverride) || 0) > 0; + + document.getElementById("seatCountInput").addEventListener("change", async e => { + const requested = Math.max(2, Math.min(24, Math.floor(Number(e.target.value)) || state.seatCount)); + const losing = state.seats.slice(requested).filter(seatHasSomeoneIn); + + if (losing.length > 0) { + const who = losing + .map((p, i) => HoldemState.displayName(p, requested + state.seats.slice(requested).indexOf(p))) + .join(", "); + const ok = await Dialog.confirm({ + title: `Remove ${losing.length} ${losing.length === 1 ? "player" : "players"}?`, + message: `Going down to ${requested} seats takes ${who} off the table along with their chips and buy-ins, and their session totals stop adding up.`, + confirmLabel: "Remove them", + danger: true, + }); + if (!ok) { + e.target.value = state.seatCount; + return; + } + } + + runUndoable(`Table set to ${requested} seats.`, () => HoldemState.resizeSeats(state, requested)); }); document.getElementById("directionSelect").addEventListener("change", e => { -- 2.43.0