Compare commits

..
3 changed files with 8 additions and 193 deletions
+1 -51
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;
@@ -1119,11 +1094,6 @@ body.hasActionBar #holdemSidebar {
font-variant-numeric: tabular-nums;
}
.actionPlayers {
color: #9c9c9c;
font-variant-numeric: tabular-nums;
}
.actionWho {
margin-left: auto;
color: #9c9c9c;
@@ -1132,26 +1102,6 @@ body.hasActionBar #holdemSidebar {
white-space: nowrap;
}
/* Stopwatch, not a shot clock: it colours up as a prompt to the dealer, and does
nothing else. */
.actionTank {
flex: none;
min-width: 3.1rem;
text-align: right;
color: #7d7d7d;
font-variant-numeric: tabular-nums;
transition: color 0.3s ease;
}
.actionTank.tankNudge {
color: #d8b46a;
}
.actionTank.tankSlow {
color: #e07b7b;
font-weight: 600;
}
/* ---------- Side-pot breakdown ---------- */
.potBreakdown {
@@ -27,8 +27,6 @@ const ActionBar = {
if (!this.isRunning()) {
host.hidden = true;
document.body.classList.remove("hasActionBar");
this.stopTankTimer();
this.tankSeatIndex = null;
return;
}
host.hidden = false;
@@ -48,8 +46,6 @@ const ActionBar = {
} else {
host.appendChild(this.renderActions());
}
this.syncTankTimer();
},
renderStatus() {
@@ -66,14 +62,6 @@ const ActionBar = {
pot.textContent = `Pot ${Money.format(Betting.potSoFar(this.state))}`;
row.appendChild(pot);
// How many are still in the hand — part of the standard per-street
// announcement, and the number a dealer is asked for most often.
const remaining = Betting.liveSeats(this.state).length;
const players = document.createElement("span");
players.className = "actionPlayers";
players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`;
row.appendChild(players);
const index = this.state.actingSeatIndex;
if (index !== null && index !== undefined && !Betting.isHandOver(this.state)) {
const player = this.state.seats[index];
@@ -81,101 +69,15 @@ const ActionBar = {
who.className = "actionWho";
who.textContent = `${HoldemState.displayName(player, index)} · ${Money.format(Betting.stackOf(this.state, player))} behind`;
row.appendChild(who);
const tank = document.createElement("span");
tank.className = "actionTank";
tank.id = "actionTank";
tank.title = "How long this player has been thinking";
tank.textContent = "0:00";
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;
},
// ---- Think timer ----
//
// A player who hasn't noticed the action is on them costs more time than anything
// else a dealer does, so the bar shows how long the current player has been in the
// tank. It's a stopwatch, not a shot clock: nothing happens when it grows, it just
// tells the dealer when it's worth a nudge.
NUDGE_AFTER_S: 30,
SLOW_AFTER_S: 60,
tankSeatIndex: null,
tankStartedAt: null,
tankInterval: null,
// Restarts only when the action actually moves — a re-render for any other reason
// (a chip edit, a name change) must not put a thinking player back to zero.
syncTankTimer() {
const index = this.state.actingSeatIndex;
const running = index !== null && index !== undefined && !Betting.isHandOver(this.state);
if (!running) {
this.stopTankTimer();
this.tankSeatIndex = null;
return;
}
if (this.tankSeatIndex !== index) {
this.tankSeatIndex = index;
this.tankStartedAt = Date.now();
}
if (!this.tankInterval) {
this.tankInterval = setInterval(() => this.paintTank(), 1000);
}
this.paintTank();
},
stopTankTimer() {
if (this.tankInterval) {
clearInterval(this.tankInterval);
this.tankInterval = null;
}
},
// Updates only the timer's own text, so the seconds ticking over never rebuilds the
// buttons under the dealer's thumb.
paintTank() {
const el = document.getElementById("actionTank");
if (!el || this.tankStartedAt === null) return;
const seconds = Math.floor((Date.now() - this.tankStartedAt) / 1000);
const mins = Math.floor(seconds / 60);
el.textContent = `${mins}:${String(seconds % 60).padStart(2, "0")}`;
el.classList.toggle("tankNudge", seconds >= this.NUDGE_AFTER_S && seconds < this.SLOW_AFTER_S);
el.classList.toggle("tankSlow", seconds >= this.SLOW_AFTER_S);
},
// Shown only once someone is all-in and the money has actually split — a single
// undivided pot needs no announcement, and a line that's always there stops being
// read.
renderPotBreakdown() {
// Only worth showing once someone is actually all-in. Mid-street the amounts in
// front of players are unequal all the time — the blinds alone are unequal —
// and layering that would announce a side pot on every hand before anyone had
// even acted.
const someoneAllIn = this.state.seats.some(
player => player.inHand && Betting.handTotal(player) > 0 && Betting.stackOf(this.state, player) <= 0
);
if (!someoneAllIn) return null;
const contributions = this.state.seats.map((player, index) => ({
id: player.id,
index,
@@ -215,11 +117,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();
@@ -230,21 +128,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) {
@@ -313,8 +210,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);
@@ -333,7 +229,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,39 +83,12 @@ 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
@@ -124,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();
@@ -207,7 +177,6 @@ document.addEventListener("DOMContentLoaded", () => {
HoldemState.clear();
Object.assign(state, HoldemState.createDefault());
clearActionHistory();
Render.mount(state, hooks);
Toast.show("Table reset.", { duration: 2500 });
});