diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css index c2aedc8..10012e1 100644 --- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css +++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css @@ -1075,12 +1075,37 @@ body.hasActionBar #holdemSidebar { .actionStatus { display: flex; + flex-wrap: wrap; align-items: baseline; - gap: 0.6rem; + gap: 0.4rem 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; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js index 9328ea7..95a4bdb 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js @@ -90,6 +90,20 @@ 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; }, @@ -201,7 +215,11 @@ const ActionBar = { return btn; }, - commit(mutate) { + // 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); mutate(); this.hooks.save(this.state); this.hooks.rerender(); @@ -212,20 +230,21 @@ 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(() => Betting.fold(this.state, index)))); + row.appendChild(this.button("Fold", "actionFold", () => this.commit(`${who}'s fold`, () => Betting.fold(this.state, index)))); if (owed <= 0) { - row.appendChild(this.button("Check", "actionCheck", () => this.commit(() => Betting.check(this.state, index)))); + row.appendChild(this.button("Check", "actionCheck", () => this.commit(`${who}'s check`, () => 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(() => Betting.allIn(this.state, index)), Money.format(stack))); + row.appendChild(this.button("Call all-in", "actionCall", () => this.commit(`${who}'s all-in`, () => Betting.allIn(this.state, index)), Money.format(stack))); } else { - row.appendChild(this.button("Call", "actionCall", () => this.commit(() => Betting.call(this.state, index)), Money.format(owed))); + row.appendChild(this.button("Call", "actionCall", () => this.commit(`${who}'s call`, () => Betting.call(this.state, index)), Money.format(owed))); } if (stack > owed) { @@ -294,7 +313,8 @@ const ActionBar = { entry.appendChild(this.button("Raise to", "raiseConfirm", () => { const target = Math.max(0, Math.min(Number(this.raiseDraft) || 0, maxTo)); this.raiseOpen = false; - this.commit(() => Betting.raiseTo(this.state, index, target)); + const who = HoldemState.displayName(player, index); + this.commit(`${who}'s raise to ${Money.format(target)}`, () => Betting.raiseTo(this.state, index, target)); })); wrap.appendChild(entry); @@ -313,7 +333,7 @@ const ActionBar = { this.hooks.resolvePot(); return; } - this.commit(() => Betting.closeStreet(this.state)); + this.commit(`dealing the ${Betting.streetLabel(Betting.STREETS[Betting.STREETS.indexOf(this.state.street) + 1]).toLowerCase()}`, () => Betting.closeStreet(this.state)); })); return row; diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js index 0c547f9..4b1e7e5 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js @@ -83,12 +83,39 @@ 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; }; + // 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 @@ -97,6 +124,9 @@ 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(); @@ -177,6 +207,7 @@ document.addEventListener("DOMContentLoaded", () => { HoldemState.clear(); Object.assign(state, HoldemState.createDefault()); + clearActionHistory(); Render.mount(state, hooks); Toast.show("Table reset.", { duration: 2500 }); });