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">
|
<div id="holdemHeaderLeft">
|
||||||
<h1>Texas Hold'em Dealer</h1>
|
<h1>Texas Hold'em Dealer</h1>
|
||||||
<p class="holdemSubtitle">Seat your players, track the action, split the pot.</p>
|
<p class="holdemSubtitle">Seat your players, track the action, split the pot.</p>
|
||||||
|
<span id="handCounterBadge" class="handCounterBadge">Hand #1</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="holdemHeaderActions">
|
<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>
|
<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>
|
<textarea id="tableNotes" rows="4" placeholder="Anything else worth tracking — side bets, IOUs, running totals..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</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>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -156,6 +166,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</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="potWizardOverlay" hidden>
|
||||||
<div id="potWizardModal">
|
<div id="potWizardModal">
|
||||||
<div class="potWizardHeader">
|
<div class="potWizardHeader">
|
||||||
|
|||||||
@@ -51,6 +51,18 @@ body {
|
|||||||
font-size: 0.9rem;
|
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 {
|
#holdemHeaderActions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.6rem;
|
gap: 0.6rem;
|
||||||
@@ -593,6 +605,50 @@ body {
|
|||||||
padding: 0.3rem 0.5rem;
|
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 {
|
#tableNotes {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
document.getElementById("resolvePotBtn").addEventListener("click", () => {
|
document.getElementById("resolvePotBtn").addEventListener("click", () => {
|
||||||
PotWizard.open(state, () => {
|
PotWizard.open(state, payouts => {
|
||||||
|
if (payouts.length > 0) HoldemState.recordHand(state, payouts);
|
||||||
HoldemState.startNextHand(state);
|
HoldemState.startNextHand(state);
|
||||||
hooks.save(state);
|
hooks.save(state);
|
||||||
Render.renderAll();
|
Render.renderAll();
|
||||||
@@ -74,4 +75,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
document.getElementById("potWizardOverlay").addEventListener("click", e => {
|
document.getElementById("potWizardOverlay").addEventListener("click", e => {
|
||||||
if (e.target.id === "potWizardOverlay") PotWizard.close();
|
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.className = "holdemBtn holdemBtnAccent";
|
||||||
applyBtn.textContent = "Start Next Hand";
|
applyBtn.textContent = "Start Next Hand";
|
||||||
applyBtn.addEventListener("click", () => {
|
applyBtn.addEventListener("click", () => {
|
||||||
|
const payouts = Array.from(this.totals.entries()).map(([id, amount]) => ({
|
||||||
|
label: this.playerLabelById.get(id),
|
||||||
|
amount,
|
||||||
|
}));
|
||||||
this.close();
|
this.close();
|
||||||
this.onFinished();
|
this.onFinished(payouts);
|
||||||
});
|
});
|
||||||
actions.appendChild(applyBtn);
|
actions.appendChild(applyBtn);
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ const Render = {
|
|||||||
this.updatePotBadge();
|
this.updatePotBadge();
|
||||||
this.renderSetupFields();
|
this.renderSetupFields();
|
||||||
this.renderTableHint();
|
this.renderTableHint();
|
||||||
|
this.renderHandHistory();
|
||||||
},
|
},
|
||||||
|
|
||||||
renderSetupFields() {
|
renderSetupFields() {
|
||||||
@@ -349,6 +350,39 @@ const Render = {
|
|||||||
list.appendChild(row);
|
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 = {
|
const Money = {
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ const HoldemState = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
MAX_HISTORY_ENTRIES: 200,
|
||||||
|
|
||||||
createDefault() {
|
createDefault() {
|
||||||
const seatCount = this.DEFAULT_SEAT_COUNT;
|
const seatCount = this.DEFAULT_SEAT_COUNT;
|
||||||
return {
|
return {
|
||||||
@@ -40,6 +42,8 @@ const HoldemState = {
|
|||||||
chipLegend: this.DEFAULT_CHIP_LEGEND.map(c => ({ ...c })),
|
chipLegend: this.DEFAULT_CHIP_LEGEND.map(c => ({ ...c })),
|
||||||
seats: Array.from({ length: seatCount }, () => this.makePlayer()),
|
seats: Array.from({ length: seatCount }, () => this.makePlayer()),
|
||||||
notes: "",
|
notes: "",
|
||||||
|
handNumber: 1,
|
||||||
|
handHistory: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -49,6 +53,8 @@ const HoldemState = {
|
|||||||
if (!raw) return this.createDefault();
|
if (!raw) return this.createDefault();
|
||||||
const parsed = JSON.parse(raw);
|
const parsed = JSON.parse(raw);
|
||||||
if (!parsed || !Array.isArray(parsed.seats)) return this.createDefault();
|
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;
|
return parsed;
|
||||||
} catch {
|
} catch {
|
||||||
return this.createDefault();
|
return this.createDefault();
|
||||||
@@ -137,6 +143,31 @@ const HoldemState = {
|
|||||||
p.amountOverride = null;
|
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) {
|
if (typeof module !== "undefined" && module.exports) {
|
||||||
|
|||||||
Reference in New Issue
Block a user