Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42b9e83e8f | ||
|
|
470fbc0d26 | ||
|
|
c4e842f3e3 | ||
|
|
1d9353c94e | ||
|
|
8616636212 | ||
|
|
6174981cdc | ||
|
|
4db6f6e4d3 | ||
|
|
0ad5592687 | ||
|
|
236c52567e | ||
|
|
b8655be258 | ||
|
|
7e420ce747 | ||
|
|
c36d7f60bd | ||
|
|
15cec18256 | ||
|
|
9c8d981e2d | ||
|
|
907e0165db | ||
|
|
86e1570618 | ||
|
|
6df400322e | ||
|
|
ca9ac7b6af | ||
|
|
80dae56f0d | ||
|
|
6ca99fde6b | ||
|
|
b2262f8604 | ||
|
|
6ad9923aae | ||
|
|
168e8a380c | ||
|
|
edc08d7213 |
@@ -318,6 +318,20 @@ button,
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
/* `hidden` means hidden.
|
||||
|
||||
The attribute only carries `display: none` from the browser's own stylesheet, which
|
||||
is the weakest rule there is: any class that sets a display — and every button here
|
||||
sets `inline-flex` — beats it. That is not a hypothetical. Seven overlays in this
|
||||
file already carry their own `[hidden] { display: none }` because each one was found
|
||||
the hard way, and the eighth was a button that shipped as a 21x44 empty box under the
|
||||
bank check on every table that didn't need it.
|
||||
|
||||
One rule, for everything, so there is no ninth. */
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ---------- Poker table ---------- */
|
||||
|
||||
#pokerTableWrap {
|
||||
@@ -608,6 +622,14 @@ button,
|
||||
margin-top: 0.6rem;
|
||||
}
|
||||
|
||||
/* The button's chip sits in this puck's top-right corner, on a layer above the text.
|
||||
Reserving its width means a long name ends in an ellipsis before it reaches the
|
||||
chip, rather than disappearing underneath one — on the one seat at the table a
|
||||
dealer looks at most. */
|
||||
.seat.seatHasButton .seatPuckName {
|
||||
padding-right: 22px;
|
||||
}
|
||||
|
||||
.seatPuckCards {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
@@ -2338,6 +2360,9 @@ body.hasActionBar #holdemSidebar {
|
||||
.waitingAdd .holdemBtn {
|
||||
flex: none;
|
||||
min-height: 44px;
|
||||
/* "Add" is three letters, which came out 43px wide — one pixel under the rule, and
|
||||
the audit called it. A thumb doesn't know the word is short. */
|
||||
min-width: 56px;
|
||||
}
|
||||
|
||||
.waitingList {
|
||||
|
||||
@@ -459,6 +459,66 @@ const Betting = {
|
||||
return (state.street || "preflop") === "preflop" && state.seats.every(p => !p.hasActed);
|
||||
},
|
||||
|
||||
// Are the blinds actually down? Moving the button or naming a blind means two
|
||||
// completely different things depending on the answer, and `onlyBlindsIn` alone
|
||||
// cannot tell them apart: a table sitting in setup, with no cards out and nothing
|
||||
// committed, satisfies it just as well as a hand that has only just been dealt.
|
||||
//
|
||||
// With a hand open, the blinds are chips on the felt. Re-aiming them has to take
|
||||
// those chips back and post them again from the new positions, or the badges and
|
||||
// the money disagree.
|
||||
//
|
||||
// Before the deal it is a marker being slid round a table with nothing in the
|
||||
// middle. Re-posting there would *deal the hand* — betting opened, blinds taken off
|
||||
// two stacks, action on the seat after the big blind — which is not what a dealer
|
||||
// means by "she's the big blind this one". Setting the positions is how a dealer
|
||||
// gets ready to deal; the cards come out when they press the button, and not a
|
||||
// moment before.
|
||||
blindsArePosted(state) {
|
||||
return !!state.bettingOpen && this.onlyBlindsIn(state);
|
||||
},
|
||||
|
||||
// Chips the pot has been priced with but nobody has actually paid.
|
||||
//
|
||||
// Two ways a contribution gets into this tool. Tapping the action moves chips: the
|
||||
// bet leaves the stack, sits in front of the player, and is swept into their hand
|
||||
// total at the end of the street. The other way is the "without live betting" panel
|
||||
// — a base pot amount, or chips counted out on a seat — where the dealer is
|
||||
// recording a hand they are watching rather than driving, and nothing ever leaves
|
||||
// anybody's stack.
|
||||
//
|
||||
// The pot wizard prices both the same way, and awarding it credits the winner. So
|
||||
// in that second mode the table simply grew: measured at a three-handed table with
|
||||
// a $50 base pot, paying the pot took the chips on the table from $600 to $750 and
|
||||
// left the bank check blaming a rebuy that never happened.
|
||||
//
|
||||
// So before the pot is pushed, whatever was assumed rather than taken comes off the
|
||||
// stacks that assumed it. Chips already committed or swept are left alone; they
|
||||
// have been paid once already, and taking them twice is the same bug the other way
|
||||
// round.
|
||||
collectAssumedContributions(state) {
|
||||
const taken = [];
|
||||
state.seats.forEach((player, index) => {
|
||||
const priced = HoldemState.computeContribution(player, state.chipLegend, state.basePotAmount);
|
||||
const alreadyPaid = this.round(this.committed(player) + (Number(player.amountOverride) || 0));
|
||||
const owed = this.round(priced - alreadyPaid);
|
||||
if (owed <= this.EPSILON_CHIPS) return;
|
||||
|
||||
// Never more than they are holding. A seat priced above its own stack is a
|
||||
// dealer's typo or a player who is short, and inventing the difference is
|
||||
// what this whole function exists to stop.
|
||||
const moved = this.round(Math.min(owed, this.stackOf(state, player)));
|
||||
if (moved <= this.EPSILON_CHIPS) return;
|
||||
|
||||
player.stack = this.round(this.stackOf(state, player) - moved);
|
||||
// Recorded where a swept bet lives, so the money is in exactly the place
|
||||
// the rest of the tool looks for chips that are in the middle this hand.
|
||||
player.amountOverride = this.round((Number(player.amountOverride) || 0) + moved);
|
||||
taken.push({ index, name: HoldemState.displayName(player, index), amount: moved });
|
||||
});
|
||||
return { taken, total: this.round(taken.reduce((sum, row) => sum + row.amount, 0)) };
|
||||
},
|
||||
|
||||
// A misdeal: the hand never happened, so every chip goes back where it came from.
|
||||
//
|
||||
// The rulings sheet in this tool has told dealers to redeal since the day it was
|
||||
|
||||
@@ -176,8 +176,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
// After it, the blinds have to follow the button or the badges and the
|
||||
// money disagree — but that's a real thing happening to real chips, and
|
||||
// the dealer gets told rather than finding out later.
|
||||
const willRepost = state.bettingOpen && Betting.onlyBlindsIn(state) &&
|
||||
HoldemState.unresolvedContributions(state) > Betting.EPSILON_CHIPS;
|
||||
const willRepost = Betting.blindsArePosted(state);
|
||||
|
||||
seatAction(index, willRepost
|
||||
? `Button on ${nameOf(index)} — blinds taken back and re-posted.`
|
||||
@@ -204,18 +203,29 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (partner === index || partner === null) partner = Betting.nextLiveIndex(state, index);
|
||||
|
||||
const label = which === "sb" ? "small blind" : "big blind";
|
||||
seatAction(index, `${nameOf(index)} posts the ${label}.`, () => {
|
||||
// Only a hand that is already open has blinds to move. Before the deal this
|
||||
// is aiming the next hand, and re-posting would have dealt it — the dealer
|
||||
// says who the blind is well before they say "cards".
|
||||
const posted = Betting.blindsArePosted(state);
|
||||
seatAction(index, posted
|
||||
? `${nameOf(index)} posts the ${label}.`
|
||||
: `${nameOf(index)} has the ${label} next hand.`, () => {
|
||||
state.blindOverride = which === "sb"
|
||||
? { sbIndex: index, bbIndex: partner }
|
||||
: { sbIndex: partner, bbIndex: index };
|
||||
if (Betting.onlyBlindsIn(state)) Betting.repostBlinds(state);
|
||||
if (posted) Betting.repostBlinds(state);
|
||||
});
|
||||
},
|
||||
|
||||
clearBlindOverride: () => seatAction(state.dealerSeatIndex, "Blinds follow the button again.", () => {
|
||||
state.blindOverride = null;
|
||||
if (Betting.onlyBlindsIn(state)) Betting.repostBlinds(state);
|
||||
}),
|
||||
clearBlindOverride: () => {
|
||||
const posted = Betting.blindsArePosted(state);
|
||||
seatAction(state.dealerSeatIndex, posted
|
||||
? "Blinds follow the button again — taken back and re-posted."
|
||||
: "Blinds follow the button again.", () => {
|
||||
state.blindOverride = null;
|
||||
if (posted) Betting.repostBlinds(state);
|
||||
});
|
||||
},
|
||||
|
||||
editSeat: index => Render.toggleSeatEditor(index),
|
||||
|
||||
@@ -261,6 +271,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
if (rake > 0) {
|
||||
state.rakeTaken = Money.round((Number(state.rakeTaken) || 0) + rake);
|
||||
}
|
||||
// Anything the pot was priced with but nobody actually paid — the
|
||||
// base pot and counted chips of the no-live-betting flow — comes off
|
||||
// the stacks that owe it before the winner is paid. Otherwise the
|
||||
// table gains the whole pot out of nowhere.
|
||||
Betting.collectAssumedContributions(state);
|
||||
if (payouts.length > 0) {
|
||||
HoldemState.recordHand(state, payouts, rake);
|
||||
// Winnings go straight back in front of the player, so the next
|
||||
|
||||
@@ -408,6 +408,12 @@ const Render = {
|
||||
const dealerChip = node.querySelector(".dealerChip");
|
||||
const hasButton = index === dealerIndex;
|
||||
dealerChip.classList.toggle("active", hasButton);
|
||||
// Marked on the seat as well as the chip, so the puck's own text can keep
|
||||
// out from under it. A name is centred across the whole puck and the chip
|
||||
// sits in the corner on a layer above: measured, "Christopher" on the
|
||||
// button lost 20px of its 64px box to the chip and showed no ellipsis,
|
||||
// because the text believed it had the room it was painted out of.
|
||||
node.classList.toggle("seatHasButton", hasButton);
|
||||
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.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Saying who the blind is, before saying "cards".
|
||||
//
|
||||
// A dealer sets the table up out loud — button here, she's the small blind, he's the
|
||||
// big blind — and only then deals. The seat menu offers "Make small blind" for exactly
|
||||
// that, and it was answering by dealing the hand: betting opened, two stacks docked,
|
||||
// the action bar up and waiting on the seat after the big blind. The dealer had said
|
||||
// where the blinds go, and the tool heard "deal".
|
||||
//
|
||||
// The condition it was asking is `onlyBlindsIn`, which means "nothing has happened in
|
||||
// this hand except the blinds" — true of a hand one second old, and equally true of a
|
||||
// table in setup with no cards out and nothing committed. Those two need telling apart,
|
||||
// which is what `blindsArePosted` is for.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load");
|
||||
|
||||
describe("before the deal", () => {
|
||||
test("the blinds are not down, however much the table looks ready", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
assert.equal(state.bettingOpen, false);
|
||||
// The old condition, on its own, says yes here — this is the whole bug.
|
||||
assert.equal(Betting.onlyBlindsIn(state), true);
|
||||
assert.equal(Betting.blindsArePosted(state), false);
|
||||
});
|
||||
|
||||
test("a table straight out of setup, with nobody seated, is the same answer", () => {
|
||||
assert.equal(Betting.blindsArePosted(HoldemState.createDefault()), false);
|
||||
});
|
||||
|
||||
test("re-posting would have dealt the hand, which is why it is guarded", () => {
|
||||
// Not a rule, a demonstration: repostBlinds ends in beginHand. Calling it in
|
||||
// setup is the deal, and nothing about naming a blind should reach it.
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
Betting.repostBlinds(state);
|
||||
assert.equal(state.bettingOpen, true, "chips off two stacks and the action bar up");
|
||||
assert.notEqual(state.actingSeatIndex, null);
|
||||
});
|
||||
|
||||
test("naming a blind moves a badge and no chips", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
const before = chipsOnTable(state);
|
||||
|
||||
// What the seat menu does: pin the override, repost only if the blinds are down.
|
||||
state.blindOverride = { sbIndex: 3, bbIndex: 0 };
|
||||
if (Betting.blindsArePosted(state)) Betting.repostBlinds(state);
|
||||
|
||||
assert.equal(state.bettingOpen, false, "still setup — the dealer hasn't dealt");
|
||||
assert.equal(state.actingSeatIndex, null, "nobody is being asked to act");
|
||||
assert.deepEqual(state.seats.map(p => Betting.committed(p)), [0, 0, 0, 0, 0, 0]);
|
||||
assert.equal(chipsOnTable(state), before);
|
||||
|
||||
// And the badges say what the dealer just said.
|
||||
const blinds = HoldemState.getBlindSeats(state);
|
||||
assert.equal(blinds.sbIndex, 3);
|
||||
assert.equal(blinds.bbIndex, 0);
|
||||
});
|
||||
|
||||
test("and the hand that follows posts from where they were aimed", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], smallBlind: 1, bigBlind: 2 });
|
||||
state.blindOverride = { sbIndex: 3, bbIndex: 0 };
|
||||
if (Betting.blindsArePosted(state)) Betting.repostBlinds(state);
|
||||
|
||||
Betting.beginHand(state);
|
||||
assert.equal(Betting.committed(state.seats[3]), 1);
|
||||
assert.equal(Betting.committed(state.seats[0]), 2);
|
||||
});
|
||||
|
||||
test("giving the button away is a marker moving too", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3] });
|
||||
const before = chipsOnTable(state);
|
||||
|
||||
const willRepost = Betting.blindsArePosted(state);
|
||||
state.dealerSeatIndex = 2;
|
||||
if (willRepost) Betting.repostBlinds(state);
|
||||
|
||||
assert.equal(willRepost, false);
|
||||
assert.equal(state.bettingOpen, false);
|
||||
assert.equal(chipsOnTable(state), before);
|
||||
});
|
||||
});
|
||||
|
||||
describe("once the hand is dealt", () => {
|
||||
test("the blinds are down, and re-aiming them moves real chips", () => {
|
||||
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
const before = chipsOnTable(state);
|
||||
Betting.beginHand(state);
|
||||
assert.equal(Betting.blindsArePosted(state), true);
|
||||
assert.equal(Betting.committed(state.seats[1]), 1);
|
||||
|
||||
state.blindOverride = { sbIndex: 3, bbIndex: 0 };
|
||||
if (Betting.blindsArePosted(state)) Betting.repostBlinds(state);
|
||||
|
||||
assert.equal(Betting.committed(state.seats[1]), 0, "the old small blind got their chip back");
|
||||
assert.equal(Betting.committed(state.seats[3]), 1);
|
||||
assert.equal(Betting.committed(state.seats[0]), 2);
|
||||
assert.equal(chipsOnTable(state), before, "no chips created or lost in the shuffle");
|
||||
});
|
||||
|
||||
test("but not once somebody has answered them", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.call(state, state.actingSeatIndex);
|
||||
assert.equal(Betting.blindsArePosted(state), false, "there is a call to unpick now");
|
||||
});
|
||||
|
||||
test("nor on a later street", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
state.street = "flop";
|
||||
assert.equal(Betting.blindsArePosted(state), false);
|
||||
});
|
||||
|
||||
test("and a misdealt hand goes back to being a table in setup", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.takeHandBack(state);
|
||||
assert.equal(Betting.blindsArePosted(state), false, "nothing is posted until the dealer deals again");
|
||||
});
|
||||
|
||||
test("as does a hand that has been paid out", () => {
|
||||
const state = makeTable({ seatCount: 4, seated: [0, 1, 2, 3], dealerSeatIndex: 0 });
|
||||
Betting.beginHand(state);
|
||||
Betting.sweepToHandTotals(state);
|
||||
HoldemState.startNextHand(state);
|
||||
assert.equal(Betting.blindsArePosted(state), false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,175 @@
|
||||
// Chips the pot was priced with that nobody actually paid.
|
||||
//
|
||||
// There are two ways a contribution gets into this tool. Tapping the action moves
|
||||
// chips: the bet leaves the stack, sits in front of the player, and is swept into their
|
||||
// hand total at the end of the street. The other is the "without live betting" panel —
|
||||
// a base pot amount, or chips counted out on a seat — where the dealer is recording a
|
||||
// hand they are watching rather than driving, and nothing ever leaves a stack.
|
||||
//
|
||||
// The pot wizard prices both the same way and awarding it credits the winner, so in
|
||||
// that second mode the table simply grew.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const { Money, Betting, HoldemState, chipsOnTable } = require("./load");
|
||||
|
||||
const basePotTable = (base = 50, stack = 200) => {
|
||||
const state = HoldemState.createDefault();
|
||||
HoldemState.resizeSeats(state, 4);
|
||||
state.basePotAmount = base;
|
||||
["Ann", "Ben", "Cal"].forEach((name, i) => {
|
||||
state.seats[i].name = name;
|
||||
state.seats[i].inHand = true;
|
||||
state.seats[i].stack = stack;
|
||||
state.seats[i].buyIns = stack;
|
||||
});
|
||||
return state;
|
||||
};
|
||||
|
||||
const pricedPot = (state) => state.seats.reduce(
|
||||
(sum, p) => sum + HoldemState.computeContribution(p, state.chipLegend, state.basePotAmount), 0);
|
||||
|
||||
describe("what nobody paid", () => {
|
||||
test("comes off the stacks that owe it", () => {
|
||||
const state = basePotTable();
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
assert.equal(collected.total, 150);
|
||||
assert.deepEqual(collected.taken.map(row => row.name), ["Ann", "Ben", "Cal"]);
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.stack), [150, 150, 150]);
|
||||
});
|
||||
|
||||
test("and lands where the rest of the tool looks for chips in the middle", () => {
|
||||
const state = basePotTable();
|
||||
Betting.collectAssumedContributions(state);
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.amountOverride), [50, 50, 50]);
|
||||
assert.equal(HoldemState.unresolvedContributions(state), 150);
|
||||
});
|
||||
|
||||
test("chips counted out on a seat are money too", () => {
|
||||
const state = basePotTable(0);
|
||||
state.seats[0].chipCounts = { red: 4 }; // four $5 chips
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
assert.equal(collected.total, 20);
|
||||
assert.equal(state.seats[0].stack, 180);
|
||||
});
|
||||
});
|
||||
|
||||
describe("what has already been paid", () => {
|
||||
test("a live bet is left exactly alone", () => {
|
||||
const state = basePotTable(0);
|
||||
state.smallBlind = 5;
|
||||
state.bigBlind = 10;
|
||||
Betting.beginHand(state);
|
||||
const before = state.seats.map(p => Betting.stackOf(state, p));
|
||||
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
assert.equal(collected.total, 0, "the blinds are already off their stacks");
|
||||
assert.deepEqual(state.seats.map(p => Betting.stackOf(state, p)), before);
|
||||
});
|
||||
|
||||
test("nor is a swept street taken a second time", () => {
|
||||
const state = basePotTable(0);
|
||||
state.smallBlind = 5;
|
||||
state.bigBlind = 10;
|
||||
Betting.beginHand(state);
|
||||
Betting.sweepToHandTotals(state);
|
||||
const before = state.seats.map(p => Betting.stackOf(state, p));
|
||||
|
||||
assert.equal(Betting.collectAssumedContributions(state).total, 0);
|
||||
assert.deepEqual(state.seats.map(p => Betting.stackOf(state, p)), before);
|
||||
});
|
||||
|
||||
test("a base pot on top of a live bet only collects the difference", () => {
|
||||
// The base pot says "assume everyone is in for fifty". Somebody who has already
|
||||
// put twenty in owes thirty of it, not fifty.
|
||||
const state = basePotTable(50);
|
||||
state.seats[0].committed = 20;
|
||||
state.seats[0].stack = 180;
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
const ann = collected.taken.find(row => row.name === "Ann");
|
||||
assert.equal(ann.amount, 30);
|
||||
assert.equal(state.seats[0].stack, 150, "fifty in the middle, either way");
|
||||
});
|
||||
});
|
||||
|
||||
describe("what it will not do", () => {
|
||||
test("take more than a player is holding", () => {
|
||||
const state = basePotTable(50);
|
||||
state.seats[1].stack = 20;
|
||||
Betting.collectAssumedContributions(state);
|
||||
assert.equal(state.seats[1].stack, 0, "all of it, and no further");
|
||||
assert.equal(state.seats[1].amountOverride, 20);
|
||||
});
|
||||
|
||||
test("touch an empty chair", () => {
|
||||
const state = basePotTable();
|
||||
const before = { ...state.seats[3] };
|
||||
Betting.collectAssumedContributions(state);
|
||||
assert.equal(state.seats[3].stack, before.stack);
|
||||
assert.equal(state.seats[3].amountOverride, before.amountOverride);
|
||||
});
|
||||
|
||||
test("or a seat that isn't in the hand", () => {
|
||||
// The base pot stands in for "everyone put the same in", and somebody sitting
|
||||
// out did not.
|
||||
const state = basePotTable();
|
||||
state.seats[2].inHand = false;
|
||||
const collected = Betting.collectAssumedContributions(state);
|
||||
assert.deepEqual(collected.taken.map(row => row.name), ["Ann", "Ben"]);
|
||||
assert.equal(state.seats[2].stack, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("the whole hand", () => {
|
||||
test("conserves the chips on the table, which it did not before", () => {
|
||||
const state = basePotTable();
|
||||
const total = chipsOnTable(state);
|
||||
const pot = pricedPot(state);
|
||||
|
||||
Betting.collectAssumedContributions(state);
|
||||
Betting.creditWinnings(state, [{ id: state.seats[0].id, label: "Ann", amount: pot }]);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(chipsOnTable(state), total, "six hundred before, six hundred after");
|
||||
assert.deepEqual(state.seats.slice(0, 3).map(p => p.stack), [300, 150, 150]);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
|
||||
test("and the bank check stays balanced all the way through", () => {
|
||||
const state = basePotTable();
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
Betting.collectAssumedContributions(state);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true, "chips are in the middle, not gone");
|
||||
Betting.creditWinnings(state, [{ id: state.seats[0].id, label: "Ann", amount: 150 }]);
|
||||
HoldemState.startNextHand(state);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
|
||||
test("a live-betting hand is unchanged by any of this", () => {
|
||||
const state = basePotTable(0);
|
||||
state.smallBlind = 5;
|
||||
state.bigBlind = 10;
|
||||
const total = chipsOnTable(state);
|
||||
Betting.beginHand(state);
|
||||
[3, 0, 1, 2].forEach(() => { if (state.actingSeatIndex !== null) Betting.call(state, state.actingSeatIndex); });
|
||||
Betting.sweepToHandTotals(state);
|
||||
const pot = HoldemState.unresolvedContributions(state);
|
||||
|
||||
Betting.collectAssumedContributions(state);
|
||||
Betting.creditWinnings(state, [{ id: state.seats[0].id, label: "Ann", amount: pot }]);
|
||||
HoldemState.startNextHand(state);
|
||||
|
||||
assert.equal(chipsOnTable(state), total);
|
||||
assert.equal(HoldemState.bankReconciliation(state).balanced, true);
|
||||
});
|
||||
|
||||
test("collecting twice takes nothing the second time", () => {
|
||||
// The pot wizard can be opened, cancelled and opened again.
|
||||
const state = basePotTable();
|
||||
Betting.collectAssumedContributions(state);
|
||||
const stacks = state.seats.map(p => p.stack);
|
||||
const again = Betting.collectAssumedContributions(state);
|
||||
assert.equal(again.total, 0);
|
||||
assert.deepEqual(state.seats.map(p => p.stack), stacks);
|
||||
});
|
||||
});
|
||||
@@ -719,6 +719,64 @@ const LayoutAudit = {
|
||||
return { name: "the felt shows one dealer button", offenders };
|
||||
},
|
||||
|
||||
// Anything that says it is hidden and is drawn anyway.
|
||||
//
|
||||
// `hidden` gets its `display: none` from the browser's own stylesheet, the weakest
|
||||
// rule in the cascade, so any class that sets a display beats it — and in this tool
|
||||
// every button class does. The result is an element the code believes is gone,
|
||||
// sitting on the screen: found as a 21x44 empty box under the bank check, and this
|
||||
// file's own touch check had been calling it "could not be probed" rather than
|
||||
// saying the plainer thing, which is that it should not be there at all.
|
||||
hiddenButPainted() {
|
||||
const offenders = [...document.querySelectorAll("[hidden]")]
|
||||
.filter(el => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
if (rect.width === 0 || rect.height === 0) return false;
|
||||
const style = getComputedStyle(el);
|
||||
return style.display !== "none" && style.visibility !== "hidden";
|
||||
})
|
||||
.map(el => `${this.describe(el)} :: hidden, but drawn ${Math.round(el.getBoundingClientRect().width)}x${Math.round(el.getBoundingClientRect().height)}`);
|
||||
return { name: "hidden means hidden", offenders };
|
||||
},
|
||||
|
||||
// A seat's name, painted under the decoration on its own puck.
|
||||
//
|
||||
// The chip that marks the button sits in the puck's top-right corner on a layer
|
||||
// above the text, and the name is centred across the whole width. A long one
|
||||
// therefore ran underneath it and stopped — measured, "Christopher" on the button
|
||||
// lost 20px of its 64px box and showed no ellipsis, because as far as the text was
|
||||
// concerned it had all the room it needed. Nothing else here would have caught it:
|
||||
// the occlusion check is about controls a thumb has to reach, and this is a label
|
||||
// a dealer has to read.
|
||||
seatLabelsClear() {
|
||||
const offenders = [];
|
||||
document.querySelectorAll(".seat").forEach(seat => {
|
||||
const name = seat.querySelector(".seatPuckName");
|
||||
if (!name || !name.textContent.trim()) return;
|
||||
const box = name.getBoundingClientRect();
|
||||
if (!box.width || !box.height) return;
|
||||
// Padding is room the text has already been kept out of, so it is not
|
||||
// somewhere the text can be covered.
|
||||
const style = getComputedStyle(name);
|
||||
const textLeft = box.left + parseFloat(style.paddingLeft || 0);
|
||||
const textRight = box.right - parseFloat(style.paddingRight || 0);
|
||||
|
||||
seat.querySelectorAll(".dealerChip, .blindBadge").forEach(mark => {
|
||||
const over = mark.getBoundingClientRect();
|
||||
if (!over.width || !over.height) return;
|
||||
const marked = getComputedStyle(mark);
|
||||
if (marked.display === "none" || marked.visibility === "hidden" ||
|
||||
parseFloat(marked.opacity) < 0.05) return;
|
||||
const covered = Math.min(textRight, over.right) - Math.max(textLeft, over.left);
|
||||
const vertical = Math.min(box.bottom, over.bottom) - Math.max(box.top, over.top);
|
||||
if (covered > 1 && vertical > 1) {
|
||||
offenders.push(`${this.describe(name)} :: ${Math.round(covered)}px under ${this.describe(mark)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
return { name: "seat names aren't painted under their own badges", offenders };
|
||||
},
|
||||
|
||||
// ---- Runner ----
|
||||
|
||||
// Every check this file knows how to make, every time.
|
||||
@@ -747,6 +805,8 @@ const LayoutAudit = {
|
||||
this.fieldFontSize(),
|
||||
this.seatOwnership(),
|
||||
this.oneDealerButton(),
|
||||
this.hiddenButPainted(),
|
||||
this.seatLabelsClear(),
|
||||
this.contrast(options.contrast),
|
||||
this.touchTargets(options.minimumTarget, options.touchTargets),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user