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; +}