From 870dee4a291f282a747325665118393e88a19de7 Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Mon, 31 Aug 2026 17:53:18 -0600 Subject: [PATCH 1/3] Add a phone-first Quick Split screen for chopping a pot mid-hand MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dealer table is a two-handed, big-screen tool. A dealer standing over a chopped pot with a phone in one hand and players waiting needs an answer in seconds, not a table to set up first โ€” so /casino/texasholdem/quicksplit is the whole job on one screen: type the pot, tap how many ways, tap the smallest chip in play, read the payout. Nothing to configure, nothing saved. It splits in whole chips rather than raw arithmetic, because a dealer can't push half a chip across the felt: everyone gets the same stack and the remainder is called out as odd chips going one at a time to the first players left of the button. A pot that doesn't divide into the chosen denomination says so instead of quietly hiding the difference. Money formatting moves out of render.js into its own file so this screen can use it without pulling in the whole seat/chip UI. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/Pages/Casino.cshtml | 9 + JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml | 2 + .../Casino/TexasHoldem/QuickSplit.cshtml | 60 ++++ .../Casino/TexasHoldem/QuickSplit.cshtml.cs | 11 + .../wwwroot/css/casino/quicksplit.css | 301 ++++++++++++++++++ .../wwwroot/js/casino/holdem/money.js | 22 ++ .../wwwroot/js/casino/holdem/quickSplit.js | 166 ++++++++++ .../wwwroot/js/casino/holdem/render.js | 18 +- .../js/casino/holdem/splitCalculator.js | 53 +++ 9 files changed, 625 insertions(+), 17 deletions(-) create mode 100644 JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml create mode 100644 JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml.cs create mode 100644 JoshHeaps.Net/wwwroot/css/casino/quicksplit.css create mode 100644 JoshHeaps.Net/wwwroot/js/casino/holdem/money.js create mode 100644 JoshHeaps.Net/wwwroot/js/casino/holdem/quickSplit.js create mode 100644 JoshHeaps.Net/wwwroot/js/casino/holdem/splitCalculator.js diff --git a/JoshHeaps.Net/Pages/Casino.cshtml b/JoshHeaps.Net/Pages/Casino.cshtml index 35326d0..ac47a88 100644 --- a/JoshHeaps.Net/Pages/Casino.cshtml +++ b/JoshHeaps.Net/Pages/Casino.cshtml @@ -19,6 +19,15 @@ + +
โž—
+
Quick Split
+
+ Chop a pot in seconds on your phone โ€” no table setup, just the amount, + how many players are splitting it, and the smallest chip in play. +
+
+
๐ŸŽฒ
Craps
diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml index fe9b55e..b532553 100644 --- a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml @@ -13,6 +13,7 @@ Hand #1
+ Quick Split @@ -220,6 +221,7 @@ } @section Scripts { + diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml new file mode 100644 index 0000000..0dfc7a6 --- /dev/null +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml @@ -0,0 +1,60 @@ +@page +@model JoshHeaps.Net.Pages.Casino.TexasHoldem.QuickSplitModel +@{ + ViewData["Title"] = "Quick Split"; + Layout = "_Layout"; +} + +
+
+

Quick Split

+

Chop a pot without setting up a table.

+
+ +
+
Each player gets
+
$0.00
+ + +
+ +
+ +
+ $ + + +
+
+ +
+ Split between +
+
+ +
+ Smallest chip in play +
+
+ + + +

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

+
+ +@section Styles { + +} + +@section Scripts { + + + +} diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml.cs b/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml.cs new file mode 100644 index 0000000..fc9080a --- /dev/null +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem/QuickSplit.cshtml.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc.RazorPages; + +namespace JoshHeaps.Net.Pages.Casino.TexasHoldem +{ + public class QuickSplitModel : PageModel + { + public void OnGet() + { + } + } +} diff --git a/JoshHeaps.Net/wwwroot/css/casino/quicksplit.css b/JoshHeaps.Net/wwwroot/css/casino/quicksplit.css new file mode 100644 index 0000000..0f15fd8 --- /dev/null +++ b/JoshHeaps.Net/wwwroot/css/casino/quicksplit.css @@ -0,0 +1,301 @@ +/* Quick Split is phone-first: sized for one thumb at the table, then allowed to + breathe on a laptop. Palette matches the dealer table so the two read as one tool. */ + +body { + background: radial-gradient(ellipse at 50% -10%, #1c1c1c 0%, #101010 55%, #0a0a0a 100%); + min-height: 100vh; + margin: 0; + color: #e6e6e6; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +#splitApp { + max-width: 480px; + margin: 0 auto; + padding: 1.25rem 1rem 3rem; +} + +/* ---------- Header ---------- */ + +#splitHeader h1 { + margin: 0; + font-size: 1.5rem; + font-weight: 300; + letter-spacing: 0.04em; + color: #f5f5f5; + position: relative; + display: inline-block; + padding-bottom: 0.35rem; +} + +#splitHeader h1::after { + content: ""; + position: absolute; + left: 0; + bottom: 0; + width: 36px; + height: 3px; + border-radius: 2px; + background: linear-gradient(90deg, #d8b46a, #a9782f); +} + +.splitSubtitle { + margin: 0.35rem 0 1.25rem; + color: #8f8f8f; + font-size: 0.85rem; +} + +/* ---------- The answer ---------- */ + +#splitResult { + background: linear-gradient(160deg, #1f2a21, #161c17); + border: 1px solid #2c3a2f; + border-radius: 14px; + padding: 1.4rem 1.1rem; + text-align: center; + margin-bottom: 1.5rem; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +#splitResult.hasAmount { + border-color: #3c6b46; + box-shadow: 0 0 0 1px rgba(78, 201, 122, 0.08), 0 6px 20px rgba(0, 0, 0, 0.35); +} + +#splitResultLabel { + color: #8f9d92; + font-size: 0.78rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +#splitResultAmount { + margin-top: 0.35rem; + font-size: clamp(2.6rem, 14vw, 3.4rem); + font-weight: 600; + line-height: 1.05; + color: #f2f2f2; + font-variant-numeric: tabular-nums; + letter-spacing: -0.01em; +} + +#splitResult.hasAmount #splitResultAmount { + color: #7ee2a0; +} + +#splitOddNote { + margin-top: 0.7rem; + color: #d8b46a; + font-size: 0.85rem; + line-height: 1.45; +} + +#splitDustNote { + margin-top: 0.5rem; + color: #e0a1a1; + font-size: 0.8rem; + line-height: 1.4; +} + +/* ---------- Inputs ---------- */ + +.splitField { + display: block; + margin-bottom: 1.35rem; +} + +.splitLabel { + display: block; + margin-bottom: 0.5rem; + color: #9a9a9a; + font-size: 0.75rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.potInputWrap { + display: flex; + align-items: center; + gap: 0.4rem; + background: #141414; + border: 1px solid #3a3a3a; + border-radius: 12px; + padding: 0.2rem 0.75rem; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} + +.potInputWrap:focus-within { + border-color: #d8b46a; + box-shadow: 0 0 0 3px rgba(216, 180, 106, 0.12); +} + +.potCurrency { + color: #777; + font-size: 1.6rem; + font-weight: 300; +} + +#potInput { + flex: 1; + min-width: 0; + background: none; + border: none; + outline: none; + color: #f2f2f2; + font-size: 2rem; + font-weight: 500; + font-variant-numeric: tabular-nums; + padding: 0.55rem 0; + font-family: inherit; +} + +#potInput::placeholder { + color: #4a4a4a; +} + +.clearPotBtn { + flex: none; + width: 34px; + height: 34px; + border-radius: 50%; + border: none; + background: #262626; + color: #999; + font-size: 0.85rem; + cursor: pointer; +} + +.clearPotBtn:hover { + background: #333; + color: #ddd; +} + +/* ---------- Tap targets ---------- */ + +.chipRow { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.choiceChip { + flex: 1 1 auto; + min-width: 58px; + min-height: 52px; + padding: 0.5rem 0.7rem; + background: linear-gradient(180deg, #2e2e2e, #232323); + border: 1px solid #3a3a3a; + border-radius: 11px; + color: #ddd; + font-size: 1rem; + font-family: inherit; + cursor: pointer; + transition: all 0.15s ease; +} + +.choiceChip:hover { + border-color: #4a4a4a; + background: linear-gradient(180deg, #383838, #2a2a2a); +} + +.choiceChip.selected { + background: linear-gradient(180deg, #3a2f18, #241d0f); + border-color: #d8b46a; + color: #f0d9a8; + font-weight: 600; +} + +.waysStepper { + display: flex; + align-items: center; + gap: 0.35rem; + flex: 1 1 auto; + min-width: 150px; + background: #191919; + border: 1px solid #333; + border-radius: 11px; + padding: 0.2rem 0.4rem; +} + +.stepBtn { + width: 40px; + height: 44px; + border: none; + border-radius: 8px; + background: #262626; + color: #ddd; + font-size: 1.25rem; + line-height: 1; + font-family: inherit; + cursor: pointer; +} + +.stepBtn:hover { + background: #333; +} + +.waysReadout { + flex: 1; + text-align: center; + color: #999; + font-size: 0.8rem; + font-variant-numeric: tabular-nums; +} + +/* ---------- Footer actions ---------- */ + +#splitActions { + display: flex; + gap: 0.6rem; + margin-top: 1.75rem; +} + +.splitBtn { + flex: 1; + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 48px; + padding: 0.6rem 0.9rem; + border-radius: 10px; + border: 1px solid #3a3a3a; + background: linear-gradient(180deg, #2e2e2e, #232323); + color: #ddd; + font-size: 0.9rem; + font-family: inherit; + text-decoration: none; + cursor: pointer; + transition: all 0.15s ease; +} + +.splitBtn:hover { + border-color: #4a4a4a; + background: linear-gradient(180deg, #383838, #2a2a2a); +} + +.splitBtnGhost { + background: transparent; +} + +.splitFootnote { + margin-top: 1.5rem; + color: #6f6f6f; + font-size: 0.78rem; + line-height: 1.5; + text-align: center; +} + +.splitFootnote a { + color: #a9782f; +} + +/* Roomier once there's a real screen to work with. */ +@media (min-width: 700px) { + #splitApp { + padding-top: 2.5rem; + } + + #splitHeader h1 { + font-size: 1.8rem; + } +} diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/money.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/money.js new file mode 100644 index 0000000..455ca9f --- /dev/null +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/money.js @@ -0,0 +1,22 @@ +// Shared money formatting for every casino surface. Kept apart from the table's +// rendering so the quick-split calculator can pull in the formatter without dragging +// the whole seat/chip UI along with it. +const Money = { + format(amount) { + const value = Number(amount) || 0; + return `$${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + }, + + // "+$40.00" / "-$40.00" rather than "$-40.00" โ€” a standings column is read at a + // glance, and the sign belongs out front where the eye lands first. + formatSigned(amount) { + const value = Number(amount) || 0; + if (value > 0) return `+${this.format(value)}`; + if (value < 0) return `-${this.format(Math.abs(value))}`; + return this.format(0); + }, +}; + +if (typeof module !== "undefined" && module.exports) { + module.exports = Money; +} diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/quickSplit.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/quickSplit.js new file mode 100644 index 0000000..aa4c164 --- /dev/null +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/quickSplit.js @@ -0,0 +1,166 @@ +// The quick-split screen: pot in, payout out, nothing to set up and nothing saved. +// A dealer opens this one-handed with players waiting, so every control is a tap and +// the answer updates as they type rather than behind a "calculate" button. +const QuickSplit = { + WAY_CHOICES: [2, 3, 4, 5], + CHIP_CHOICES: [ + { value: 0.01, label: "1ยข" }, + { value: 0.25, label: "25ยข" }, + { value: 1, label: "$1" }, + { value: 5, label: "$5" }, + { value: 25, label: "$25" }, + ], + MAX_WAYS: 12, + + ways: 2, + chip: 1, + + mount() { + this.potInput = document.getElementById("potInput"); + this.clearPotBtn = document.getElementById("clearPotBtn"); + + this.renderWayChoices(); + this.renderChipChoices(); + + this.potInput.addEventListener("input", () => { + this.sanitizePotInput(); + this.update(); + }); + + this.clearPotBtn.addEventListener("click", () => { + this.potInput.value = ""; + this.potInput.focus(); + this.update(); + }); + + document.getElementById("resetSplitBtn").addEventListener("click", () => { + this.potInput.value = ""; + this.ways = 2; + this.chip = 1; + this.renderWayChoices(); + this.renderChipChoices(); + this.potInput.focus(); + this.update(); + }); + + this.update(); + }, + + // Keeps the field to something parseable without fighting the typist: digits and a + // single decimal point, at most two places. + sanitizePotInput() { + const cleaned = this.potInput.value + .replace(/[^0-9.]/g, "") + .replace(/(\..*)\./g, "$1") + .replace(/(\.\d\d)\d+/, "$1"); + if (cleaned !== this.potInput.value) this.potInput.value = cleaned; + }, + + makeChoiceButton(label, isSelected, onPick) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "choiceChip"; + btn.textContent = label; + btn.setAttribute("aria-pressed", String(isSelected)); + btn.classList.toggle("selected", isSelected); + btn.addEventListener("click", onPick); + return btn; + }, + + renderWayChoices() { + const row = document.getElementById("waysChoices"); + row.innerHTML = ""; + + this.WAY_CHOICES.forEach(count => { + row.appendChild( + this.makeChoiceButton(String(count), this.ways === count, () => { + this.ways = count; + this.renderWayChoices(); + this.update(); + }) + ); + }); + + // Beyond a five-way chop it's rare enough to hide behind a stepper rather than + // spend a row of buttons on it. + const more = document.createElement("div"); + more.className = "waysStepper"; + + const minus = this.makeStepButton("โˆ’", () => this.stepWays(-1)); + const plus = this.makeStepButton("+", () => this.stepWays(1)); + const readout = document.createElement("span"); + readout.className = "waysReadout"; + readout.textContent = `${this.ways} ways`; + + more.appendChild(minus); + more.appendChild(readout); + more.appendChild(plus); + row.appendChild(more); + }, + + makeStepButton(label, onPick) { + const btn = document.createElement("button"); + btn.type = "button"; + btn.className = "stepBtn"; + btn.textContent = label; + btn.addEventListener("click", onPick); + return btn; + }, + + stepWays(delta) { + this.ways = Math.max(2, Math.min(this.MAX_WAYS, this.ways + delta)); + this.renderWayChoices(); + this.update(); + }, + + renderChipChoices() { + const row = document.getElementById("chipChoices"); + row.innerHTML = ""; + this.CHIP_CHOICES.forEach(choice => { + row.appendChild( + this.makeChoiceButton(choice.label, this.chip === choice.value, () => { + this.chip = choice.value; + this.renderChipChoices(); + this.update(); + }) + ); + }); + }, + + update() { + const potValue = Number(this.potInput.value); + const pot = Number.isNaN(potValue) ? 0 : potValue; + this.clearPotBtn.hidden = this.potInput.value === ""; + + const result = SplitCalculator.split(pot, this.ways, this.chip); + + document.getElementById("splitResultLabel").textContent = + `Each of ${result.players} players gets`; + document.getElementById("splitResultAmount").textContent = Money.format(result.each); + + const oddNote = document.getElementById("splitOddNote"); + if (result.valid && result.oddChips > 0) { + oddNote.textContent = result.oddChips === 1 + ? `plus one odd ${Money.format(result.chip)} chip to the first player left of the button` + : `plus ${result.oddChips} odd ${Money.format(result.chip)} chips โ€” one each to the ` + + `first ${result.oddChips} players left of the button`; + oddNote.hidden = false; + } else { + oddNote.hidden = true; + } + + const dustNote = document.getElementById("splitDustNote"); + if (result.valid && result.dust > 0) { + dustNote.textContent = + `${Money.format(result.dust)} doesn't divide into ${Money.format(result.chip)} chips โ€” ` + + `pick a smaller denomination.`; + dustNote.hidden = false; + } else { + dustNote.hidden = true; + } + + document.getElementById("splitResult").classList.toggle("hasAmount", result.valid && result.each > 0); + }, +}; + +document.addEventListener("DOMContentLoaded", () => QuickSplit.mount()); diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js index d2bb85d..54dd8d5 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js @@ -415,22 +415,6 @@ const Render = { }, }; -const Money = { - format(amount) { - const value = Number(amount) || 0; - return `$${value.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; - }, - - // "+$40.00" / "-$40.00" rather than "$-40.00" โ€” a standings column is read at a - // glance, and the sign belongs out front where the eye lands first. - formatSigned(amount) { - const value = Number(amount) || 0; - if (value > 0) return `+${this.format(value)}`; - if (value < 0) return `-${this.format(Math.abs(value))}`; - return this.format(0); - }, -}; - if (typeof module !== "undefined" && module.exports) { - module.exports = { Render, Money }; + module.exports = { Render }; } diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/splitCalculator.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/splitCalculator.js new file mode 100644 index 0000000..22252c2 --- /dev/null +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/splitCalculator.js @@ -0,0 +1,53 @@ +// Pure math for chopping one pot between tied players. No DOM. +// +// A dealer can't push half a chip across the felt, so this works in whole chips of +// whatever the smallest denomination in play is: everyone gets the same stack, and +// whatever won't divide is called out separately as odd chips. House rule for those +// is that they go to the first player left of the button, one at a time. +const SplitCalculator = { + // Everything is done in cents to keep 0.1 + 0.2 out of a payout. + toCents(amount) { + return Math.round((Number(amount) || 0) * 100); + }, + + split(potAmount, ways, chipAmount) { + const potCents = this.toCents(potAmount); + const chipCents = Math.max(1, this.toCents(chipAmount)); + const players = Math.max(1, Math.floor(Number(ways) || 0)); + + if (potCents <= 0) { + return { valid: false, each: 0, oddChips: 0, chip: chipCents / 100, dust: 0, players }; + } + + const wholeChips = Math.floor(potCents / chipCents); + // Left over when the pot itself isn't a whole number of the smallest chip โ€” + // usually means the dealer picked the wrong denomination, so it's surfaced + // rather than silently rolled into someone's stack. + const dustCents = potCents - wholeChips * chipCents; + + const chipsEach = Math.floor(wholeChips / players); + const oddChips = wholeChips - chipsEach * players; + + return { + valid: true, + players, + each: (chipsEach * chipCents) / 100, + oddChips, + chip: chipCents / 100, + dust: dustCents / 100, + }; + }, + + // Sanity check for the UI: what the split actually hands out has to equal the pot. + accountedFor(result) { + return ( + Math.round(result.each * 100) * result.players + + Math.round(result.chip * 100) * result.oddChips + + Math.round(result.dust * 100) + ) / 100; + }, +}; + +if (typeof module !== "undefined" && module.exports) { + module.exports = SplitCalculator; +} From a39675f975b3734e5c0e363e4cd3415266a9c67f Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Mon, 31 Aug 2026 18:11:09 -0600 Subject: [PATCH 2/3] Make the dealer table usable on a phone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things were broken on a narrow screen. The seat ring is drawn on a radius measured from the table wrap, so at phone width the left- and right-most seats hung off the edge of the viewport โ€” at a ten-handed table two players were simply not on screen. And tapping a seat opened an editor far enough down the page that it looked like nothing had happened. The ring is now inset by half a puck on phones, which keeps every seat on screen, and the pucks are narrower there so neighbours stop overlapping where the ring flattens out at the top and bottom. The seat editor becomes a bottom sheet that slides up over the table: grip, Done button, tap the same seat again to dismiss, table still visible above it. Its controls grow to thumb size, and every field goes to 16px โ€” below that iOS zooms the page on focus and the dealer has to pinch back out mid-hand. Header actions become full-width tap targets, and the page padding tightens to give the table the width it needs. Nothing above 700px changes. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml | 6 +- JoshHeaps.Net/wwwroot/css/casino/holdem.css | 211 ++++++++++++++++++ .../wwwroot/js/casino/holdem/main.js | 2 + .../wwwroot/js/casino/holdem/render.js | 15 +- 4 files changed, 232 insertions(+), 2 deletions(-) diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml index b532553..e7464c8 100644 --- a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml @@ -38,7 +38,11 @@
+
+ + +