Add a persistent hand history log to the Texas Hold'em dealer tool
Give the dealer a running record of resolved pots — hand number, total pot, who got paid what, and the time — instead of losing that information the moment the pot wizard closes. It's the kind of audit trail a real dealer needs when a chip count gets disputed after the fact. - state.js tracks state.handNumber and state.handHistory (capped at 200 entries) and persists both through the existing localStorage save/load path, with backfill defaults for tables saved before this change. - potWizard.js now hands its final payouts back to the caller instead of discarding them once "Start Next Hand" is clicked. - main.js records a history entry (skipping hands where nobody actually won anything) before advancing to the next hand, and wires up a "Clear history" control that also resets the hand counter. - A new sidebar panel lists history entries newest-first; the header shows a running "Hand #N" badge next to the existing subtitle. Verified via Playwright MCP: seated two players, resolved a pot, and confirmed the header's hand counter incremented, the history panel listed the payout, the entry survived a full page reload (localStorage persistence), and Clear History reset both the log and the counter. No console errors during the flow. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01C3NiiUDimoKDJ8GjC3KL3x
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
<div id="holdemHeaderLeft">
|
||||
<h1>Texas Hold'em Dealer</h1>
|
||||
<p class="holdemSubtitle">Seat your players, track the action, split the pot.</p>
|
||||
<span id="handCounterBadge" class="handCounterBadge">Hand #1</span>
|
||||
</div>
|
||||
<div id="holdemHeaderActions">
|
||||
<button id="nextHandBtn" class="holdemBtn" title="Move the button, small blind and big blind to the next occupied seat">Next Hand ⟳</button>
|
||||
@@ -84,6 +85,15 @@
|
||||
<textarea id="tableNotes" rows="4" placeholder="Anything else worth tracking — side bets, IOUs, running totals..."></textarea>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary>Hand History</summary>
|
||||
<div class="panelBody">
|
||||
<div id="handHistoryList"></div>
|
||||
<p id="handHistoryEmptyHint" class="fieldHint">Resolved pots show up here as you play, in case a chip count is disputed later.</p>
|
||||
<button id="clearHistoryBtn" class="holdemBtn holdemBtnSmall holdemBtnDanger">Clear history</button>
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,6 +166,17 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template id="handHistoryRowTemplate">
|
||||
<div class="handHistoryRow">
|
||||
<div class="handHistoryRowHeader">
|
||||
<span class="handHistoryHandNumber"></span>
|
||||
<span class="handHistoryPot"></span>
|
||||
<span class="handHistoryTime"></span>
|
||||
</div>
|
||||
<div class="handHistoryPayouts"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div id="potWizardOverlay" hidden>
|
||||
<div id="potWizardModal">
|
||||
<div class="potWizardHeader">
|
||||
|
||||
@@ -51,6 +51,18 @@ body {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.handCounterBadge {
|
||||
display: inline-block;
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.2rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(216, 180, 106, 0.12);
|
||||
border: 1px solid rgba(216, 180, 106, 0.4);
|
||||
color: #d8b46a;
|
||||
font-size: 0.75rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
#holdemHeaderActions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
@@ -593,6 +605,50 @@ body {
|
||||
padding: 0.3rem 0.5rem;
|
||||
}
|
||||
|
||||
#handHistoryList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.handHistoryRow {
|
||||
background: #141414;
|
||||
border: 1px solid #2c2c2c;
|
||||
border-radius: 6px;
|
||||
padding: 0.45rem 0.6rem;
|
||||
}
|
||||
|
||||
.handHistoryRowHeader {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.handHistoryHandNumber {
|
||||
color: #d8b46a;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.handHistoryPot {
|
||||
color: #eee;
|
||||
}
|
||||
|
||||
.handHistoryTime {
|
||||
margin-left: auto;
|
||||
color: #777;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.handHistoryPayouts {
|
||||
margin-top: 0.15rem;
|
||||
color: #aaa;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#tableNotes {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
@@ -63,7 +63,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
});
|
||||
|
||||
document.getElementById("resolvePotBtn").addEventListener("click", () => {
|
||||
PotWizard.open(state, () => {
|
||||
PotWizard.open(state, payouts => {
|
||||
if (payouts.length > 0) HoldemState.recordHand(state, payouts);
|
||||
HoldemState.startNextHand(state);
|
||||
hooks.save(state);
|
||||
Render.renderAll();
|
||||
@@ -74,4 +75,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("potWizardOverlay").addEventListener("click", e => {
|
||||
if (e.target.id === "potWizardOverlay") PotWizard.close();
|
||||
});
|
||||
|
||||
document.getElementById("clearHistoryBtn").addEventListener("click", () => {
|
||||
if (!confirm("Clear the hand history and reset the hand counter to 1? This can't be undone.")) return;
|
||||
HoldemState.clearHistory(state);
|
||||
hooks.save(state);
|
||||
Render.renderAll();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -175,8 +175,12 @@ const PotWizard = {
|
||||
applyBtn.className = "holdemBtn holdemBtnAccent";
|
||||
applyBtn.textContent = "Start Next Hand";
|
||||
applyBtn.addEventListener("click", () => {
|
||||
const payouts = Array.from(this.totals.entries()).map(([id, amount]) => ({
|
||||
label: this.playerLabelById.get(id),
|
||||
amount,
|
||||
}));
|
||||
this.close();
|
||||
this.onFinished();
|
||||
this.onFinished(payouts);
|
||||
});
|
||||
actions.appendChild(applyBtn);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ const Render = {
|
||||
this.updatePotBadge();
|
||||
this.renderSetupFields();
|
||||
this.renderTableHint();
|
||||
this.renderHandHistory();
|
||||
},
|
||||
|
||||
renderSetupFields() {
|
||||
@@ -349,6 +350,39 @@ const Render = {
|
||||
list.appendChild(row);
|
||||
});
|
||||
},
|
||||
|
||||
// ---- Hand counter + history ----
|
||||
|
||||
formatHandTime(timestamp) {
|
||||
try {
|
||||
return new Date(timestamp).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
|
||||
renderHandHistory() {
|
||||
document.getElementById("handCounterBadge").textContent = `Hand #${this.state.handNumber || 1}`;
|
||||
|
||||
const list = document.getElementById("handHistoryList");
|
||||
const emptyHint = document.getElementById("handHistoryEmptyHint");
|
||||
const history = this.state.handHistory || [];
|
||||
list.innerHTML = "";
|
||||
emptyHint.hidden = history.length > 0;
|
||||
|
||||
const template = document.getElementById("handHistoryRowTemplate");
|
||||
history.forEach(entry => {
|
||||
const row = template.content.firstElementChild.cloneNode(true);
|
||||
row.querySelector(".handHistoryHandNumber").textContent = `Hand #${entry.handNumber}`;
|
||||
row.querySelector(".handHistoryPot").textContent = Money.format(entry.pot);
|
||||
row.querySelector(".handHistoryTime").textContent = this.formatHandTime(entry.timestamp);
|
||||
|
||||
const payoutsEl = row.querySelector(".handHistoryPayouts");
|
||||
payoutsEl.textContent = entry.payouts.map(p => `${p.label} ${Money.format(p.amount)}`).join(", ");
|
||||
|
||||
list.appendChild(row);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const Money = {
|
||||
|
||||
@@ -29,6 +29,8 @@ const HoldemState = {
|
||||
};
|
||||
},
|
||||
|
||||
MAX_HISTORY_ENTRIES: 200,
|
||||
|
||||
createDefault() {
|
||||
const seatCount = this.DEFAULT_SEAT_COUNT;
|
||||
return {
|
||||
@@ -40,6 +42,8 @@ const HoldemState = {
|
||||
chipLegend: this.DEFAULT_CHIP_LEGEND.map(c => ({ ...c })),
|
||||
seats: Array.from({ length: seatCount }, () => this.makePlayer()),
|
||||
notes: "",
|
||||
handNumber: 1,
|
||||
handHistory: [],
|
||||
};
|
||||
},
|
||||
|
||||
@@ -49,6 +53,8 @@ const HoldemState = {
|
||||
if (!raw) return this.createDefault();
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || !Array.isArray(parsed.seats)) return this.createDefault();
|
||||
if (typeof parsed.handNumber !== "number" || parsed.handNumber < 1) parsed.handNumber = 1;
|
||||
if (!Array.isArray(parsed.handHistory)) parsed.handHistory = [];
|
||||
return parsed;
|
||||
} catch {
|
||||
return this.createDefault();
|
||||
@@ -137,6 +143,31 @@ const HoldemState = {
|
||||
p.amountOverride = null;
|
||||
});
|
||||
},
|
||||
|
||||
// Appends a resolved-pot record to the running session log a dealer can point to
|
||||
// for chip-count disputes, then advances the hand counter. `payouts` is
|
||||
// [{ label, amount }, ...] — who got paid what out of this hand's pot(s).
|
||||
recordHand(state, payouts) {
|
||||
if (typeof state.handNumber !== "number") state.handNumber = 1;
|
||||
if (!Array.isArray(state.handHistory)) state.handHistory = [];
|
||||
|
||||
const pot = payouts.reduce((sum, p) => sum + p.amount, 0);
|
||||
state.handHistory.unshift({
|
||||
handNumber: state.handNumber,
|
||||
timestamp: Date.now(),
|
||||
pot,
|
||||
payouts,
|
||||
});
|
||||
if (state.handHistory.length > this.MAX_HISTORY_ENTRIES) {
|
||||
state.handHistory.length = this.MAX_HISTORY_ENTRIES;
|
||||
}
|
||||
state.handNumber += 1;
|
||||
},
|
||||
|
||||
clearHistory(state) {
|
||||
state.handHistory = [];
|
||||
state.handNumber = 1;
|
||||
},
|
||||
};
|
||||
|
||||
if (typeof module !== "undefined" && module.exports) {
|
||||
|
||||
Reference in New Issue
Block a user