Somebody rebuying is the most common interruption in a cash game, and the only way to record it was to open the seat editor and retype a stack from memory. Press and hold a seat and there's now "Add chips…": quick amounts off the table's starting stack — half a buy-in, a buy-in, two — with a keypad for the times it isn't one of those. It's undoable like every other seat action, and it records cash handed over as well as chips added, because those answer different questions. The stack moves every hand; what a player has paid in only moves when they buy. The export gains bought in, chips now, and up/down, so settling up at the end of the night is reading a row rather than reconstructing it. Getting that last column right needed care. Chips in front of a player disagree with the hand log for as long as a hand is open — and since blinds post the instant the previous hand settles, that window is almost always. Counting what a player already has in the current pot as still theirs makes the cash position agree with the hand log at any moment, including across a rebuy. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
81 lines
3.1 KiB
JavaScript
81 lines
3.1 KiB
JavaScript
// Turns the in-memory hand history into a CSV file the dealer can download for a
|
|
// paper trail — a house manager or a disputed chip count needs something that
|
|
// outlives the browser tab, not just a scrollable panel in the sidebar.
|
|
const HandHistoryExport = {
|
|
csvField(value) {
|
|
const str = String(value ?? "");
|
|
return /[",\n]/.test(str) ? `"${str.replace(/"/g, '""')}"` : str;
|
|
},
|
|
|
|
formatTimestamp(timestamp) {
|
|
try {
|
|
return new Date(timestamp).toLocaleString();
|
|
} catch {
|
|
return "";
|
|
}
|
|
},
|
|
|
|
toCsv(handHistory, standings = []) {
|
|
const rows = [["Hand #", "Time", "Pot", "Payouts"]];
|
|
// handHistory is stored newest-first; the export reads better oldest-first.
|
|
[...handHistory].reverse().forEach(entry => {
|
|
const payouts = (entry.payouts || [])
|
|
.map(p => `${p.label}: ${Money.format(p.amount)}`)
|
|
.join("; ");
|
|
rows.push([
|
|
entry.handNumber,
|
|
this.formatTimestamp(entry.timestamp),
|
|
Money.format(entry.pot),
|
|
payouts,
|
|
]);
|
|
});
|
|
|
|
// The settle-up numbers go in the same file as the hands they came from, so a
|
|
// dealer handing over one export isn't asked for a second one at cash-out.
|
|
if (standings.length > 0) {
|
|
rows.push([]);
|
|
rows.push(["Session Standings"]);
|
|
// Bought in and chips now are what a cash-out is settled from; paid in and
|
|
// won describe the hands. Both belong in the file the dealer hands over.
|
|
rows.push(["Player", "Paid In", "Won", "Net", "Bought In", "Chips Now", "Up/Down"]);
|
|
standings.forEach(row => {
|
|
rows.push([
|
|
row.name,
|
|
Money.format(row.in),
|
|
Money.format(row.won),
|
|
Money.formatSigned(row.net),
|
|
Money.format(row.boughtIn || 0),
|
|
Money.format(row.chipsNow || 0),
|
|
Money.formatSigned(row.cashPosition || 0),
|
|
]);
|
|
});
|
|
}
|
|
|
|
return rows.map(row => row.map(f => this.csvField(f)).join(",")).join("\r\n");
|
|
},
|
|
|
|
fileName() {
|
|
const now = new Date();
|
|
const pad = n => String(n).padStart(2, "0");
|
|
const stamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}-${pad(now.getHours())}${pad(now.getMinutes())}`;
|
|
return `holdem-hand-history-${stamp}.csv`;
|
|
},
|
|
|
|
download(handHistory, standings = []) {
|
|
const csv = this.toCsv(handHistory, standings);
|
|
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = this.fileName();
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
},
|
|
};
|
|
|
|
if (typeof module !== "undefined" && module.exports) {
|
|
module.exports = HandHistoryExport;
|
|
}
|