Hold'em dealer: 55 polish rounds, one merge #101
@@ -96,6 +96,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Two players change chairs. Undoable like every other seat action, because a
|
||||||
|
// swap is easy to aim wrong and the seats look alike at a glance.
|
||||||
|
swapSeats: (from, to) => {
|
||||||
|
const movedName = nameOf(from);
|
||||||
|
const intoName = nameOf(to);
|
||||||
|
const wasEmpty = Render.isEmptySeat(state.seats[to]);
|
||||||
|
runUndoable(
|
||||||
|
wasEmpty
|
||||||
|
? `${movedName} moved to seat ${to + 1}.`
|
||||||
|
: `${movedName} and ${intoName} changed seats.`,
|
||||||
|
() => HoldemState.swapSeats(state, from, to));
|
||||||
|
},
|
||||||
|
|
||||||
makeDealer: index => {
|
makeDealer: index => {
|
||||||
// Before the deal there's nothing posted, so this is just moving a marker.
|
// Before the deal there's nothing posted, so this is just moving a marker.
|
||||||
// After it, the blinds have to follow the button or the badges and the
|
// After it, the blinds have to follow the button or the badges and the
|
||||||
|
|||||||
@@ -49,11 +49,21 @@ const Render = {
|
|||||||
document.getElementById("defaultStackInput").value = this.state.defaultStack ?? "";
|
document.getElementById("defaultStackInput").value = this.state.defaultStack ?? "";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Whether this device can actually drag a seat. HTML5 drag events are never
|
||||||
|
// generated from a touch screen, so on a phone the last clause of the hint below
|
||||||
|
// described something impossible — on the device the rest of the sentence was
|
||||||
|
// written for. Touch gets told about the menu route instead, which works.
|
||||||
|
canDragSeats() {
|
||||||
|
return typeof matchMedia !== "function" || matchMedia("(hover: hover)").matches;
|
||||||
|
},
|
||||||
|
|
||||||
renderTableHint() {
|
renderTableHint() {
|
||||||
const hint = document.getElementById("tableHint");
|
const hint = document.getElementById("tableHint");
|
||||||
// Gestures are invisible until someone tells you about them, and this is the
|
// Gestures are invisible until someone tells you about them, and this is the
|
||||||
// only place on the table with room to say so.
|
// only place on the table with room to say so.
|
||||||
hint.textContent = "Tap a seat to edit · double-tap to fold · press and hold for quick actions · drag to swap seats.";
|
hint.textContent = this.canDragSeats()
|
||||||
|
? "Tap a seat to edit · double-tap to fold · press and hold for quick actions · drag to swap seats."
|
||||||
|
: "Tap a seat to edit · double-tap to fold · press and hold for quick actions, including moving a seat.";
|
||||||
},
|
},
|
||||||
|
|
||||||
renderNotes() {
|
renderNotes() {
|
||||||
|
|||||||
@@ -6,10 +6,16 @@ const SeatMenu = {
|
|||||||
hooks: null,
|
hooks: null,
|
||||||
seatIndex: null,
|
seatIndex: null,
|
||||||
|
|
||||||
|
mode: null,
|
||||||
|
|
||||||
open(state, hooks, seatIndex) {
|
open(state, hooks, seatIndex) {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.hooks = hooks;
|
this.hooks = hooks;
|
||||||
this.seatIndex = seatIndex;
|
this.seatIndex = seatIndex;
|
||||||
|
// Always the action list, never whatever the sheet was showing when it last
|
||||||
|
// closed — a seat menu that opens on a list of chairs is a menu that lost the
|
||||||
|
// dealer's place.
|
||||||
|
this.mode = null;
|
||||||
this.render();
|
this.render();
|
||||||
document.getElementById("seatMenuOverlay").hidden = false;
|
document.getElementById("seatMenuOverlay").hidden = false;
|
||||||
},
|
},
|
||||||
@@ -63,29 +69,81 @@ const SeatMenu = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Changing chairs was drag-and-drop only, and HTML5 drag events are never
|
||||||
|
// generated from a touch screen — so on the device this tool is mostly used on,
|
||||||
|
// the "drag to swap seats" the hint advertised did nothing at all. This is the
|
||||||
|
// same move, by the route a thumb can actually take.
|
||||||
|
if (this.state.seats.length > 1) {
|
||||||
|
actions.push({ label: "Move to…", run: () => this.openMovePicker() });
|
||||||
|
}
|
||||||
|
|
||||||
actions.push({ label: "Edit seat…", run: () => this.hooks.editSeat(index) });
|
actions.push({ label: "Edit seat…", run: () => this.hooks.editSeat(index) });
|
||||||
return actions;
|
return actions;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// The chairs this player could move to. Every other chair: swapping into an empty
|
||||||
|
// one is how a player moves, and swapping with an occupied one is two players
|
||||||
|
// changing places, which is the other half of the same job.
|
||||||
|
destinationsFor(index) {
|
||||||
|
return this.state.seats
|
||||||
|
.map((player, seat) => ({ player, seat }))
|
||||||
|
.filter(entry => entry.seat !== index);
|
||||||
|
},
|
||||||
|
|
||||||
|
openMovePicker() {
|
||||||
|
this.mode = "move";
|
||||||
|
this.render();
|
||||||
|
},
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const player = this.state.seats[this.seatIndex];
|
const player = this.state.seats[this.seatIndex];
|
||||||
document.getElementById("seatMenuTitle").textContent = HoldemState.displayName(player, this.seatIndex);
|
const title = document.getElementById("seatMenuTitle");
|
||||||
|
const subtitle = document.getElementById("seatMenuSubtitle");
|
||||||
|
const list = document.getElementById("seatMenuActions");
|
||||||
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
if (this.mode === "move") {
|
||||||
|
title.textContent = `Move ${HoldemState.displayName(player, this.seatIndex)} to…`;
|
||||||
|
subtitle.textContent = "Their chips, cards and bet go with them. The button and the blinds stay on the chair.";
|
||||||
|
this.destinationsFor(this.seatIndex).forEach(({ player: other, seat }) => {
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.type = "button";
|
||||||
|
btn.className = "seatMenuBtn";
|
||||||
|
// displayName falls back to "Seat 5" for a chair with nobody in it,
|
||||||
|
// which reads as "Seat 5 — Seat 5". An empty chair says it's empty.
|
||||||
|
const named = (other.name || "").trim();
|
||||||
|
btn.textContent = named ? `Seat ${seat + 1} — ${named}` : `Seat ${seat + 1} — empty`;
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
const from = this.seatIndex;
|
||||||
|
this.close();
|
||||||
|
this.hooks.swapSeats(from, seat);
|
||||||
|
});
|
||||||
|
list.appendChild(btn);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
title.textContent = HoldemState.displayName(player, this.seatIndex);
|
||||||
|
|
||||||
const stack = Betting.stackOf(this.state, player);
|
const stack = Betting.stackOf(this.state, player);
|
||||||
const owed = Betting.toCall(this.state, player);
|
const owed = Betting.toCall(this.state, player);
|
||||||
const bits = [];
|
const bits = [];
|
||||||
if (stack > 0) bits.push(`${Money.format(stack)} behind`);
|
if (stack > 0) bits.push(`${Money.format(stack)} behind`);
|
||||||
if (owed > 0) bits.push(`${Money.format(owed)} to call`);
|
if (owed > 0) bits.push(`${Money.format(owed)} to call`);
|
||||||
document.getElementById("seatMenuSubtitle").textContent = bits.join(" · ");
|
subtitle.textContent = bits.join(" · ");
|
||||||
|
|
||||||
const list = document.getElementById("seatMenuActions");
|
|
||||||
list.innerHTML = "";
|
|
||||||
this.actionsFor(player, this.seatIndex).forEach(action => {
|
this.actionsFor(player, this.seatIndex).forEach(action => {
|
||||||
const btn = document.createElement("button");
|
const btn = document.createElement("button");
|
||||||
btn.type = "button";
|
btn.type = "button";
|
||||||
btn.className = `seatMenuBtn ${action.className || ""}`;
|
btn.className = `seatMenuBtn ${action.className || ""}`;
|
||||||
btn.textContent = action.label;
|
btn.textContent = action.label;
|
||||||
btn.addEventListener("click", () => {
|
btn.addEventListener("click", () => {
|
||||||
|
// "Move to…" stays on the sheet and swaps it for the seat list; every
|
||||||
|
// other action is the end of the interaction.
|
||||||
|
if (action.label === "Move to…") {
|
||||||
|
action.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.close();
|
this.close();
|
||||||
action.run();
|
action.run();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -467,6 +467,21 @@ const HoldemState = {
|
|||||||
.sort((a, b) => b.net - a.net);
|
.sort((a, b) => b.net - a.net);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Two players change chairs.
|
||||||
|
//
|
||||||
|
// The whole seat moves — name, stack, cards, what they have in the pot — because
|
||||||
|
// that is what changing chairs is. What stays behind is everything that belongs to
|
||||||
|
// the *position*: the button, the blinds and the acting index all name a chair, and
|
||||||
|
// a player who moves into the big blind's chair is the big blind. That is the same
|
||||||
|
// answer a card room gives.
|
||||||
|
swapSeats(state, a, b) {
|
||||||
|
const seats = state.seats || [];
|
||||||
|
if (a === b) return false;
|
||||||
|
if (!seats[a] || !seats[b]) return false;
|
||||||
|
[seats[a], seats[b]] = [seats[b], seats[a]];
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
// The one check a banker actually needs, and the one nothing here did.
|
// The one check a banker actually needs, and the one nothing here did.
|
||||||
//
|
//
|
||||||
// Every chip on the table is a promise to pay cash for it later. If the chips in
|
// Every chip on the table is a promise to pay cash for it later. If the chips in
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
// Two players changing chairs.
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load");
|
||||||
|
|
||||||
|
test("the whole seat moves", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[0].name = "Ann";
|
||||||
|
state.seats[0].cards = ["AS", "KD"];
|
||||||
|
state.seats[0].stack = 175;
|
||||||
|
state.seats[4].name = "";
|
||||||
|
|
||||||
|
HoldemState.swapSeats(state, 0, 4);
|
||||||
|
|
||||||
|
assert.equal(state.seats[4].name, "Ann");
|
||||||
|
assert.deepEqual(state.seats[4].cards, ["AS", "KD"]);
|
||||||
|
assert.equal(state.seats[4].stack, 175);
|
||||||
|
assert.equal(state.seats[0].name, "");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("what they have in the pot goes with them", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
const posted = state.seats.map(p => p.committed);
|
||||||
|
const before = chipsOnTable(state);
|
||||||
|
|
||||||
|
HoldemState.swapSeats(state, 1, 4);
|
||||||
|
|
||||||
|
assert.equal(state.seats[4].committed, posted[1]);
|
||||||
|
assert.equal(state.seats[1].committed, posted[4]);
|
||||||
|
assert.equal(chipsOnTable(state), before, "a swap never creates or destroys a chip");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the button stays on the chair, not on the player", () => {
|
||||||
|
// A card room moves the button around the table, not around the people. Somebody
|
||||||
|
// who sits down in the big blind's chair is the big blind.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
state.seats[0].name = "Ann";
|
||||||
|
|
||||||
|
HoldemState.swapSeats(state, 0, 2);
|
||||||
|
|
||||||
|
assert.equal(state.dealerSeatIndex, 0);
|
||||||
|
assert.notEqual(state.seats[0].name, "Ann");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the blinds follow the chairs too", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
const before = HoldemState.getBlindSeats(state);
|
||||||
|
|
||||||
|
HoldemState.swapSeats(state, before.sbIndex, before.bbIndex);
|
||||||
|
const after = HoldemState.getBlindSeats(state);
|
||||||
|
|
||||||
|
assert.equal(after.sbIndex, before.sbIndex);
|
||||||
|
assert.equal(after.bbIndex, before.bbIndex);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swapping a seat with itself does nothing and says so", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
const before = JSON.stringify(state);
|
||||||
|
|
||||||
|
assert.equal(HoldemState.swapSeats(state, 1, 1), false);
|
||||||
|
assert.equal(JSON.stringify(state), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a chair that isn't there is refused rather than swallowed", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
const before = JSON.stringify(state);
|
||||||
|
|
||||||
|
assert.equal(HoldemState.swapSeats(state, 0, 99), false);
|
||||||
|
assert.equal(HoldemState.swapSeats(state, -1, 0), false);
|
||||||
|
assert.equal(JSON.stringify(state), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("swapping twice puts everything back", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
const before = JSON.stringify(state);
|
||||||
|
|
||||||
|
HoldemState.swapSeats(state, 1, 3);
|
||||||
|
HoldemState.swapSeats(state, 1, 3);
|
||||||
|
|
||||||
|
assert.equal(JSON.stringify(state), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("moving into an empty chair leaves an empty chair behind", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
state.seats[1].name = "Ben";
|
||||||
|
|
||||||
|
HoldemState.swapSeats(state, 1, 5);
|
||||||
|
|
||||||
|
assert.equal(state.seats[5].name, "Ben");
|
||||||
|
assert.equal(state.seats[5].inHand, true);
|
||||||
|
assert.equal(state.seats[1].inHand, false);
|
||||||
|
});
|
||||||
@@ -522,8 +522,15 @@ const LayoutAudit = {
|
|||||||
const chips = [...document.querySelectorAll(".dealerChip")];
|
const chips = [...document.querySelectorAll(".dealerChip")];
|
||||||
const shown = chips.filter(chip => {
|
const shown = chips.filter(chip => {
|
||||||
const style = getComputedStyle(chip);
|
const style = getComputedStyle(chip);
|
||||||
return style.display !== "none" && style.visibility !== "hidden" &&
|
if (style.display === "none" || style.visibility === "hidden") return false;
|
||||||
parseFloat(style.opacity) > 0.05;
|
if (parseFloat(style.opacity) <= 0.05) return false;
|
||||||
|
// A hovered seat deliberately fades its empty slot in — that's the "put the
|
||||||
|
// button here" preview, and it is on screen because a pointer is sitting on
|
||||||
|
// that seat. Counting it reports two buttons whenever the mouse happens to
|
||||||
|
// rest on the table, which is most of the time a mouse exists.
|
||||||
|
const seat = chip.closest(".seat");
|
||||||
|
if (seat && seat.matches(":hover") && !chip.classList.contains("active")) return false;
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
const offenders = [];
|
const offenders = [];
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user