Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
168e8a380c | ||
|
|
edc08d7213 |
@@ -163,7 +163,7 @@
|
|||||||
<template id="seatTemplate">
|
<template id="seatTemplate">
|
||||||
<div class="seat" draggable="true">
|
<div class="seat" draggable="true">
|
||||||
<div class="seatChairNumber"></div>
|
<div class="seatChairNumber"></div>
|
||||||
<button class="dealerChip" type="button" title="Make this seat the dealer button">D</button>
|
<button class="dealerChip" title="Make this seat the dealer button">D</button>
|
||||||
<span class="blindBadge sbBadge">SB</span>
|
<span class="blindBadge sbBadge">SB</span>
|
||||||
<span class="blindBadge bbBadge">BB</span>
|
<span class="blindBadge bbBadge">BB</span>
|
||||||
<div class="seatPuckName"></div>
|
<div class="seatPuckName"></div>
|
||||||
|
|||||||
@@ -308,22 +308,9 @@ button,
|
|||||||
|
|
||||||
#pokerTableWrap {
|
#pokerTableWrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
/* The shape is stated once, and both the height and the cap below are derived from
|
|
||||||
it. It used to be a `padding-bottom: 72%` box, which has a trap in it: percentage
|
|
||||||
padding resolves against the *containing block's* width, not the element's own,
|
|
||||||
so narrowing the wrap left it exactly as tall as before. Capping the width was
|
|
||||||
therefore no way to shorten the table — measured, after trying it: the felt went
|
|
||||||
822px wide to 746px and stayed 617px tall, and the bottom seat sat as far into
|
|
||||||
the action bar as it had. aspect-ratio ties the two together properly. */
|
|
||||||
--feltAspect: 0.72;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
aspect-ratio: 1 / var(--feltAspect);
|
padding-bottom: 72%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
/* Capping the width is how the felt is told to stay above the action bar.
|
|
||||||
--feltRoomPx is published by actionBar.js with the bar's real height and the
|
|
||||||
bottom seat's overhang already taken out; the fallback leaves the table its full
|
|
||||||
size if that ever fails to arrive. */
|
|
||||||
max-width: calc(var(--feltRoomPx, 100vh) / var(--feltAspect));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -604,6 +591,7 @@ button,
|
|||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
opacity: 0.32;
|
||||||
transition: opacity 0.15s ease;
|
transition: opacity 0.15s ease;
|
||||||
/* A dealer puck should look like a puck — small, sitting on the rim of its seat.
|
/* A dealer puck should look like a puck — small, sitting on the rim of its seat.
|
||||||
But tapping it hands the button to that seat and re-posts the blinds, and 22px
|
But tapping it hands the button to that seat and re-posts the blinds, and 22px
|
||||||
@@ -634,40 +622,16 @@ button,
|
|||||||
left: -18px;
|
left: -18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A poker table has one dealer button. This one drew nine.
|
.dealerChip:focus-visible {
|
||||||
|
opacity: 1;
|
||||||
Every seat carried a puck — the button's, and eight ghosts at 0.32 opacity that
|
|
||||||
were there to be pressed. Measured on a nine-seat table: eight pale discs, all
|
|
||||||
eight showing the letter D, one of them real. Finding the button meant comparing
|
|
||||||
nine identical shapes for the one that's gold, when it should be the only thing
|
|
||||||
of its kind on the felt. The ghosts also sat on top of their neighbours: every
|
|
||||||
remaining "something important is covered" offender on a crowded table was a
|
|
||||||
seat covered by the seat-next-door's ghost puck.
|
|
||||||
|
|
||||||
So a seat that doesn't have the button doesn't draw one. The affordance is still
|
|
||||||
there — hovering a seat fades its slot in, which is a preview of where the button
|
|
||||||
would go — and on touch, where there is no hover, "Give the button" has always
|
|
||||||
been in the seat's long-press menu. Keyboard reaches it by focus.
|
|
||||||
|
|
||||||
pointer-events matters as much as the opacity: an invisible puck that still takes
|
|
||||||
taps would be worse than a visible one. */
|
|
||||||
.dealerChip:not(.active) {
|
|
||||||
opacity: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
.seat:hover .dealerChip:not(.active) {
|
.seat:hover .dealerChip {
|
||||||
opacity: 0.5;
|
opacity: 1;
|
||||||
pointer-events: auto;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dealerChip:not(.active):focus-visible {
|
|
||||||
opacity: 1;
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dealerChip.active {
|
.dealerChip.active {
|
||||||
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
|
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
|
||||||
border-color: #a9782f;
|
border-color: #a9782f;
|
||||||
@@ -2139,23 +2103,6 @@ body.hasActionBar #holdemSidebar {
|
|||||||
margin-top: 0.1rem;
|
margin-top: 0.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* "ALL IN" takes the stack line's place, so it has to not read as a stack. Wider
|
|
||||||
letters and a warm colour separate it from the grey money above it at a glance —
|
|
||||||
which is the whole job, because the dealer is scanning the ring for who can still
|
|
||||||
be bet at. */
|
|
||||||
.seatPuckStack.seatPuckAllIn {
|
|
||||||
color: #ffcf6b;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* And the seat itself carries a hint of it, so the mark survives being read out of
|
|
||||||
the corner of an eye. Deliberately quieter than .seatActing — all-in is a standing
|
|
||||||
fact about a player, not a request for the dealer's attention. */
|
|
||||||
.seat.seatAllIn {
|
|
||||||
border-color: rgba(255, 207, 107, 0.45);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tournament-sized amounts. A number has no spaces to break on, so an amount too
|
/* Tournament-sized amounts. A number has no spaces to break on, so an amount too
|
||||||
wide for the 64px puck neither wraps nor ellipsises — it draws straight out over
|
wide for the 64px puck neither wraps nor ellipsises — it draws straight out over
|
||||||
the seat next to it. These step the size down so every digit stays on its own
|
the seat next to it. These step the size down so every digit stays on its own
|
||||||
@@ -2534,16 +2481,6 @@ body.hasActionBar #toastHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 700px) {
|
@media (max-width: 700px) {
|
||||||
/* The narrow layout already had an answer for the bar: it reserves the bar's height
|
|
||||||
as scroll room below the table, and a phone is a thing you scroll. Capping the
|
|
||||||
felt here as well was measured and rejected — at 360x640 it took the table from
|
|
||||||
321px to 226px, which fixed the six-seat ring but broke the nine-seat one, two
|
|
||||||
covered seats traded for two seats whose centre landed on a neighbour. The cap is
|
|
||||||
for the wide layout, which never consulted the bar's height at all. */
|
|
||||||
#pokerTableWrap {
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Stacked, with the action that ends the hand furthest from a stray thumb. */
|
/* Stacked, with the action that ends the hand furthest from a stray thumb. */
|
||||||
.potWizardActions {
|
.potWizardActions {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
@@ -2559,9 +2496,8 @@ body.hasActionBar #toastHost {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* One column, so the table has the full width and can afford to be round. */
|
|
||||||
#pokerTableWrap {
|
#pokerTableWrap {
|
||||||
--feltAspect: 1;
|
padding-bottom: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seat {
|
.seat {
|
||||||
@@ -2886,11 +2822,7 @@ body.hasActionBar #toastHost {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--feltRoom);
|
height: var(--feltRoom);
|
||||||
min-height: 7rem;
|
min-height: 7rem;
|
||||||
/* This branch sets its own height outright, so neither the aspect ratio nor the
|
padding-bottom: 0;
|
||||||
cap derived from it has anything to say here — and the cap would only narrow
|
|
||||||
a table that is already as short as the screen allows. */
|
|
||||||
aspect-ratio: auto;
|
|
||||||
max-width: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The header ate 148px of a 390px-tall window, which was most of the reason the
|
/* The header ate 148px of a 390px-tall window, which was most of the reason the
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ const ActionBar = {
|
|||||||
const visible = !host.hidden;
|
const visible = !host.hidden;
|
||||||
const height = visible ? host.getBoundingClientRect().height : 0;
|
const height = visible ? host.getBoundingClientRect().height : 0;
|
||||||
document.documentElement.style.setProperty("--actionBarHeight", `${Math.round(height)}px`);
|
document.documentElement.style.setProperty("--actionBarHeight", `${Math.round(height)}px`);
|
||||||
this.publishFeltRoom(height);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof ResizeObserver === "function") {
|
if (typeof ResizeObserver === "function") {
|
||||||
@@ -41,36 +40,6 @@ const ActionBar = {
|
|||||||
publish();
|
publish();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Half a seat, the gap left under the lowest one, and the smallest felt worth
|
|
||||||
// drawing. A seat is about 73px tall at every width — the puck is text-sized, not
|
|
||||||
// table-sized — so the overhang is a constant rather than something read back off
|
|
||||||
// the layout this is about to change. Measuring it would mean shrinking the felt,
|
|
||||||
// moving the seats, and getting a different answer on the next pass.
|
|
||||||
SEAT_OVERHANG_PX: 37,
|
|
||||||
FELT_GAP_PX: 8,
|
|
||||||
FELT_MIN_PX: 220,
|
|
||||||
|
|
||||||
// How tall the felt can be and still leave the whole ring of seats above the bar.
|
|
||||||
//
|
|
||||||
// Publishing the bar's height was only half the job. The wide layout never used it:
|
|
||||||
// the felt sizes itself from its own width — a box 72% as tall as it is wide — and
|
|
||||||
// so has no idea what is underneath it. The bar meanwhile grows with the hand, from
|
|
||||||
// 117px to 172px the moment there's a side pot to show, which is exactly when the
|
|
||||||
// dealer most needs to see who is still in. Measured with two side pots, the bottom
|
|
||||||
// seat sat 22px into the bar at 1280x900, 54px at 1600x1000, 23px at 768x1024.
|
|
||||||
//
|
|
||||||
// The wrap's offset is taken in document space rather than viewport space, so
|
|
||||||
// scrolling the page doesn't resize the table under the dealer's hands.
|
|
||||||
publishFeltRoom(barHeight) {
|
|
||||||
const wrap = document.getElementById("pokerTableWrap");
|
|
||||||
if (!wrap) return;
|
|
||||||
const topInDocument = wrap.getBoundingClientRect().top + window.scrollY;
|
|
||||||
const room = window.innerHeight - barHeight - topInDocument -
|
|
||||||
this.SEAT_OVERHANG_PX - this.FELT_GAP_PX;
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--feltRoomPx", `${Math.max(this.FELT_MIN_PX, Math.round(room))}px`);
|
|
||||||
},
|
|
||||||
|
|
||||||
// Anything to act on? A hand with nobody dealt in doesn't get a bar — and that
|
// Anything to act on? A hand with nobody dealt in doesn't get a bar — and that
|
||||||
// still holds during setup, because a seated player is a live seat whether or not
|
// still holds during setup, because a seated player is a live seat whether or not
|
||||||
// the betting has been opened. An empty table stays uncluttered.
|
// the betting has been opened. An empty table stays uncluttered.
|
||||||
|
|||||||
@@ -117,20 +117,6 @@ const Betting = {
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
// A player still in the hand with chips in the pot and nothing left behind. This is
|
|
||||||
// the same stack test nextToAct uses to pass over them, which is the point: the
|
|
||||||
// seat the felt marks all-in is exactly the seat the dealer will never be asked to
|
|
||||||
// act on again.
|
|
||||||
//
|
|
||||||
// The "chips in the pot" half matters. A seat with an empty stack who hasn't put
|
|
||||||
// anything in is a busted player who shouldn't have been dealt in, not a player
|
|
||||||
// who moved all-in, and calling them all-in on the felt would be a lie.
|
|
||||||
isAllIn(state, player) {
|
|
||||||
if (!player || !player.inHand || player.folded) return false;
|
|
||||||
return this.handTotal(player) > this.EPSILON_CHIPS &&
|
|
||||||
this.stackOf(state, player) <= this.EPSILON_CHIPS;
|
|
||||||
},
|
|
||||||
|
|
||||||
// Which chair sits `step` places along in the direction of play. The table owns
|
// Which chair sits `step` places along in the direction of play. The table owns
|
||||||
// this — walking the seats is exactly where the button, the blinds and the action
|
// this — walking the seats is exactly where the button, the blinds and the action
|
||||||
// used to drift apart from each other, so there is one implementation of it.
|
// used to drift apart from each other, so there is one implementation of it.
|
||||||
|
|||||||
@@ -174,18 +174,8 @@ const Render = {
|
|||||||
node.querySelector(".seatChairNumber").textContent = index + 1;
|
node.querySelector(".seatChairNumber").textContent = index + 1;
|
||||||
|
|
||||||
const dealerChip = node.querySelector(".dealerChip");
|
const dealerChip = node.querySelector(".dealerChip");
|
||||||
const hasButton = index === dealerIndex;
|
dealerChip.classList.toggle("active", index === dealerIndex);
|
||||||
dealerChip.classList.toggle("active", hasButton);
|
|
||||||
dealerChip.classList.toggle("hidden", !!this.state.dedicatedDealer);
|
dealerChip.classList.toggle("hidden", !!this.state.dedicatedDealer);
|
||||||
// Every one of these announced itself as "D", so a screen reader heard a row
|
|
||||||
// of nine identical buttons and couldn't tell which seat held the real one.
|
|
||||||
// The label now says whose it is, or what pressing it would do.
|
|
||||||
const who = (player.name || "").trim() || `seat ${index + 1}`;
|
|
||||||
const chipLabel = hasButton
|
|
||||||
? `${who} has the dealer button`
|
|
||||||
: `Give the dealer button to ${who}`;
|
|
||||||
dealerChip.setAttribute("aria-label", chipLabel);
|
|
||||||
dealerChip.title = chipLabel;
|
|
||||||
// Tapping the puck is the same act as "Give the button" in the seat menu,
|
// Tapping the puck is the same act as "Give the button" in the seat menu,
|
||||||
// so it goes through the same hook — otherwise this one path would move
|
// so it goes through the same hook — otherwise this one path would move
|
||||||
// the button without moving the blinds that follow it.
|
// the button without moving the blinds that follow it.
|
||||||
@@ -279,18 +269,11 @@ const Render = {
|
|||||||
totalEl.textContent = total > 0 ? Money.format(total) : "";
|
totalEl.textContent = total > 0 ? Money.format(total) : "";
|
||||||
this.fitAmount(totalEl);
|
this.fitAmount(totalEl);
|
||||||
|
|
||||||
// A player with nothing behind used to leave this line blank, so the felt drew
|
|
||||||
// an all-in exactly like a seat whose stack nobody had entered. On a table with
|
|
||||||
// side pots that's the one distinction the dealer is working from — who can
|
|
||||||
// still be bet at, and who is already committed for everything they have.
|
|
||||||
const stackEl = node.querySelector(".seatPuckStack");
|
const stackEl = node.querySelector(".seatPuckStack");
|
||||||
const stack = Betting.stackOf(this.state, player);
|
const stack = Betting.stackOf(this.state, player);
|
||||||
const allIn = Betting.isAllIn(this.state, player);
|
stackEl.textContent = !empty && stack > 0 ? Money.format(stack) : "";
|
||||||
stackEl.textContent = empty ? "" : allIn ? "ALL IN" : stack > 0 ? Money.format(stack) : "";
|
|
||||||
stackEl.classList.toggle("seatPuckAllIn", allIn);
|
|
||||||
this.fitAmount(stackEl);
|
this.fitAmount(stackEl);
|
||||||
|
|
||||||
node.classList.toggle("seatAllIn", allIn);
|
|
||||||
node.classList.toggle("seatActing", index === this.state.actingSeatIndex);
|
node.classList.toggle("seatActing", index === this.state.actingSeatIndex);
|
||||||
node.classList.toggle("seatEmpty", empty);
|
node.classList.toggle("seatEmpty", empty);
|
||||||
node.classList.toggle("seatInactive", !player.inHand && !empty);
|
node.classList.toggle("seatInactive", !player.inHand && !empty);
|
||||||
|
|||||||
@@ -40,22 +40,6 @@ const Shortcuts = {
|
|||||||
return [...document.querySelectorAll('[id$="Overlay"]')].some(el => !el.hidden);
|
return [...document.querySelectorAll('[id$="Overlay"]')].some(el => !el.hidden);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Enter and Space are how a keyboard presses the control it is already sitting on.
|
|
||||||
// Claiming them for "advance" didn't just block that press — it ran a different
|
|
||||||
// action instead. Measured: Tab to Resolve Pot, press Enter, and the table dealt
|
|
||||||
// the next hand. Space on the same button worked, because "advance" happened to
|
|
||||||
// decline that time and the browser's own activation got through. A control that
|
|
||||||
// does something else every other press is worse than one that does nothing.
|
|
||||||
//
|
|
||||||
// Only these two keys are handed back. The letter shortcuts stay live with a
|
|
||||||
// button focused, because a button does nothing with "f" and the dealer's hand
|
|
||||||
// shouldn't have to leave them.
|
|
||||||
focusedControlOwnsKey(key) {
|
|
||||||
if (key !== "enter" && key !== " ") return false;
|
|
||||||
const el = document.activeElement;
|
|
||||||
return !!(el && el.matches && el.matches('button, [role="button"], a[href], summary'));
|
|
||||||
},
|
|
||||||
|
|
||||||
onKey(e) {
|
onKey(e) {
|
||||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||||
|
|
||||||
@@ -63,8 +47,6 @@ const Shortcuts = {
|
|||||||
const action = this.KEYS[key];
|
const action = this.KEYS[key];
|
||||||
if (!action) return;
|
if (!action) return;
|
||||||
|
|
||||||
if (this.focusedControlOwnsKey(key)) return;
|
|
||||||
|
|
||||||
// Escape is the exception: it should still close the raise pad even if focus
|
// Escape is the exception: it should still close the raise pad even if focus
|
||||||
// happens to be sitting in its amount field.
|
// happens to be sitting in its amount field.
|
||||||
if (this.isTyping() && !(action === "cancel")) return;
|
if (this.isTyping() && !(action === "cancel")) return;
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
// Who the felt marks "ALL IN".
|
|
||||||
//
|
|
||||||
// The mark's whole value is that a dealer can scan the ring and see who can still be
|
|
||||||
// bet at, so the test that matters most is the last one: the seats it marks are
|
|
||||||
// exactly the seats the betting round refuses to ask for another chip.
|
|
||||||
const test = require("node:test");
|
|
||||||
const assert = require("node:assert");
|
|
||||||
const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load");
|
|
||||||
|
|
||||||
test("a player who moved all-in is marked", () => {
|
|
||||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
|
||||||
state.seats[3].stack = 12;
|
|
||||||
Betting.beginHand(state);
|
|
||||||
Betting.allIn(state, state.actingSeatIndex);
|
|
||||||
|
|
||||||
assert.equal(Betting.isAllIn(state, state.seats[3]), true);
|
|
||||||
assert.equal(state.seats[3].stack, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a player with chips behind is not", () => {
|
|
||||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
|
||||||
Betting.beginHand(state);
|
|
||||||
assert.equal(Betting.isAllIn(state, state.seats[0]), false);
|
|
||||||
assert.equal(Betting.isAllIn(state, state.seats[1]), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a busted player who was dealt in but has put nothing in is not", () => {
|
|
||||||
// An empty stack alone isn't all-in — it's someone who shouldn't have been dealt
|
|
||||||
// in. Calling that "ALL IN" on the felt would be a lie about a live pot.
|
|
||||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
|
||||||
state.seats[2].stack = 0;
|
|
||||||
assert.equal(Betting.isAllIn(state, state.seats[2]), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a folded player with an empty stack is not", () => {
|
|
||||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
|
||||||
state.seats[1].committed = 5;
|
|
||||||
state.seats[1].stack = 0;
|
|
||||||
state.seats[1].folded = true;
|
|
||||||
assert.equal(Betting.isAllIn(state, state.seats[1]), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("an empty chair is not", () => {
|
|
||||||
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
|
||||||
assert.equal(Betting.isAllIn(state, state.seats[4]), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a seat with no stack of its own falls back to the table's and isn't marked", () => {
|
|
||||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
|
||||||
state.defaultStack = 200;
|
|
||||||
state.seats[2].stack = null;
|
|
||||||
state.seats[2].committed = 10;
|
|
||||||
assert.equal(Betting.isAllIn(state, state.seats[2]), false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the seats marked are exactly the ones the action passes over", () => {
|
|
||||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
|
||||||
state.seats[1].stack = 8;
|
|
||||||
state.seats[3].stack = 30;
|
|
||||||
Betting.beginHand(state);
|
|
||||||
|
|
||||||
// Everyone in, two of them for less than the others can cover.
|
|
||||||
for (let step = 0; step < 4; step++) Betting.allIn(state, state.actingSeatIndex);
|
|
||||||
|
|
||||||
const marked = state.seats
|
|
||||||
.map((player, index) => (Betting.isAllIn(state, player) ? index : null))
|
|
||||||
.filter(index => index !== null);
|
|
||||||
const skipped = state.seats
|
|
||||||
.map((player, index) =>
|
|
||||||
player.inHand && !player.folded && Betting.nextToAct(state, index - 1) !== index ? index : null)
|
|
||||||
.filter(index => index !== null);
|
|
||||||
|
|
||||||
assert.deepEqual(marked, skipped);
|
|
||||||
assert.ok(marked.length > 0, "the scenario should actually produce all-in players");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("nobody is marked once the next hand starts", () => {
|
|
||||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 100 });
|
|
||||||
state.seats[2].stack = 5;
|
|
||||||
Betting.beginHand(state);
|
|
||||||
Betting.allIn(state, state.actingSeatIndex);
|
|
||||||
assert.ok(state.seats.some(player => Betting.isAllIn(state, player)));
|
|
||||||
|
|
||||||
// The short stack got there by winning nothing; give them chips back the way a
|
|
||||||
// dealer would before the next deal.
|
|
||||||
HoldemState.startNextHand(state);
|
|
||||||
state.seats[2].stack = 50;
|
|
||||||
assert.equal(state.seats.filter(player => Betting.isAllIn(state, player)).length, 0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("marking nobody doesn't move a chip", () => {
|
|
||||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
|
||||||
state.seats[3].stack = 12;
|
|
||||||
Betting.beginHand(state);
|
|
||||||
const before = chipsOnTable(state);
|
|
||||||
state.seats.forEach(player => Betting.isAllIn(state, player));
|
|
||||||
assert.equal(chipsOnTable(state), before);
|
|
||||||
});
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
// The keyboard shortcuts, and the one rule that keeps them from stealing a press.
|
|
||||||
//
|
|
||||||
// Shortcuts talks to `document` directly, so these tests stand a small fake one up
|
|
||||||
// rather than a whole DOM: the only things it asks about are what has focus and
|
|
||||||
// whether an overlay is open.
|
|
||||||
const test = require("node:test");
|
|
||||||
const assert = require("node:assert");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const Shortcuts = require(path.join(
|
|
||||||
__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "js", "casino", "holdem", "shortcuts.js"));
|
|
||||||
|
|
||||||
// A stand-in for a focused element. `matches` answers the same selector list the
|
|
||||||
// real one would, which is the whole of what focusedControlOwnsKey looks at.
|
|
||||||
function element(tagName, { selectors = [], isContentEditable = false } = {}) {
|
|
||||||
return {
|
|
||||||
tagName,
|
|
||||||
isContentEditable,
|
|
||||||
matches: selector => selectors.some(s => selector.includes(s)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const BUTTON = element("BUTTON", { selectors: ["button"] });
|
|
||||||
const SUMMARY = element("SUMMARY", { selectors: ["summary"] });
|
|
||||||
const LINK = element("A", { selectors: ["a[href]"] });
|
|
||||||
const DIV = element("DIV");
|
|
||||||
const TEXT_FIELD = element("INPUT");
|
|
||||||
|
|
||||||
// Runs one keypress against a fresh Shortcuts and reports which action fired.
|
|
||||||
function press(key, { focused = DIV, overlays = [] } = {}) {
|
|
||||||
const fired = [];
|
|
||||||
let defaultPrevented = false;
|
|
||||||
|
|
||||||
globalThis.document = {
|
|
||||||
activeElement: focused,
|
|
||||||
addEventListener() {},
|
|
||||||
querySelectorAll: () => overlays,
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlers = {};
|
|
||||||
for (const action of Object.values(Shortcuts.KEYS)) {
|
|
||||||
handlers[action] = () => { fired.push(action); return true; };
|
|
||||||
}
|
|
||||||
Shortcuts.handlers = handlers;
|
|
||||||
Shortcuts.onKey({ key, preventDefault: () => { defaultPrevented = true; } });
|
|
||||||
|
|
||||||
return { fired: fired[0] || null, defaultPrevented };
|
|
||||||
}
|
|
||||||
|
|
||||||
test.after(() => { delete globalThis.document; });
|
|
||||||
|
|
||||||
test("Enter on a focused button is left to the button", () => {
|
|
||||||
assert.equal(press("Enter", { focused: BUTTON }).fired, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Space on a focused button is left to the button", () => {
|
|
||||||
assert.equal(press(" ", { focused: BUTTON }).fired, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a key left to the control is not swallowed either", () => {
|
|
||||||
// preventDefault would stop the browser activating the button, which is the
|
|
||||||
// half of the bug that turned "wrong action" into "no action".
|
|
||||||
assert.equal(press("Enter", { focused: BUTTON }).defaultPrevented, false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("summary and links own Enter and Space too", () => {
|
|
||||||
assert.equal(press("Enter", { focused: SUMMARY }).fired, null);
|
|
||||||
assert.equal(press(" ", { focused: LINK }).fired, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Space still advances when nothing pressable has focus", () => {
|
|
||||||
assert.equal(press(" ", { focused: DIV }).fired, "advance");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Enter still advances when nothing pressable has focus", () => {
|
|
||||||
assert.equal(press("Enter", { focused: DIV }).fired, "advance");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("the letter shortcuts stay live with a button focused", () => {
|
|
||||||
// A button does nothing with "f", so there is no press to hand back — and a
|
|
||||||
// dealer whose hand is on the keys shouldn't lose them to a stray focus ring.
|
|
||||||
assert.equal(press("f", { focused: BUTTON }).fired, "fold");
|
|
||||||
assert.equal(press("c", { focused: BUTTON }).fired, "checkOrCall");
|
|
||||||
assert.equal(press("r", { focused: BUTTON }).fired, "raise");
|
|
||||||
assert.equal(press("a", { focused: BUTTON }).fired, "allIn");
|
|
||||||
assert.equal(press("u", { focused: BUTTON }).fired, "undo");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("typing still silences everything but Escape", () => {
|
|
||||||
assert.equal(press("f", { focused: TEXT_FIELD }).fired, null);
|
|
||||||
assert.equal(press("Escape", { focused: TEXT_FIELD }).fired, "cancel");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("an open overlay still owns the keyboard", () => {
|
|
||||||
assert.equal(press("f", { focused: DIV, overlays: [{ hidden: false }] }).fired, null);
|
|
||||||
assert.equal(press("f", { focused: DIV, overlays: [{ hidden: true }] }).fired, "fold");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("a focused control wins before the overlay check, not after", () => {
|
|
||||||
// Both would decline here; the point is that neither path fires an action.
|
|
||||||
assert.equal(press("Enter", { focused: BUTTON, overlays: [{ hidden: false }] }).fired, null);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("modified keys are never shortcuts", () => {
|
|
||||||
globalThis.document = { activeElement: DIV, addEventListener() {}, querySelectorAll: () => [] };
|
|
||||||
const fired = [];
|
|
||||||
Shortcuts.handlers = { advance: () => { fired.push("advance"); return true; } };
|
|
||||||
Shortcuts.onKey({ key: " ", ctrlKey: true, preventDefault() {} });
|
|
||||||
assert.deepEqual(fired, []);
|
|
||||||
});
|
|
||||||
@@ -514,34 +514,6 @@ const LayoutAudit = {
|
|||||||
return { name: "a seat's centre belongs to that seat", offenders };
|
return { name: "a seat's centre belongs to that seat", offenders };
|
||||||
},
|
},
|
||||||
|
|
||||||
// A poker table has one dealer button. Nine pucks all showing "D", one of them
|
|
||||||
// gold, turns the most-read marker on the felt into a spot-the-difference — and
|
|
||||||
// a screen reader heard nine buttons named "D" with nothing to tell them apart.
|
|
||||||
// Both halves are checked here: what's drawn, and what it announces itself as.
|
|
||||||
oneDealerButton() {
|
|
||||||
const chips = [...document.querySelectorAll(".dealerChip")];
|
|
||||||
const shown = chips.filter(chip => {
|
|
||||||
const style = getComputedStyle(chip);
|
|
||||||
return style.display !== "none" && style.visibility !== "hidden" &&
|
|
||||||
parseFloat(style.opacity) > 0.05;
|
|
||||||
});
|
|
||||||
const offenders = [];
|
|
||||||
|
|
||||||
if (shown.length > 1) {
|
|
||||||
offenders.push(`${shown.length} dealer buttons drawn on the felt at once`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const labels = chips.map(chip => chip.getAttribute("aria-label") || chip.textContent.trim());
|
|
||||||
const duplicated = labels.filter((label, i) => labels.indexOf(label) !== i);
|
|
||||||
if (duplicated.length) {
|
|
||||||
offenders.push(
|
|
||||||
`${duplicated.length + 1} dealer buttons announce themselves as "${duplicated[0]}"`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { name: "the felt shows one dealer button", offenders };
|
|
||||||
},
|
|
||||||
|
|
||||||
// ---- Runner ----
|
// ---- Runner ----
|
||||||
|
|
||||||
runAll(options = {}) {
|
runAll(options = {}) {
|
||||||
@@ -555,7 +527,6 @@ const LayoutAudit = {
|
|||||||
this.horizontalOverflow(),
|
this.horizontalOverflow(),
|
||||||
this.fieldFontSize(),
|
this.fieldFontSize(),
|
||||||
this.seatOwnership(),
|
this.seatOwnership(),
|
||||||
this.oneDealerButton(),
|
|
||||||
];
|
];
|
||||||
if (options.includeTouchTargets) checks.push(this.touchTargets(options.minimumTarget));
|
if (options.includeTouchTargets) checks.push(this.touchTargets(options.minimumTarget));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user