Compare commits

..
5 changed files with 8 additions and 226 deletions
@@ -294,7 +294,6 @@
<script src="~/js/casino/holdem/seatMenu.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/betting.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/actionBar.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/shortcuts.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/potWizard.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/handHistoryExport.js?v=@ViewData["cssVersion"]"></script>
<script src="~/js/casino/holdem/main.js?v=@ViewData["cssVersion"]"></script>
+1 -50
View File
@@ -1075,37 +1075,12 @@ body.hasActionBar #holdemSidebar {
.actionStatus {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 0.4rem 0.6rem;
gap: 0.6rem;
margin-bottom: 0.55rem;
font-size: 0.78rem;
}
/* Always there while there's something to take back, rather than a toast per tap. */
.actionUndo {
flex: none;
min-height: 30px;
max-width: 100%;
padding: 0.15rem 0.6rem;
border-radius: 999px;
border: 1px solid #414141;
background: #262626;
color: #bdbdbd;
font-family: inherit;
font-size: 0.72rem;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actionUndo:hover {
background: #333;
border-color: #4f4f4f;
color: #eee;
}
.actionStreet {
color: #d8b46a;
font-weight: 600;
@@ -1219,7 +1194,6 @@ body.hasActionBar #holdemSidebar {
}
.actionBtn {
position: relative;
flex: 1;
display: flex;
flex-direction: column;
@@ -1257,29 +1231,6 @@ 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;
@@ -90,20 +90,6 @@ const ActionBar = {
row.appendChild(tank);
}
// A standing control rather than a toast per action: a dealer taps this bar
// constantly, and a toast after every fold would be noise they'd learn to
// ignore — which is exactly when they'd miss the one that mattered.
const undoLabel = this.hooks.lastActionLabel();
if (undoLabel) {
const undo = document.createElement("button");
undo.type = "button";
undo.className = "actionUndo";
undo.title = `Undo ${undoLabel}`;
undo.textContent = `↶ ${undoLabel}`;
undo.addEventListener("click", () => this.hooks.undoLast());
row.appendChild(undo);
}
return row;
},
@@ -194,19 +180,6 @@ 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";
@@ -217,14 +190,6 @@ 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";
@@ -236,11 +201,7 @@ const ActionBar = {
return btn;
},
// Every action taken from the bar goes through here, which is also where it gets
// recorded so it can be taken back. A misclick on Fold is the one mistake in this
// tool a dealer can't talk their way out of.
commit(label, mutate) {
this.hooks.beginAction(label);
commit(mutate) {
mutate();
this.hooks.save(this.state);
this.hooks.rerender();
@@ -251,21 +212,20 @@ const ActionBar = {
const player = this.state.seats[index];
const owed = Betting.toCall(this.state, player);
const stack = Betting.stackOf(this.state, player);
const who = HoldemState.displayName(player, index);
const row = document.createElement("div");
row.className = "actionButtons";
row.appendChild(this.button("Fold", "actionFold", () => this.commit(`${who}'s fold`, () => Betting.fold(this.state, index))));
row.appendChild(this.button("Fold", "actionFold", () => this.commit(() => Betting.fold(this.state, index))));
if (owed <= 0) {
row.appendChild(this.button("Check", "actionCheck", () => this.commit(`${who}'s check`, () => Betting.check(this.state, index))));
row.appendChild(this.button("Check", "actionCheck", () => this.commit(() => Betting.check(this.state, index))));
} else if (owed >= stack) {
// Calling would put them all-in, so say that rather than quoting a number
// they can't cover.
row.appendChild(this.button("Call all-in", "actionCall", () => this.commit(`${who}'s all-in`, () => Betting.allIn(this.state, index)), Money.format(stack)));
row.appendChild(this.button("Call all-in", "actionCall", () => this.commit(() => Betting.allIn(this.state, index)), Money.format(stack)));
} else {
row.appendChild(this.button("Call", "actionCall", () => this.commit(`${who}'s call`, () => Betting.call(this.state, index)), Money.format(owed)));
row.appendChild(this.button("Call", "actionCall", () => this.commit(() => Betting.call(this.state, index)), Money.format(owed)));
}
if (stack > owed) {
@@ -334,8 +294,7 @@ const ActionBar = {
entry.appendChild(this.button("Raise to", "raiseConfirm", () => {
const target = Math.max(0, Math.min(Number(this.raiseDraft) || 0, maxTo));
this.raiseOpen = false;
const who = HoldemState.displayName(player, index);
this.commit(`${who}'s raise to ${Money.format(target)}`, () => Betting.raiseTo(this.state, index, target));
this.commit(() => Betting.raiseTo(this.state, index, target));
}));
wrap.appendChild(entry);
@@ -354,7 +313,7 @@ const ActionBar = {
this.hooks.resolvePot();
return;
}
this.commit(`dealing the ${Betting.streetLabel(Betting.STREETS[Betting.STREETS.indexOf(this.state.street) + 1]).toLowerCase()}`, () => Betting.closeStreet(this.state));
this.commit(() => Betting.closeStreet(this.state));
}));
return row;
@@ -83,69 +83,10 @@ document.addEventListener("DOMContentLoaded", () => {
});
};
// A short history of actions taken from the bar, so a misclicked Fold can be taken
// back — and the one before it, since a dealer usually notices a beat late. Kept in
// memory only: this is for the mistake you just made, not last night's hand.
const MAX_UNDO_STEPS = 12;
const actionHistory = [];
ActionBar.mount(state, {
save: hooks.save,
rerender: hooks.rerender,
resolvePot: openPotWizard,
beginAction: label => {
actionHistory.push({ label, snapshot: JSON.parse(JSON.stringify(state)) });
if (actionHistory.length > MAX_UNDO_STEPS) actionHistory.shift();
},
lastActionLabel: () => (actionHistory.length ? actionHistory[actionHistory.length - 1].label : null),
undoLast: () => {
const step = actionHistory.pop();
if (!step) return;
Object.keys(state).forEach(key => delete state[key]);
Object.assign(state, step.snapshot);
hooks.save(state);
Render.renderAll();
Toast.show(`Took back ${step.label}.`, { duration: 2500 });
},
});
// Anything that ends the hand makes the per-action history meaningless — the
// 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 —
@@ -156,9 +97,6 @@ document.addEventListener("DOMContentLoaded", () => {
const runUndoable = (message, mutate) => {
const snapshot = JSON.parse(JSON.stringify(state));
mutate();
// Starting or settling a hand invalidates the per-action history: those
// snapshots describe a hand that's over. The whole-step Undo below replaces it.
clearActionHistory();
hooks.save(state);
Render.renderAll();
@@ -239,7 +177,6 @@ document.addEventListener("DOMContentLoaded", () => {
HoldemState.clear();
Object.assign(state, HoldemState.createDefault());
clearActionHistory();
Render.mount(state, hooks);
Toast.show("Table reset.", { duration: 2500 });
});
@@ -1,64 +0,0 @@
// 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;
}