Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
38536a5c0c | ||
|
|
286a924fe4 | ||
|
|
3e9e2e899d | ||
|
|
96cd7684f0 | ||
|
|
1af82dfddf |
@@ -212,6 +212,20 @@ button,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.holdemBtn {
|
.holdemBtn {
|
||||||
|
/* Stated, because this class is worn by both <button> and <a> and the browser
|
||||||
|
does not give them the same box. Chromium's own stylesheet makes a button
|
||||||
|
border-box and leaves a link content-box, so `min-height: 46px` on the header
|
||||||
|
row came out as 46px on the buttons and 64px on the two links — and the row
|
||||||
|
stretches to its tallest item, so every button became 64px too. A 46px touch
|
||||||
|
target was quietly costing 64px of a phone screen, twice over, because the
|
||||||
|
header wraps to two rows. */
|
||||||
|
box-sizing: border-box;
|
||||||
|
/* And the touch floor is stated here rather than only under a width breakpoint.
|
||||||
|
It used to arrive by accident — the content-box links were 64px tall and the
|
||||||
|
row stretched everything to match — so once the boxes agree, a landscape phone
|
||||||
|
(wider than the phone breakpoint, still a thumb) would have been left with 42px
|
||||||
|
buttons. 44px is the floor everywhere; the narrow layout still asks for 46. */
|
||||||
|
min-height: 44px;
|
||||||
background: linear-gradient(180deg, #2e2e2e, #232323);
|
background: linear-gradient(180deg, #2e2e2e, #232323);
|
||||||
color: #eee;
|
color: #eee;
|
||||||
border: 1px solid #3a3a3a;
|
border: 1px solid #3a3a3a;
|
||||||
@@ -695,6 +709,23 @@ button,
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Seats on the bottom of the ring wear their badge underneath, because that's the
|
||||||
|
edge the pot isn't on. Bets sit between a seat and the pot, so on the bottom arc
|
||||||
|
the chip comes down onto the seat's top edge — which is exactly where this badge
|
||||||
|
was, and the chip is a layer above it. See render.js for the measurement. */
|
||||||
|
.seat.seatBelowCentre .blindBadge {
|
||||||
|
top: auto;
|
||||||
|
bottom: -9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* And the badge outranks the chips regardless, so a layout this doesn't anticipate
|
||||||
|
can't hide it again. Which blind a seat is posting is the one thing on that puck
|
||||||
|
nothing else on the felt repeats — a bet chip's amount is on the seat's own total
|
||||||
|
line as well. Measured across 12 table sizes: no chip loses its amount to this. */
|
||||||
|
.blindBadge {
|
||||||
|
z-index: 4;
|
||||||
|
}
|
||||||
|
|
||||||
/* Both badges failed WCAG AA against their own gradients — measured at the worst
|
/* Both badges failed WCAG AA against their own gradients — measured at the worst
|
||||||
stop, the small blind was 3.52:1 and the big blind 2.15:1, in 9px bold text a
|
stop, the small blind was 3.52:1 and the big blind 2.15:1, in 9px bold text a
|
||||||
dealer is meant to read at a glance across a table.
|
dealer is meant to read at a glance across a table.
|
||||||
@@ -982,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;
|
||||||
@@ -1439,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;
|
||||||
@@ -3154,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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -345,7 +345,10 @@ const ActionBar = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (stack > owed) {
|
if (stack > owed) {
|
||||||
row.appendChild(this.button(owed > 0 ? "Raise" : "Bet", "actionRaise", () => {
|
// Not "does this player owe anything" — the big blind owes nothing and is
|
||||||
|
// still raising. It's whether there is a live bet to raise.
|
||||||
|
const raising = Betting.wagerVerb(this.state) === "raise";
|
||||||
|
row.appendChild(this.button(raising ? "Raise" : "Bet", "actionRaise", () => {
|
||||||
this.raiseOpen = true;
|
this.raiseOpen = true;
|
||||||
this.raiseDraft = Math.min(Betting.minRaiseTo(this.state), Betting.committed(player) + stack);
|
this.raiseDraft = Math.min(Betting.minRaiseTo(this.state), Betting.committed(player) + stack);
|
||||||
this.render();
|
this.render();
|
||||||
@@ -378,6 +381,7 @@ const ActionBar = {
|
|||||||
// or a player whose whole stack is under a full raise never gets told that.
|
// or a player whose whole stack is under a full raise never gets told that.
|
||||||
const legalMin = Betting.minRaiseTo(this.state);
|
const legalMin = Betting.minRaiseTo(this.state);
|
||||||
const minTo = Math.min(legalMin, maxTo);
|
const minTo = Math.min(legalMin, maxTo);
|
||||||
|
const raising = Betting.wagerVerb(this.state) === "raise";
|
||||||
|
|
||||||
const wrap = document.createElement("div");
|
const wrap = document.createElement("div");
|
||||||
wrap.className = "raisePad";
|
wrap.className = "raisePad";
|
||||||
@@ -410,7 +414,7 @@ const ActionBar = {
|
|||||||
input.inputMode = "decimal";
|
input.inputMode = "decimal";
|
||||||
input.className = "raiseInput";
|
input.className = "raiseInput";
|
||||||
input.value = this.raiseDraft ?? minTo;
|
input.value = this.raiseDraft ?? minTo;
|
||||||
input.setAttribute("aria-label", "Raise to amount");
|
input.setAttribute("aria-label", raising ? "Raise to amount" : "Bet amount");
|
||||||
input.addEventListener("input", () => {
|
input.addEventListener("input", () => {
|
||||||
this.raiseDraft = input.value.replace(/[^0-9.]/g, "");
|
this.raiseDraft = input.value.replace(/[^0-9.]/g, "");
|
||||||
this.updateRaiseNote(wrap, legalMin, maxTo);
|
this.updateRaiseNote(wrap, legalMin, maxTo);
|
||||||
@@ -422,11 +426,16 @@ const ActionBar = {
|
|||||||
this.render();
|
this.render();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
entry.appendChild(this.button("Raise to", "raiseConfirm", () => {
|
// "Raise to $60" and "Bet $10" — a bet isn't *to* anything, there's nothing
|
||||||
|
// under it to raise past.
|
||||||
|
entry.appendChild(this.button(raising ? "Raise to" : "Bet", "raiseConfirm", () => {
|
||||||
const target = Math.max(0, Math.min(Number(this.raiseDraft) || 0, maxTo));
|
const target = Math.max(0, Math.min(Number(this.raiseDraft) || 0, maxTo));
|
||||||
this.raiseOpen = false;
|
this.raiseOpen = false;
|
||||||
const who = HoldemState.displayName(player, index);
|
const who = HoldemState.displayName(player, index);
|
||||||
this.commit(`${who}'s raise to ${Money.format(target)}`, () => Betting.raiseTo(this.state, index, target));
|
const label = raising
|
||||||
|
? `${who}'s raise to ${Money.format(target)}`
|
||||||
|
: `${who}'s bet of ${Money.format(target)}`;
|
||||||
|
this.commit(label, () => Betting.raiseTo(this.state, index, target));
|
||||||
}));
|
}));
|
||||||
|
|
||||||
wrap.appendChild(entry);
|
wrap.appendChild(entry);
|
||||||
@@ -455,6 +464,7 @@ const ActionBar = {
|
|||||||
|
|
||||||
const typed = Number(this.raiseDraft);
|
const typed = Number(this.raiseDraft);
|
||||||
const current = this.state.currentBet || 0;
|
const current = this.state.currentBet || 0;
|
||||||
|
const raising = Betting.wagerVerb(this.state) === "raise";
|
||||||
|
|
||||||
if (!Number.isFinite(typed) || typed <= 0) { note.hidden = true; return; }
|
if (!Number.isFinite(typed) || typed <= 0) { note.hidden = true; return; }
|
||||||
|
|
||||||
@@ -463,16 +473,20 @@ const ActionBar = {
|
|||||||
const isAllIn = Math.abs(typed - maxTo) < Betting.EPSILON_CHIPS;
|
const isAllIn = Math.abs(typed - maxTo) < Betting.EPSILON_CHIPS;
|
||||||
|
|
||||||
if (typed > current && typed < minTo - Betting.EPSILON_CHIPS && !isAllIn) {
|
if (typed > current && typed < minTo - Betting.EPSILON_CHIPS && !isAllIn) {
|
||||||
note.textContent =
|
// The way out of an under-minimum differs: with a bet in front of you the
|
||||||
`${Money.format(typed)} is under the ${Money.format(minTo)} minimum. A legal raise is ${Money.format(minTo)} or more — otherwise it's a call, or all-in.`;
|
// alternative is calling it, and with an empty pot there's nothing to call.
|
||||||
|
note.textContent = raising
|
||||||
|
? `${Money.format(typed)} is under the ${Money.format(minTo)} minimum. A legal raise is ${Money.format(minTo)} or more — otherwise it's a call, or all-in.`
|
||||||
|
: `${Money.format(typed)} is under the ${Money.format(minTo)} minimum. A legal bet is ${Money.format(minTo)} or more — otherwise it's a check, or all-in.`;
|
||||||
note.className = "raiseNote raiseNoteWarn";
|
note.className = "raiseNote raiseNoteWarn";
|
||||||
note.hidden = false;
|
note.hidden = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAllIn && typed > current && typed < minTo - Betting.EPSILON_CHIPS) {
|
if (isAllIn && typed > current && typed < minTo - Betting.EPSILON_CHIPS) {
|
||||||
note.textContent =
|
note.textContent = raising
|
||||||
`All-in for less than a full raise. Legal, but it doesn't reopen the betting — anyone who has already acted owes the difference or folds.`;
|
? `All-in for less than a full raise. Legal, but it doesn't reopen the betting — anyone who has already acted owes the difference or folds.`
|
||||||
|
: `All-in for less than a full bet. Legal, but it doesn't reopen the betting — anyone who has already checked can call it or fold, not raise.`;
|
||||||
note.className = "raiseNote raiseNoteInfo";
|
note.className = "raiseNote raiseNoteInfo";
|
||||||
note.hidden = false;
|
note.hidden = false;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -90,6 +90,44 @@ const Betting = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Does handing this seat chips also put them back in the game?
|
||||||
|
//
|
||||||
|
// At a real table those are one event, and here they were two: a busted player got
|
||||||
|
// their chips and stayed sat out, silently skipped hand after hand until somebody
|
||||||
|
// noticed and found "Deal in" in the seat menu. Nothing said so — the toast just
|
||||||
|
// read "Ben added $100".
|
||||||
|
//
|
||||||
|
// Not for a seat that was sat out while *holding* chips. That's a dealer who sat
|
||||||
|
// somebody out on purpose and is topping them up, and dealing them back in would be
|
||||||
|
// the tool guessing at something it wasn't told. Only a seat with no stack of its
|
||||||
|
// own: busted at zero, or a chair being bought into for the first time.
|
||||||
|
//
|
||||||
|
// Deliberately reads player.stack rather than stackOf(), which substitutes the
|
||||||
|
// table's starting stack for a seat that has none — the question here is what this
|
||||||
|
// seat was actually recorded as holding.
|
||||||
|
buyInDealsThemIn(player) {
|
||||||
|
if (!player || player.inHand) return false;
|
||||||
|
const own = player.stack;
|
||||||
|
const recorded = own !== null && own !== undefined && String(own).trim() !== "";
|
||||||
|
return !recorded || Number(own) <= this.EPSILON_CHIPS;
|
||||||
|
},
|
||||||
|
|
||||||
|
// "bet" or "raise" — what putting more in is actually called right now.
|
||||||
|
//
|
||||||
|
// A wager into a pot nobody has wagered into is a bet; everything after that is a
|
||||||
|
// raise. The bar decided this from what the acting player still owes, which gets
|
||||||
|
// the big blind's option backwards: they owe nothing, so it offered "Bet", but
|
||||||
|
// their own blind is a live bet and putting more in is a raise. It gets the other
|
||||||
|
// half wrong too — the raise pad said "Raise to" for an opening bet on the flop,
|
||||||
|
// and wrote "Ann's raise to $10" into the undo control for it.
|
||||||
|
//
|
||||||
|
// The arithmetic was always right (minRaiseTo returns two big blinds for the big
|
||||||
|
// blind's option and one for an opening bet). This is only what it's called, which
|
||||||
|
// for a tool a dealer announces the action from is not a small thing.
|
||||||
|
wagerVerb(state) {
|
||||||
|
return (state.currentBet || 0) > this.EPSILON_CHIPS ? "raise" : "bet";
|
||||||
|
},
|
||||||
|
|
||||||
committed(player) {
|
committed(player) {
|
||||||
return this.round(player.committed || 0);
|
return this.round(player.committed || 0);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -140,11 +140,27 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
editSeat: index => Render.toggleSeatEditor(index),
|
editSeat: index => Render.toggleSeatEditor(index),
|
||||||
|
|
||||||
addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
|
addChips: index => AddChips.open(state, index, (seatIndex, amount) => {
|
||||||
seatAction(seatIndex, `${nameOf(seatIndex)} added ${Money.format(amount)}.`, player => {
|
// Buying chips puts you back in the game — see Betting.buyInDealsThemIn.
|
||||||
|
const dealBackIn = Betting.buyInDealsThemIn(state.seats[seatIndex]);
|
||||||
|
const handRunning = !Betting.onlyBlindsIn(state) || Betting.potSoFar(state) > 0;
|
||||||
|
|
||||||
|
const message = `${nameOf(seatIndex)} added ${Money.format(amount)}` +
|
||||||
|
(dealBackIn
|
||||||
|
? handRunning ? " — in from the next hand." : " — dealt in."
|
||||||
|
: ".");
|
||||||
|
|
||||||
|
seatAction(seatIndex, message, p => {
|
||||||
// Chips in front and cash in are different questions — the stack moves
|
// Chips in front and cash in are different questions — the stack moves
|
||||||
// every hand, what they've handed over only moves when they buy.
|
// every hand, what they've handed over only moves when they buy.
|
||||||
player.stack = Money.round(Betting.stackOf(state, player) + amount);
|
p.stack = Money.round(Betting.stackOf(state, p) + amount);
|
||||||
player.buyIns = Money.round((Number(player.buyIns) || 0) + amount);
|
p.buyIns = Money.round((Number(p.buyIns) || 0) + amount);
|
||||||
|
|
||||||
|
if (dealBackIn) {
|
||||||
|
p.inHand = true;
|
||||||
|
// Same rule "Deal in" follows: you can't buy into a pot you haven't
|
||||||
|
// been paying for, and `folded` is exactly that state for one hand.
|
||||||
|
p.folded = handRunning;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -171,6 +171,16 @@ const Render = {
|
|||||||
node.style.top = `${pos.top}%`;
|
node.style.top = `${pos.top}%`;
|
||||||
node.dataset.seatIndex = String(index);
|
node.dataset.seatIndex = String(index);
|
||||||
|
|
||||||
|
// Which side of the seat the pot is on, which is the side its bet chip
|
||||||
|
// arrives from — the chip sits on the same spoke, closer in. For a seat on
|
||||||
|
// the top of the ring that's from below; on the bottom, from above.
|
||||||
|
//
|
||||||
|
// The blind badge has to be on the other side or the chip lands on top of
|
||||||
|
// it. Measured on a 390px phone: the big blind's own $2 chip covered 196 of
|
||||||
|
// the badge's 288 square pixels, and it sits a layer higher, so the marker
|
||||||
|
// telling the dealer who the big blind is was simply gone.
|
||||||
|
node.classList.toggle("seatBelowCentre", pos.top > 50);
|
||||||
|
|
||||||
node.querySelector(".seatChairNumber").textContent = index + 1;
|
node.querySelector(".seatChairNumber").textContent = index + 1;
|
||||||
|
|
||||||
const dealerChip = node.querySelector(".dealerChip");
|
const dealerChip = node.querySelector(".dealerChip");
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Whether handing a seat chips also puts them back in the game.
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Betting, makeTable } = require("./load");
|
||||||
|
|
||||||
|
test("a busted player who has been sat out comes back in", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[1].stack = 0;
|
||||||
|
Betting.beginHand(state); // the deal sits them out
|
||||||
|
|
||||||
|
assert.equal(state.seats[1].inHand, false);
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[1]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a chair being bought into for the first time comes in", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
state.seats[4].name = "Eve";
|
||||||
|
state.seats[4].stack = null;
|
||||||
|
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[4]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a seat sat out while holding chips stays out", () => {
|
||||||
|
// A dealer sat them out on purpose and is topping them up. Dealing them back in
|
||||||
|
// would be the tool guessing at something it wasn't told.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].inHand = false;
|
||||||
|
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[2]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("somebody already in the hand is left exactly where they are", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[0]), false);
|
||||||
|
|
||||||
|
// Including a player who is in but out of chips — they're all-in, not sat out.
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
assert.equal(state.seats[2].inHand, true);
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[2]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a blank stack field reads as nothing recorded, not as a number", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
state.seats[3].name = "Dee";
|
||||||
|
state.seats[3].stack = "";
|
||||||
|
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[3]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("it doesn't ask the table what a missing stack would have been", () => {
|
||||||
|
// stackOf substitutes the table's starting stack for a seat that has none, which
|
||||||
|
// would make a seat with nothing recorded look like it was holding chips.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
state.defaultStack = 500;
|
||||||
|
state.seats[4].name = "Eve";
|
||||||
|
state.seats[4].stack = null;
|
||||||
|
|
||||||
|
assert.equal(Betting.stackOf(state, state.seats[4]), 500);
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(state.seats[4]), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("asking doesn't change anything", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
state.seats[2].inHand = false;
|
||||||
|
const before = JSON.stringify(state);
|
||||||
|
|
||||||
|
Betting.buyInDealsThemIn(state.seats[2]);
|
||||||
|
|
||||||
|
assert.equal(JSON.stringify(state), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("nothing to decide about a seat that isn't there", () => {
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(null), false);
|
||||||
|
assert.equal(Betting.buyInDealsThemIn(undefined), false);
|
||||||
|
});
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
// Whether putting more in is called a bet or a raise.
|
||||||
|
//
|
||||||
|
// The arithmetic was always right — minRaiseTo returns two big blinds for the big
|
||||||
|
// blind's option and one for an opening bet. This is only what it's called, which for
|
||||||
|
// a tool a dealer announces the action from is not a small thing.
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Betting, makeTable } = require("./load");
|
||||||
|
|
||||||
|
// Walks the preflop action round to the big blind, who is owed nothing and gets to
|
||||||
|
// choose between checking and putting more in.
|
||||||
|
function toTheBigBlindsOption() {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
const bigBlind = state.seats.findIndex(player => Betting.committed(player) === state.bigBlind);
|
||||||
|
while (state.actingSeatIndex !== null && state.actingSeatIndex !== bigBlind) {
|
||||||
|
Betting.call(state, state.actingSeatIndex);
|
||||||
|
}
|
||||||
|
return { state, bigBlind };
|
||||||
|
}
|
||||||
|
|
||||||
|
test("the big blind's option is a raise, not a bet", () => {
|
||||||
|
const { state, bigBlind } = toTheBigBlindsOption();
|
||||||
|
|
||||||
|
assert.equal(state.actingSeatIndex, bigBlind);
|
||||||
|
assert.equal(Betting.toCall(state, state.seats[bigBlind]), 0, "they owe nothing");
|
||||||
|
assert.equal(Betting.wagerVerb(state), "raise", "but their own blind is a live bet");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and the minimum there is a real raise, not a call of the blind", () => {
|
||||||
|
const { state } = toTheBigBlindsOption();
|
||||||
|
assert.equal(Betting.minRaiseTo(state), state.bigBlind * 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("facing the blinds preflop is a raise", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
assert.equal(Betting.wagerVerb(state), "raise");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("first in on the flop is a bet", () => {
|
||||||
|
const { state, bigBlind } = toTheBigBlindsOption();
|
||||||
|
Betting.check(state, bigBlind);
|
||||||
|
Betting.closeStreet(state);
|
||||||
|
|
||||||
|
assert.equal(state.street, "flop");
|
||||||
|
assert.equal(Betting.wagerVerb(state), "bet");
|
||||||
|
assert.equal(Betting.minRaiseTo(state), state.bigBlind, "a first bet is one big blind");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("after that bet it's a raise again", () => {
|
||||||
|
const { state, bigBlind } = toTheBigBlindsOption();
|
||||||
|
Betting.check(state, bigBlind);
|
||||||
|
Betting.closeStreet(state);
|
||||||
|
Betting.raiseTo(state, state.actingSeatIndex, 10);
|
||||||
|
|
||||||
|
assert.equal(Betting.wagerVerb(state), "raise");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("and a bet again on the next street once it's checked around", () => {
|
||||||
|
const { state, bigBlind } = toTheBigBlindsOption();
|
||||||
|
Betting.check(state, bigBlind);
|
||||||
|
Betting.closeStreet(state);
|
||||||
|
while (state.actingSeatIndex !== null) Betting.check(state, state.actingSeatIndex);
|
||||||
|
Betting.closeStreet(state);
|
||||||
|
|
||||||
|
assert.equal(state.street, "turn");
|
||||||
|
assert.equal(Betting.wagerVerb(state), "bet");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a table playing without blinds opens with a bet", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100, smallBlind: 0, bigBlind: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
assert.equal(state.currentBet, 0);
|
||||||
|
assert.equal(Betting.wagerVerb(state), "bet");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("asking doesn't change anything", () => {
|
||||||
|
const { state } = toTheBigBlindsOption();
|
||||||
|
const before = JSON.stringify(state);
|
||||||
|
Betting.wagerVerb(state);
|
||||||
|
assert.equal(JSON.stringify(state), before);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user