@@ -348,6 +368,7 @@
+
diff --git a/JoshHeaps.Net/wwwroot/css/casino/holdem.css b/JoshHeaps.Net/wwwroot/css/casino/holdem.css
index 0e48f2c..8d9ca2e 100644
--- a/JoshHeaps.Net/wwwroot/css/casino/holdem.css
+++ b/JoshHeaps.Net/wwwroot/css/casino/holdem.css
@@ -585,6 +585,145 @@ body {
font-weight: 400;
}
+/* ---------- Add chips ---------- */
+
+.seatBuyIns {
+ margin-top: -0.3rem;
+ color: #9a9a9a;
+ font-size: 0.75rem;
+ font-variant-numeric: tabular-nums;
+}
+
+#addChipsOverlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.72);
+ display: flex;
+ align-items: flex-end;
+ justify-content: center;
+ z-index: 98;
+}
+
+#addChipsOverlay[hidden] {
+ display: none;
+}
+
+#addChipsSheet {
+ width: 100%;
+ max-width: 420px;
+ background: linear-gradient(180deg, #242424, #1a1a1a);
+ border: 1px solid #3a3a3a;
+ border-bottom: none;
+ border-radius: 16px 16px 0 0;
+ padding: 0.35rem 1rem calc(1.2rem + env(safe-area-inset-bottom, 0px));
+ box-shadow: 0 -14px 40px rgba(0, 0, 0, 0.65);
+}
+
+.addChipsHeader {
+ margin-bottom: 0.8rem;
+}
+
+.addChipsHeader h2 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 500;
+ color: #f5f5f5;
+}
+
+#addChipsSubtitle {
+ color: #9a9a9a;
+ font-size: 0.78rem;
+ font-variant-numeric: tabular-nums;
+}
+
+#addChipsQuick {
+ display: flex;
+ gap: 0.45rem;
+ margin-bottom: 0.7rem;
+}
+
+.addChipsQuickBtn {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 0.1rem;
+ min-height: 52px;
+ border-radius: 11px;
+ border: 1px solid #3a3a3a;
+ background: linear-gradient(180deg, #2e2e2e, #232323);
+ color: #ddd;
+ font-family: inherit;
+ cursor: pointer;
+}
+
+.addChipsQuickBtn.selected {
+ border-color: #d8b46a;
+ background: linear-gradient(180deg, #3a2f18, #241d0f);
+ color: #f0d9a8;
+}
+
+.addChipsQuickLabel {
+ font-size: 0.78rem;
+ font-weight: 600;
+}
+
+.addChipsQuickAmount {
+ font-size: 0.7rem;
+ color: #a8a8a8;
+ font-variant-numeric: tabular-nums;
+}
+
+.addChipsQuickBtn.selected .addChipsQuickAmount {
+ color: #d8b46a;
+}
+
+.addChipsEntry {
+ display: flex;
+ align-items: center;
+ gap: 0.4rem;
+ background: #141414;
+ border: 1px solid #3a3a3a;
+ border-radius: 11px;
+ padding: 0.2rem 0.8rem;
+ margin-bottom: 0.8rem;
+}
+
+.addChipsEntry:focus-within {
+ border-color: #d8b46a;
+}
+
+.addChipsCurrency {
+ color: #777;
+ font-size: 1.3rem;
+}
+
+#addChipsInput {
+ flex: 1;
+ min-width: 0;
+ background: none;
+ border: none;
+ outline: none;
+ color: #f2f2f2;
+ font-family: inherit;
+ font-size: 1.5rem;
+ font-weight: 500;
+ font-variant-numeric: tabular-nums;
+ padding: 0.4rem 0;
+}
+
+.addChipsActions {
+ display: flex;
+ flex-direction: column-reverse;
+ gap: 0.5rem;
+}
+
+.addChipsActions .holdemBtn {
+ width: 100%;
+ justify-content: center;
+ min-height: 48px;
+}
+
/* ---------- Rulings reference ---------- */
.tableHintActions {
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/addChips.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/addChips.js
new file mode 100644
index 0000000..f63b3ee
--- /dev/null
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/addChips.js
@@ -0,0 +1,92 @@
+// "Add chips" — a rebuy, a top-up, or the first buy-in of the night.
+//
+// Quick amounts come off the table's starting stack, because a rebuy is almost
+// always one buy-in, sometimes half or two. The keypad is there for the times it
+// isn't. Recorded as cash in as well as chips, so at the end of the night the
+// dealer can say what each player actually handed over.
+const AddChips = {
+ state: null,
+ onConfirm: null,
+ seatIndex: null,
+ draft: "",
+
+ open(state, seatIndex, onConfirm) {
+ this.state = state;
+ this.seatIndex = seatIndex;
+ this.onConfirm = onConfirm;
+ this.draft = String(this.baseAmount());
+ this.render();
+ document.getElementById("addChipsOverlay").hidden = false;
+ },
+
+ close() {
+ document.getElementById("addChipsOverlay").hidden = true;
+ },
+
+ // The table's starting stack if there is one, otherwise something sane to nudge
+ // rather than an empty box.
+ baseAmount() {
+ const configured = Number(this.state.defaultStack);
+ if (!Number.isNaN(configured) && configured > 0) return configured;
+ const bb = Number(this.state.bigBlind) || 0;
+ return bb > 0 ? bb * 100 : 100;
+ },
+
+ quickAmounts() {
+ const base = this.baseAmount();
+ return [
+ { label: "½ buy-in", value: Math.round(base / 2 * 100) / 100 },
+ { label: "Buy-in", value: base },
+ { label: "2 buy-ins", value: base * 2 },
+ ];
+ },
+
+ render() {
+ const player = this.state.seats[this.seatIndex];
+ document.getElementById("addChipsTitle").textContent =
+ `Add chips — ${HoldemState.displayName(player, this.seatIndex)}`;
+
+ const stack = Betting.stackOf(this.state, player);
+ const boughtIn = Number(player.buyIns) || 0;
+ document.getElementById("addChipsSubtitle").textContent = boughtIn > 0
+ ? `${Money.format(stack)} in front · ${Money.format(boughtIn)} in so far`
+ : `${Money.format(stack)} in front`;
+
+ const quick = document.getElementById("addChipsQuick");
+ quick.innerHTML = "";
+ this.quickAmounts().forEach(option => {
+ const btn = document.createElement("button");
+ btn.type = "button";
+ btn.className = "addChipsQuickBtn";
+ btn.classList.toggle("selected", Number(this.draft) === option.value);
+
+ const label = document.createElement("span");
+ label.className = "addChipsQuickLabel";
+ label.textContent = option.label;
+ const amount = document.createElement("span");
+ amount.className = "addChipsQuickAmount";
+ amount.textContent = Money.format(option.value);
+
+ btn.appendChild(label);
+ btn.appendChild(amount);
+ btn.addEventListener("click", () => {
+ this.draft = String(option.value);
+ this.render();
+ });
+ quick.appendChild(btn);
+ });
+
+ const input = document.getElementById("addChipsInput");
+ input.value = this.draft;
+ },
+
+ confirm() {
+ const amount = Math.max(0, Number(document.getElementById("addChipsInput").value) || 0);
+ this.close();
+ if (amount > 0) this.onConfirm(this.seatIndex, Math.round(amount * 100) / 100);
+ },
+};
+
+if (typeof module !== "undefined" && module.exports) {
+ module.exports = AddChips;
+}
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/handHistoryExport.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/handHistoryExport.js
index 20aef90..571ad8a 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/handHistoryExport.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/handHistoryExport.js
@@ -35,9 +35,19 @@ const HandHistoryExport = {
if (standings.length > 0) {
rows.push([]);
rows.push(["Session Standings"]);
- rows.push(["Player", "Paid In", "Won", "Net"]);
+ // 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)]);
+ 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),
+ ]);
});
}
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
index a68cc98..f6915ae 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/main.js
@@ -57,6 +57,15 @@ document.addEventListener("DOMContentLoaded", () => {
},
makeDealer: index => seatAction(index, `Button on ${nameOf(index)}.`, () => { state.dealerSeatIndex = index; }),
editSeat: index => Render.toggleSeatEditor(index),
+
+ addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
+ seatAction(seatIndex, `${nameOf(seatIndex)} added ${Money.format(amount)}.`, player => {
+ // Chips in front and cash in are different questions — the stack moves
+ // every hand, what they've handed over only moves when they buy.
+ player.stack = Math.round((Betting.stackOf(state, player) + amount) * 100) / 100;
+ player.buyIns = Math.round(((Number(player.buyIns) || 0) + amount) * 100) / 100;
+ });
+ }),
};
Render.mount(state, hooks);
@@ -260,6 +269,15 @@ document.addEventListener("DOMContentLoaded", () => {
document.getElementById("closeSeatEditorBtn").addEventListener("click", () => Render.closeSeatEditor());
+ document.getElementById("addChipsConfirmBtn").addEventListener("click", () => AddChips.confirm());
+ document.getElementById("addChipsCancelBtn").addEventListener("click", () => AddChips.close());
+ document.getElementById("addChipsOverlay").addEventListener("click", e => {
+ if (e.target.id === "addChipsOverlay") AddChips.close();
+ });
+ document.getElementById("addChipsInput").addEventListener("keydown", e => {
+ if (e.key === "Enter") AddChips.confirm();
+ });
+
const rulingsOverlay = document.getElementById("rulingsOverlay");
const closeRulings = () => { rulingsOverlay.hidden = true; };
document.getElementById("openRulingsBtn").addEventListener("click", () => { rulingsOverlay.hidden = false; });
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
index afbab1f..d25950f 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/render.js
@@ -491,6 +491,13 @@ const Render = {
});
});
+ // What they've handed over tonight, shown next to what's in front of them so a
+ // cash-out is one subtraction rather than a memory test.
+ const boughtIn = Number(player.buyIns) || 0;
+ const boughtInEl = form.querySelector(".seatBuyIns");
+ boughtInEl.textContent = boughtIn > 0 ? `Bought in ${Money.format(boughtIn)}` : "";
+ boughtInEl.hidden = boughtIn <= 0;
+
const stackInput = form.querySelector(".stackInput");
stackInput.value = player.stack ?? "";
stackInput.addEventListener("input", () => {
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js
index ba38380..7c05825 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/seatMenu.js
@@ -32,6 +32,10 @@ const SeatMenu = {
actions.push({ label: "Un-fold", run: () => this.hooks.unfold(index) });
}
+ // The most common interruption in a cash game, and until now it meant opening
+ // the editor and retyping a stack from memory.
+ actions.push({ label: "Add chips…", run: () => this.hooks.addChips(index) });
+
actions.push({
label: dealtIn ? "Sit out" : "Deal in",
run: () => this.hooks.toggleDealtIn(index),
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/shortcuts.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/shortcuts.js
index 15ae99d..95bac23 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/shortcuts.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/shortcuts.js
@@ -35,7 +35,7 @@ const Shortcuts = {
// An overlay owns the keyboard while it's up: the confirm dialog already binds
// Enter and Escape, and the card picker and seat menu are modal.
overlayOpen() {
- return ["confirmOverlay", "potWizardOverlay", "cardPickerOverlay", "seatMenuOverlay", "rulingsOverlay"]
+ return ["confirmOverlay", "potWizardOverlay", "cardPickerOverlay", "seatMenuOverlay", "rulingsOverlay", "addChipsOverlay"]
.some(id => {
const el = document.getElementById(id);
return el && !el.hidden;
diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
index a2c7012..67b4a4e 100644
--- a/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
+++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/state.js
@@ -31,6 +31,8 @@ const HoldemState = {
stack: null,
committed: 0,
hasActed: false,
+ // Cash handed over tonight, which only moves when they buy chips.
+ buyIns: 0,
};
},
@@ -79,6 +81,7 @@ const HoldemState = {
if (player.stack === undefined) player.stack = null;
if (player.committed === undefined) player.committed = 0;
if (player.hasActed === undefined) player.hasActed = false;
+ if (player.buyIns === undefined) player.buyIns = 0;
});
return parsed;
@@ -236,13 +239,32 @@ const HoldemState = {
});
});
+ // Cash bought in and chips currently in front don't come from the hand log at
+ // all, so they're read off the seat rather than summed — a rebuy isn't a hand.
+ const seatById = new Map(state.seats.map(p => [p.id, p]));
+
return Array.from(totals.values())
- .map(t => ({
- ...t,
- in: Math.round(t.in * 100) / 100,
- won: Math.round(t.won * 100) / 100,
- net: Math.round((t.won - t.in) * 100) / 100,
- }))
+ .map(t => {
+ const seat = seatById.get(t.id);
+ const boughtIn = seat ? Math.round((Number(seat.buyIns) || 0) * 100) / 100 : 0;
+ const chipsNow = seat ? Math.round((Number(seat.stack) || 0) * 100) / 100 : 0;
+ // Chips already pushed into the hand in progress are still this
+ // player's money until it's won. Leaving them out makes the cash
+ // position disagree with the hand log for as long as a hand is open —
+ // and blinds are posted the instant the previous one settles, so that
+ // window is almost always.
+ const inFrontOfThem = seat ? (Number(seat.amountOverride) || 0) + (Number(seat.committed) || 0) : 0;
+
+ return {
+ ...t,
+ in: Math.round(t.in * 100) / 100,
+ won: Math.round(t.won * 100) / 100,
+ net: Math.round((t.won - t.in) * 100) / 100,
+ boughtIn,
+ chipsNow,
+ cashPosition: Math.round((chipsNow + inFrontOfThem - boughtIn) * 100) / 100,
+ };
+ })
.sort((a, b) => b.net - a.net);
},
};