From 4ceeed71a2f161e755b4861af32ef32ba8bc37bc Mon Sep 17 00:00:00 2001 From: JoshHeaps Date: Mon, 31 Aug 2026 22:52:52 -0600 Subject: [PATCH] Add keyboard control for a dealer working on a laptop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The phone got gestures a few rounds back; the desktop still had a mouse and nothing else. On a bar that gets tapped thirty times a hand, moving a hand to the trackpad between every decision is the slow part. F folds, C checks or calls, R opens the raise pad, A shoves, Enter confirms a raise or moves the hand on, Esc backs out, U takes the last action back. Each button wears its key in the corner, hidden on touch devices where it would just be clutter on a button being tapped anyway. The shortcuts press the same buttons a dealer would tap rather than calling the betting engine themselves, so there's no second code path to drift out of step with what the bar is actually offering — if a button isn't on screen, its key does nothing. Nothing fires while a name, amount or note is being typed, or while an overlay owns the keyboard. Typing "Francesca" into a seat name must not fold the table. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb --- JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml | 1 + JoshHeaps.Net/wwwroot/css/casino/holdem.css | 24 +++++++ .../wwwroot/js/casino/holdem/actionBar.js | 21 ++++++ .../wwwroot/js/casino/holdem/main.js | 32 ++++++++++ .../wwwroot/js/casino/holdem/shortcuts.js | 64 +++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 JoshHeaps.Net/wwwroot/js/casino/holdem/shortcuts.js diff --git a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml index 38022df..b15577b 100644 --- a/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml +++ b/JoshHeaps.Net/Pages/Casino/TexasHoldem.cshtml @@ -294,6 +294,7 @@ + diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index 10012e1..145f96c 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -1219,6 +1219,7 @@ body.hasActionBar #holdemSidebar { } .actionBtn { + position: relative; flex: 1; display: flex; flex-direction: column; @@ -1256,6 +1257,29 @@ body.hasActionBar #holdemSidebar { font-variant-numeric: tabular-nums; } +.actionKey { + position: absolute; + top: 5px; + right: 7px; + padding: 0 0.25rem; + border-radius: 4px; + border: 1px solid currentColor; + opacity: 0.4; + font-family: inherit; + font-size: 0.6rem; + line-height: 1.4; + text-transform: uppercase; + pointer-events: none; +} + +/* No keyboard, no key badges — on a phone they'd just be clutter on the buttons + the dealer is already tapping. */ +@media (hover: none) { + .actionKey { + display: none; + } +} + .actionFold { background: linear-gradient(180deg, #3d1f1f, #2c1515); border-color: #5b2626; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js index 95a4bdb..d440315 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js @@ -194,6 +194,19 @@ const ActionBar = { ); }, + // Which key presses each button. Shown on the button itself and hidden on touch + // devices — a shortcut nobody can discover may as well not exist, and a key badge + // on a phone is just clutter. + KEY_HINTS: { + actionFold: "F", + actionCheck: "C", + actionCall: "C", + actionRaise: "R", + actionAdvance: "↵", + raiseConfirm: "↵", + raiseCancel: "esc", + }, + button(label, className, onClick, sublabel) { const btn = document.createElement("button"); btn.type = "button"; @@ -204,6 +217,14 @@ const ActionBar = { main.textContent = label; btn.appendChild(main); + const hint = this.KEY_HINTS[className]; + if (hint) { + const kbd = document.createElement("kbd"); + kbd.className = "actionKey"; + kbd.textContent = hint; + btn.appendChild(kbd); + } + if (sublabel) { const sub = document.createElement("span"); sub.className = "actionBtnSub"; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js index 4b1e7e5..cb2cc36 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js @@ -116,6 +116,38 @@ document.addEventListener("DOMContentLoaded", () => { // snapshots refer to a hand that no longer exists. const clearActionHistory = () => { actionHistory.length = 0; }; + // Keyboard shortcuts press the very same buttons a dealer would tap, rather than + // calling the betting engine themselves — there's no second code path to drift out + // of step with what the bar is actually offering right now. + const pressInBar = selector => { + const btn = document.querySelector(`#actionBar ${selector}`); + if (!btn || btn.disabled) return false; + btn.click(); + return true; + }; + + const quickRaiseButton = label => + [...document.querySelectorAll("#actionBar .raiseQuickBtn")] + .find(b => b.querySelector(".actionBtnLabel").textContent === label); + + Shortcuts.mount({ + fold: () => pressInBar(".actionFold"), + checkOrCall: () => pressInBar(".actionCheck") || pressInBar(".actionCall"), + raise: () => pressInBar(".actionRaise"), + allIn: () => { + // Open the raise pad first if it isn't already, then take its All-in amount. + if (!document.querySelector("#actionBar .raisePad") && !pressInBar(".actionRaise")) return false; + const quick = quickRaiseButton("All-in"); + if (!quick) return false; + quick.click(); + return true; + }, + // Enter confirms a raise if one is being entered, otherwise moves the hand on. + advance: () => pressInBar(".raiseConfirm") || pressInBar(".actionAdvance"), + undo: () => pressInBar(".actionUndo"), + cancel: () => pressInBar(".raiseCancel"), + }); + // Moving to the next hand used to ask "are you sure?" every single time — // the most-pressed button in a session, gated behind a system dialog. It now // just happens, and the toast offers the way back for the few seconds anyone diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/shortcuts.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/shortcuts.js new file mode 100644 index 0000000..eb1077b --- /dev/null +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/shortcuts.js @@ -0,0 +1,64 @@ +// Keyboard control for a dealer running this on a laptop. +// +// The phone has gestures; the desktop had a mouse and nothing else. These are the +// same actions the bar already offers, reachable without moving a hand to the +// trackpad between every decision — which is the whole point when the bar is being +// tapped thirty times a hand. +const Shortcuts = { + // Deliberately single letters with no modifier: a dealer's spare hand rests on + // the keyboard, and a chord is slower than reaching for the button would be. + KEYS: { + f: "fold", + c: "checkOrCall", + r: "raise", + a: "allIn", + " ": "advance", + enter: "advance", + u: "undo", + escape: "cancel", + }, + + mount(handlers) { + this.handlers = handlers; + document.addEventListener("keydown", e => this.onKey(e)); + }, + + // Nothing fires while the dealer is typing a name, an amount, or a note — the + // "c" in "Cass" must not fold the table. + isTyping() { + const el = document.activeElement; + if (!el) return false; + const tag = el.tagName; + return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable; + }, + + // 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"] + .some(id => { + const el = document.getElementById(id); + return el && !el.hidden; + }); + }, + + onKey(e) { + if (e.metaKey || e.ctrlKey || e.altKey) return; + + const key = e.key.length === 1 ? e.key.toLowerCase() : e.key.toLowerCase(); + const action = this.KEYS[key]; + if (!action) return; + + // Escape is the exception: it should still close the raise pad even if focus + // happens to be sitting in its amount field. + if (this.isTyping() && !(action === "cancel")) return; + if (this.overlayOpen()) return; + + const handled = this.handlers[action] && this.handlers[action](); + if (handled) e.preventDefault(); + }, +}; + +if (typeof module !== "undefined" && module.exports) { + module.exports = Shortcuts; +}