Say ALL IN on the felt, and keep the table above the action bar #63
@@ -308,9 +308,22 @@ 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%;
|
||||||
padding-bottom: 72%;
|
aspect-ratio: 1 / var(--feltAspect);
|
||||||
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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -2126,6 +2139,23 @@ 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
|
||||||
@@ -2504,6 +2534,16 @@ 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;
|
||||||
@@ -2519,8 +2559,9 @@ 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 {
|
||||||
padding-bottom: 100%;
|
--feltAspect: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seat {
|
.seat {
|
||||||
@@ -2845,7 +2886,11 @@ body.hasActionBar #toastHost {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: var(--feltRoom);
|
height: var(--feltRoom);
|
||||||
min-height: 7rem;
|
min-height: 7rem;
|
||||||
padding-bottom: 0;
|
/* This branch sets its own height outright, so neither the aspect ratio nor the
|
||||||
|
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,6 +29,7 @@ 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") {
|
||||||
@@ -40,6 +41,36 @@ 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,6 +117,20 @@ 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.
|
||||||
|
|||||||
@@ -279,11 +279,18 @@ 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);
|
||||||
stackEl.textContent = !empty && stack > 0 ? Money.format(stack) : "";
|
const allIn = Betting.isAllIn(this.state, player);
|
||||||
|
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);
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// 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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user