Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fe16fa7f2 | ||
|
|
f199148046 | ||
|
|
38536a5c0c |
@@ -1013,9 +1013,24 @@ button,
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A bare checkbox ships at 13x13, which misses the 24px minimum with a mouse as well
|
||||||
|
as with a thumb. The row around it is the real target on touch, but the box itself
|
||||||
|
should be findable on any device. */
|
||||||
|
#dedicatedDealerToggle {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.linkBtn {
|
.linkBtn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
/* An underlined word was a 91x14 target, which misses the 24px minimum with a
|
||||||
|
mouse as well as with a thumb. The coarse-pointer block takes it to 44; this is
|
||||||
|
the floor everywhere else, and it changes nothing about how it looks. */
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 24px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
color: #d8b46a;
|
color: #d8b46a;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
@@ -1470,7 +1485,16 @@ button,
|
|||||||
|
|
||||||
.sidebarPanel summary {
|
.sidebarPanel summary {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 0.7rem 0;
|
/* These headers are the sidebar's whole navigation and were 38px tall on every
|
||||||
|
device, phones included — the touch pass further down never reached them.
|
||||||
|
Padding rather than a flex box with min-height, because a <summary> only draws
|
||||||
|
its disclosure triangle while it is display: list-item. */
|
||||||
|
padding: 0.85rem 0;
|
||||||
|
/* A <summary> is content-box, so min-height would have been *added* to the padding
|
||||||
|
— measured at 71px before this line went in. Same trap the header buttons were
|
||||||
|
in, one file over. */
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-height: 44px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: #eee;
|
color: #eee;
|
||||||
letter-spacing: 0.03em;
|
letter-spacing: 0.03em;
|
||||||
@@ -3185,3 +3209,66 @@ textarea:focus-visible {
|
|||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
color: #9a9a9a;
|
color: #9a9a9a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Touch targets on a wide touch screen ----------
|
||||||
|
|
||||||
|
Every touch-target rule in this file sits behind `max-width: 700px`, and width was
|
||||||
|
never the question. A phone held sideways is 844px wide, so none of that work
|
||||||
|
applied to it. Measured on an emulated touch device at 844x390, with the sidebar
|
||||||
|
panels open: twelve controls under 44px, including the rulings link at 91x14 and
|
||||||
|
the dedicated-dealer checkbox at 13x13.
|
||||||
|
|
||||||
|
Restated here against the pointer instead of the viewport. The width-scoped block
|
||||||
|
keeps its layout changes — one column, stacked dialogs — because those really are
|
||||||
|
about how much room there is. This is only about what a thumb can hit, so it asks
|
||||||
|
about the thumb. */
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
.panelBody input:not([type="checkbox"]):not([type="color"]),
|
||||||
|
.subPanelBody input:not([type="checkbox"]):not([type="color"]),
|
||||||
|
.panelBody select {
|
||||||
|
font-size: 16px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0.4rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelBody .holdemBtnSmall,
|
||||||
|
.handHistoryActions .holdemBtn,
|
||||||
|
.toastAction {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Destructive and tiny is the worst combination: these delete a chip colour or a
|
||||||
|
custom field, and are 28px wide. */
|
||||||
|
.removeChipColorBtn,
|
||||||
|
.removeFieldBtn {
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* flex-shrink would take a stated width straight back off again. */
|
||||||
|
.chipColorInput {
|
||||||
|
flex: none;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A bare checkbox can't grow into a target, but the row it sits in can — the
|
||||||
|
label is what a tap actually lands on. */
|
||||||
|
.panelBody .fieldRow {
|
||||||
|
min-height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#dedicatedDealerToggle {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* An underlined word is a 14px-tall target; give it room without making it look
|
||||||
|
like a button. */
|
||||||
|
.linkBtn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 0.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -394,6 +407,29 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
Toast.show("Table reset.", { duration: 2500 });
|
Toast.show("Table reset.", { duration: 2500 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// A pre-filled amount is a suggestion, so reaching for the box replaces it.
|
||||||
|
//
|
||||||
|
// Delegated from the document rather than wired per field: the raise pad, the
|
||||||
|
// add-chips sheet and the seat editor all build their inputs fresh every render,
|
||||||
|
// and a box added later should not have to remember to ask for this.
|
||||||
|
document.addEventListener("focusin", event => {
|
||||||
|
const field = event.target;
|
||||||
|
if (!Money.selectsOnFocus(field)) return;
|
||||||
|
if (!field.value) return;
|
||||||
|
// After the browser has finished placing its own caret, or it wins the race
|
||||||
|
// and the selection collapses again the moment the tap completes.
|
||||||
|
setTimeout(() => {
|
||||||
|
if (document.activeElement !== field) return;
|
||||||
|
try {
|
||||||
|
field.select();
|
||||||
|
} catch {
|
||||||
|
// A number input refuses setSelectionRange in some browsers; select()
|
||||||
|
// is the one that works on both, and if neither does the field is
|
||||||
|
// simply left as it was.
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById("resolvePotBtn").addEventListener("click", openPotWizard);
|
document.getElementById("resolvePotBtn").addEventListener("click", openPotWizard);
|
||||||
|
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -48,6 +48,24 @@ const Money = {
|
|||||||
return (this.toCents(amount) / 100).toFixed(2);
|
return (this.toCents(amount) / 100).toFixed(2);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Should focusing this field select what's already in it?
|
||||||
|
//
|
||||||
|
// Every amount box in this tool opens pre-filled with a suggestion: the raise pad
|
||||||
|
// offers the minimum, Add chips offers a buy-in, the seat editor offers the stack
|
||||||
|
// it already has. A dealer taps the box because they want a *different* number,
|
||||||
|
// and a tap puts the caret after the suggestion instead of over it. Measured on a
|
||||||
|
// phone: the raise pad showing 4, tap, type "60" — the field reads 460. Add chips
|
||||||
|
// showing 200, tap, type "50" — 20050.
|
||||||
|
//
|
||||||
|
// Only number boxes. A name or a note is a thing you edit, and selecting all of
|
||||||
|
// it on focus would be the same mistake pointing the other way.
|
||||||
|
selectsOnFocus(field) {
|
||||||
|
if (!field || field.tagName !== "INPUT") return false;
|
||||||
|
const type = String(field.type || "").toLowerCase();
|
||||||
|
if (type === "number") return true;
|
||||||
|
return type === "text" && String(field.inputMode || "").toLowerCase() === "decimal";
|
||||||
|
},
|
||||||
|
|
||||||
// "+$40.00" / "-$40.00" rather than "$-40.00" — a standings column is read at a
|
// "+$40.00" / "-$40.00" rather than "$-40.00" — a standings column is read at a
|
||||||
// glance, and the sign belongs out front where the eye lands first.
|
// glance, and the sign belongs out front where the eye lands first.
|
||||||
formatSigned(amount) {
|
formatSigned(amount) {
|
||||||
|
|||||||
@@ -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,53 @@
|
|||||||
|
// Which fields replace their contents when a dealer reaches for them.
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Money } = require("./load");
|
||||||
|
|
||||||
|
// Enough of an input to answer the question, without a DOM.
|
||||||
|
const field = (props = {}) => ({ tagName: "INPUT", type: "text", inputMode: "", ...props });
|
||||||
|
|
||||||
|
test("a number box selects", () => {
|
||||||
|
assert.equal(Money.selectsOnFocus(field({ type: "number" })), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a decimal keypad box selects", () => {
|
||||||
|
// The raise pad's amount is type=text with inputmode=decimal, so the phone shows
|
||||||
|
// a number pad without the browser's spinner arrows.
|
||||||
|
assert.equal(Money.selectsOnFocus(field({ type: "text", inputMode: "decimal" })), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a name does not", () => {
|
||||||
|
// Selecting all of a name on focus is the same mistake pointing the other way:
|
||||||
|
// a name is a thing you edit, not a suggestion you replace.
|
||||||
|
assert.equal(Money.selectsOnFocus(field({ type: "text" })), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a colour swatch does not", () => {
|
||||||
|
assert.equal(Money.selectsOnFocus(field({ type: "color" })), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a checkbox does not", () => {
|
||||||
|
assert.equal(Money.selectsOnFocus(field({ type: "checkbox" })), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a textarea does not, whatever it is asked", () => {
|
||||||
|
assert.equal(Money.selectsOnFocus({ tagName: "TEXTAREA", type: "text", inputMode: "decimal" }), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a select does not", () => {
|
||||||
|
assert.equal(Money.selectsOnFocus({ tagName: "SELECT", type: "select-one" }), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("case doesn't matter", () => {
|
||||||
|
assert.equal(Money.selectsOnFocus(field({ type: "NUMBER" })), true);
|
||||||
|
assert.equal(Money.selectsOnFocus(field({ type: "TEXT", inputMode: "DECIMAL" })), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nothing at all is not a field", () => {
|
||||||
|
assert.equal(Money.selectsOnFocus(null), false);
|
||||||
|
assert.equal(Money.selectsOnFocus(undefined), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a field with no type stated is a text box, so it does not", () => {
|
||||||
|
assert.equal(Money.selectsOnFocus({ tagName: "INPUT" }), false);
|
||||||
|
});
|
||||||
@@ -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