Ask for the hands at the showdown, the way the streets are already asked for
The card feature had two halves and only one of them ever asked a question. Turn the
setting on and every street opens a sheet for the cards coming off the deck — that
worked, on a phone and on a desk, in a normal hand and in an all-in run-out. The
other half never asked at all. A dealer who entered all five community cards, pressed
"Showdown — resolve pot", and got a wizard saying
Christopher, Mike from work, Alexandra and Bob haven't turned over, so who
won is still yours to say.
with nothing in that sheet to turn anybody over with. The only route in was to close
the wizard, tap a seat puck, find the two card slots in the seat editor, do it four
times and re-open the wizard, with the pot on the table and four people waiting. The
one moment the tool is certain to know the hands is the moment it stopped asking.
So the showdown button asks. The sheet opens on whoever the bar has just named as
showing first, takes their two cards, and moves round the table from there; hands
already entered are skipped, because a hand entered is a question already answered.
Four sheets later the wizard opens reading "Chris wins $40 with three of a kind,
nines" and its button says "Pay Chris" instead of "Confirm Winner(s)".
Leaving the sheet stops the asking rather than skipping a player: closing it says the
tool has the wrong end of the stick, and the answer to that is to get out of the way.
It isn't a dead end — the wizard still names whoever didn't turn over, the bar still
says "Showdown — resolve pot", and pressing it again resumes at the player the dealer
stopped on, because the list is read off the cards on the table rather than remembered.
CardPicker.close() takes a reason so the chain can tell "that sheet filled its last
slot" from "the dealer left". Two callers pass nothing and don't care.
The setting kept its behaviour and lost its jargon: "Track the board and read the
showdown" was the feature's name rather than what turning it on does to your evening.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -571,6 +571,56 @@ narrowing fix has to be shown not to have blinded the thing it quietened, so the
|
||||
broken versions are re-run **with the Keyboard panel hidden** — the condition that was
|
||||
breaking it — and each is still caught, while the unbroken page stays clean.
|
||||
|
||||
## Half a feature, and the half that was missing
|
||||
|
||||
The card feature has always had two halves. Turn on the setting and every street asks
|
||||
which cards came off the deck, and puts them on the felt. That half worked, on the
|
||||
phone and on the desk, in a normal hand and in an all-in run-out.
|
||||
|
||||
The other half never got asked for at all. A dealer who entered all five community
|
||||
cards, pressed **Showdown — resolve pot**, and got a wizard that said
|
||||
|
||||
> Christopher, Mike from work, Alexandra and Bob haven't turned over, so who won is
|
||||
> still yours to say.
|
||||
|
||||
with nothing anywhere in that sheet to turn anybody over with. The only route in was to
|
||||
close the wizard, tap a seat puck, find the two card slots in the seat editor, do it
|
||||
four times, and re-open the wizard — with the pot on the table and four people waiting.
|
||||
The one moment the tool is certain to know the hands is the moment it stopped asking.
|
||||
|
||||
So the showdown button now asks, exactly the way the street button does: the sheet opens
|
||||
on the first player, takes two cards, and moves to the next. `showdownCards.test.js` is
|
||||
the list it asks from — who is on it and in what order — and three rules hold it
|
||||
together, one test each way:
|
||||
|
||||
**Only when the dealer asked.** The setting is off by default, and with it off the list
|
||||
is empty. A table that never wanted this never sees a sheet.
|
||||
|
||||
**Only what isn't known.** A hand already entered is an answer already given. Asking
|
||||
again offers to overwrite it, so an entered hand drops out of the list and the rest keep
|
||||
their order.
|
||||
|
||||
**In the order the cards come over.** The bar already announces "Alexandra shows first —
|
||||
last to bet or raise" a line above the button. Asking about somebody else first would be
|
||||
the tool contradicting itself in the same breath, so the list starts where the bar says
|
||||
and goes round the table from there.
|
||||
|
||||
### The sheet has to be leavable, and leaving it has to mean something
|
||||
|
||||
Escape, Done and a tap on the scrim all closed the picker already. What they didn't have
|
||||
was a way to say *why*, and a chain of four sheets needs to know the difference: a sheet
|
||||
that filled its last slot means "next player", and a dealer closing one means "stop".
|
||||
|
||||
So `close()` takes a reason. Two callers pass nothing and don't care. The showdown chain
|
||||
does, and it stops rather than skipping — a dealer who closes the sheet is saying the
|
||||
tool has the wrong end of the stick, and the answer to that is to get out of the way, not
|
||||
to put the same sheet in front of them three more times.
|
||||
|
||||
Stopping early is not a dead end, which is the part worth checking by hand. The wizard
|
||||
still names whoever didn't turn over; closing it leaves **Showdown — resolve pot** on the
|
||||
bar; and pressing it again resumes at the player the dealer stopped on, because the list
|
||||
is computed from the cards on the table rather than remembered from the last attempt.
|
||||
|
||||
## Why the hand evaluator is tested the way it is
|
||||
|
||||
`handRank.js` is the only module here that can pay the wrong person without anybody
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
// Who the tool asks to turn over, and in what order.
|
||||
//
|
||||
// The board half of the card feature always worked: every street, a sheet asks which
|
||||
// cards came off the deck. The hands half never did. A dealer entered five community
|
||||
// cards, pressed the showdown button, and the pot wizard told them "Chris, Mike and
|
||||
// Alex haven't turned over, so who won is still yours to say" — with nothing in the
|
||||
// sheet to turn anybody over with. The only way in was to close the wizard, tap a seat
|
||||
// puck, find the card slots in the seat editor, and repeat it for each player, with the
|
||||
// pot sitting on the table.
|
||||
//
|
||||
// So the showdown button now asks, the same way the street button does. This file is
|
||||
// the list it asks from. The sheet itself is DOM and is checked in the browser; the
|
||||
// question of who is on the list, and in which order, is state and lives here.
|
||||
//
|
||||
// Three rules hold it together:
|
||||
//
|
||||
// Only when asked for. The setting is off by default and the list is empty when it
|
||||
// is off, so a table that never wanted this never sees a sheet.
|
||||
//
|
||||
// Only what isn't known. A hand already entered is an answer already given, and
|
||||
// asking again offers to overwrite it.
|
||||
//
|
||||
// In the order the cards actually come over — the showdown order the action bar
|
||||
// already announces, then round the table from there. A dealer looking at the seat
|
||||
// the bar just named should not be asked about the one behind them.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Render, Betting, HoldemState, makeTable } = require("./load");
|
||||
|
||||
// A hand played to the river with everyone still in, which is the only shape that
|
||||
// has a showdown to ask about.
|
||||
const atShowdown = ({ seats = 4, tracking = true } = {}) => {
|
||||
const state = makeTable({ seatCount: seats, stack: 500, smallBlind: 5, bigBlind: 10 });
|
||||
Betting.beginHand(state);
|
||||
state.street = "river";
|
||||
state.board = ["2C", "7D", "9S", "JH", "3S"];
|
||||
state.trackBoard = tracking;
|
||||
state.seats.forEach(player => { player.hasActed = true; });
|
||||
return state;
|
||||
};
|
||||
|
||||
const queue = state => Render.showdownCardQueue(state);
|
||||
|
||||
describe("only when the dealer asked to be asked", () => {
|
||||
test("the setting off means nobody is asked anything", () => {
|
||||
assert.deepStrictEqual(queue(atShowdown({ tracking: false })), []);
|
||||
});
|
||||
|
||||
test("the setting on asks everyone still in", () => {
|
||||
assert.strictEqual(queue(atShowdown()).length, 4);
|
||||
});
|
||||
|
||||
test("a table nobody is sitting at is not a showdown", () => {
|
||||
const state = atShowdown();
|
||||
state.seats.forEach(player => { player.inHand = false; });
|
||||
assert.deepStrictEqual(queue(state), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("a hand won without a showdown", () => {
|
||||
// Everyone else folded, so there is one hand left and it never comes over. The
|
||||
// bar says "Award pot to Chris" here, not "Showdown", and asking Chris to turn
|
||||
// over a hand nobody paid to see is asking for a card the tool will never use.
|
||||
test("one player left is asked for nothing", () => {
|
||||
const state = atShowdown();
|
||||
[1, 2, 3].forEach(i => { state.seats[i].folded = true; });
|
||||
assert.deepStrictEqual(queue(state), []);
|
||||
});
|
||||
|
||||
test("two left is a showdown, however it was arrived at", () => {
|
||||
const state = atShowdown();
|
||||
[2, 3].forEach(i => { state.seats[i].folded = true; });
|
||||
assert.strictEqual(queue(state).length, 2);
|
||||
});
|
||||
|
||||
// Everybody all-in on the flop is still a showdown: the cards go on their backs,
|
||||
// the board runs out, and the hands are read exactly as they would have been.
|
||||
test("all-in with the betting finished is still asked about", () => {
|
||||
const state = atShowdown();
|
||||
state.seats.forEach(player => { player.stack = 0; });
|
||||
assert.ok(!Betting.canStillBet(state));
|
||||
assert.strictEqual(queue(state).length, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("only the hands it doesn't already have", () => {
|
||||
test("a hand already entered is not asked for again", () => {
|
||||
const state = atShowdown();
|
||||
state.seats[1].cards = ["AS", "AH"];
|
||||
assert.ok(!queue(state).includes(1));
|
||||
});
|
||||
|
||||
test("half a hand entered is still asked for", () => {
|
||||
const state = atShowdown();
|
||||
state.seats[1].cards = ["AS", ""];
|
||||
assert.ok(queue(state).includes(1));
|
||||
});
|
||||
|
||||
test("every hand entered means the sheet never opens", () => {
|
||||
const state = atShowdown();
|
||||
const hands = [["AS", "AH"], ["KS", "KH"], ["QS", "QH"], ["9H", "9D"]];
|
||||
state.seats.forEach((player, i) => { player.cards = hands[i]; });
|
||||
assert.deepStrictEqual(queue(state), []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("in the order the cards come over", () => {
|
||||
// The bar already says "Mike shows first — last to bet or raise" out loud. The
|
||||
// sheet asking about somebody else first would be the tool contradicting itself
|
||||
// in the same breath.
|
||||
test("it starts with whoever the bar says shows first", () => {
|
||||
const state = atShowdown();
|
||||
state.streetAggressorIndex = 2;
|
||||
assert.strictEqual(Betting.showdownOrder(state).index, 2);
|
||||
assert.strictEqual(queue(state)[0], 2);
|
||||
});
|
||||
|
||||
test("and then goes round the table from there", () => {
|
||||
const state = atShowdown();
|
||||
state.streetAggressorIndex = 2;
|
||||
assert.deepStrictEqual(queue(state), [2, 3, 0, 1]);
|
||||
});
|
||||
|
||||
test("nobody checked the river, so it starts left of the button", () => {
|
||||
const state = atShowdown();
|
||||
state.streetAggressorIndex = null;
|
||||
state.handAggressorIndex = null;
|
||||
const first = Betting.showdownOrder(state);
|
||||
assert.strictEqual(first.reason, "leftOfButton");
|
||||
assert.strictEqual(queue(state)[0], first.index);
|
||||
});
|
||||
|
||||
test("a player already entered drops out of the order without moving the rest", () => {
|
||||
const state = atShowdown();
|
||||
state.streetAggressorIndex = 2;
|
||||
state.seats[3].cards = ["KS", "KH"];
|
||||
assert.deepStrictEqual(queue(state), [2, 0, 1]);
|
||||
});
|
||||
|
||||
test("everyone still in is asked exactly once", () => {
|
||||
const state = atShowdown({ seats: 9 });
|
||||
const asked = queue(state);
|
||||
assert.strictEqual(new Set(asked).size, asked.length);
|
||||
assert.strictEqual(asked.length, Betting.liveSeats(state).length);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user