Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1af82dfddf | ||
|
|
2cc2c66976 |
@@ -129,8 +129,12 @@ const ActionBar = {
|
|||||||
row.appendChild(pot);
|
row.appendChild(pot);
|
||||||
|
|
||||||
// How many are still in the hand — part of the standard per-street
|
// How many are still in the hand — part of the standard per-street
|
||||||
// announcement, and the number a dealer is asked for most often.
|
// announcement, and the number a dealer is asked for most often. Before the
|
||||||
const remaining = Betting.liveSeats(this.state).length;
|
// deal it counts the table as it will be dealt, so a busted player waiting on a
|
||||||
|
// rebuy isn't announced as one of the players. Read-only: the sitting-out
|
||||||
|
// itself happens when the hand is dealt, not while the bar is being drawn.
|
||||||
|
const counted = this.state.bettingOpen ? this.state : Betting.tableAsDealt(this.state);
|
||||||
|
const remaining = Betting.liveSeats(counted).length;
|
||||||
const players = document.createElement("span");
|
const players = document.createElement("span");
|
||||||
players.className = "actionPlayers";
|
players.className = "actionPlayers";
|
||||||
players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`;
|
players.textContent = `${remaining} ${remaining === 1 ? "player" : "players"}`;
|
||||||
@@ -341,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();
|
||||||
@@ -374,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";
|
||||||
@@ -406,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);
|
||||||
@@ -418,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);
|
||||||
@@ -451,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; }
|
||||||
|
|
||||||
@@ -459,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;
|
||||||
@@ -503,22 +521,35 @@ const ActionBar = {
|
|||||||
|
|
||||||
const preview = Betting.blindPreview(this.state);
|
const preview = Betting.blindPreview(this.state);
|
||||||
const seated = Betting.liveSeats(this.state).length;
|
const seated = Betting.liveSeats(this.state).length;
|
||||||
|
const broke = Betting.brokeSeats(this.state);
|
||||||
|
const name = i => HoldemState.displayName(this.state.seats[i], i);
|
||||||
|
const listNames = indexes => indexes.length === 1
|
||||||
|
? name(indexes[0])
|
||||||
|
: `${indexes.slice(0, -1).map(name).join(", ")} and ${name(indexes[indexes.length - 1])}`;
|
||||||
|
|
||||||
const line = document.createElement("p");
|
const line = document.createElement("p");
|
||||||
line.className = "dealPromptLine";
|
line.className = "dealPromptLine";
|
||||||
|
|
||||||
if (!preview) {
|
if (!preview) {
|
||||||
// Two players is the least you can deal. Say what's missing rather than
|
// Two players is the least you can deal. Say what's missing rather than
|
||||||
// offering a button that would quietly do nothing.
|
// offering a button that would quietly do nothing — and say the *right*
|
||||||
line.textContent = seated === 0
|
// thing: "only one player is dealt in" is wrong when four are sitting there
|
||||||
? "Nobody is dealt in yet. Tap a seat to add a player."
|
// and three of them are just out of chips.
|
||||||
: "Only one player is dealt in — a hand needs two.";
|
const withChips = seated - broke.length;
|
||||||
|
if (broke.length > 0 && withChips < 2) {
|
||||||
|
line.textContent = withChips === 1
|
||||||
|
? `Only ${listNames(Betting.liveSeats(this.state).map(s => s.index).filter(i => !broke.includes(i)))} has chips — ${listNames(broke)} ${broke.length === 1 ? "needs" : "need"} to buy in.`
|
||||||
|
: "Nobody at the table has chips left. Add a buy-in to deal.";
|
||||||
|
} else {
|
||||||
|
line.textContent = seated === 0
|
||||||
|
? "Nobody is dealt in yet. Tap a seat to add a player."
|
||||||
|
: "Only one player is dealt in — a hand needs two.";
|
||||||
|
}
|
||||||
line.classList.add("dealPromptBlocked");
|
line.classList.add("dealPromptBlocked");
|
||||||
wrap.appendChild(line);
|
wrap.appendChild(line);
|
||||||
return wrap;
|
return wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
const name = i => HoldemState.displayName(this.state.seats[i], i);
|
|
||||||
line.textContent =
|
line.textContent =
|
||||||
`${name(preview.sbIndex)} posts ${Money.format(preview.smallBlind)}, ` +
|
`${name(preview.sbIndex)} posts ${Money.format(preview.smallBlind)}, ` +
|
||||||
`${name(preview.bbIndex)} posts ${Money.format(preview.bigBlind)}` +
|
`${name(preview.bbIndex)} posts ${Money.format(preview.bigBlind)}` +
|
||||||
@@ -536,6 +567,16 @@ const ActionBar = {
|
|||||||
wrap.appendChild(moved);
|
wrap.appendChild(moved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Dealing sits out anyone with nothing behind. Said before it happens, for the
|
||||||
|
// same reason the button move is: a dealer shouldn't find out from the felt.
|
||||||
|
if (preview.satOut.length > 0) {
|
||||||
|
const out = document.createElement("p");
|
||||||
|
out.className = "dealPromptLine dealPromptAside";
|
||||||
|
out.textContent = `${listNames(preview.satOut)} ${preview.satOut.length === 1 ? "has" : "have"} no chips — ` +
|
||||||
|
`${preview.satOut.length === 1 ? "sitting" : "they sit"} out this hand.`;
|
||||||
|
wrap.appendChild(out);
|
||||||
|
}
|
||||||
|
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "actionButtons";
|
row.className = "actionButtons";
|
||||||
row.appendChild(this.button("Post blinds & deal", "actionDeal", () => {
|
row.appendChild(this.button("Post blinds & deal", "actionDeal", () => {
|
||||||
|
|||||||
@@ -32,28 +32,80 @@ const Betting = {
|
|||||||
// Who would post if the hand were dealt right now. Used to preview the blinds
|
// Who would post if the hand were dealt right now. Used to preview the blinds
|
||||||
// while the table is still being set, so a dealer can see where they'll land
|
// while the table is still being set, so a dealer can see where they'll land
|
||||||
// before any money moves.
|
// before any money moves.
|
||||||
|
// A seat that is dealt in, has nothing in the pot, and has no chips behind.
|
||||||
|
//
|
||||||
|
// Not the same as all-in: an all-in player has money riding on the hand. This is
|
||||||
|
// somebody who busted and hasn't bought back in yet, which a home game produces
|
||||||
|
// about once an hour. A seat with no stack *recorded* isn't broke either —
|
||||||
|
// stackOf falls back to the table's starting stack for those — only an explicit
|
||||||
|
// zero counts.
|
||||||
|
brokeSeats(state) {
|
||||||
|
const broke = [];
|
||||||
|
state.seats.forEach((player, index) => {
|
||||||
|
if (!player.inHand) return;
|
||||||
|
if (this.handTotal(player) > this.EPSILON_CHIPS) return;
|
||||||
|
if (this.stackOf(state, player) > this.EPSILON_CHIPS) return;
|
||||||
|
broke.push(index);
|
||||||
|
});
|
||||||
|
return broke;
|
||||||
|
},
|
||||||
|
|
||||||
|
// The table as it will be once the seats that can't post are dealt around. A view,
|
||||||
|
// not a change: the preview has to ask the same question the deal will answer, and
|
||||||
|
// asking it of a different shape of table is how the two came apart last time.
|
||||||
|
tableAsDealt(state) {
|
||||||
|
const broke = this.brokeSeats(state);
|
||||||
|
if (broke.length === 0) return state;
|
||||||
|
const skip = new Set(broke);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
seats: state.seats.map((player, index) =>
|
||||||
|
skip.has(index) ? { ...player, inHand: false, folded: false } : player),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
blindPreview(state) {
|
blindPreview(state) {
|
||||||
// Dealing relocates a button parked on an empty chair before it works out who
|
// Dealing relocates a button parked on an empty chair before it works out who
|
||||||
// posts, so the preview has to answer for the button where it *will* be. Asking
|
// posts, so the preview has to answer for the button where it *will* be. Asking
|
||||||
// from where it currently sits gave a different pair of blinds than the deal
|
// from where it currently sits gave a different pair of blinds than the deal
|
||||||
// produced — the preview promising one thing and doing another is exactly the
|
// produced — the preview promising one thing and doing another is exactly the
|
||||||
// guessing this whole step exists to remove.
|
// guessing this whole step exists to remove. Same reason the broke seats come
|
||||||
const buttonIndex = HoldemState.buttonSeatForDeal(state);
|
// out first: the preview once promised a small blind from a seat with no chips.
|
||||||
const asDealt = buttonIndex === state.dealerSeatIndex
|
const dealt = this.tableAsDealt(state);
|
||||||
? state
|
const buttonIndex = HoldemState.buttonSeatForDeal(dealt);
|
||||||
: { ...state, dealerSeatIndex: buttonIndex };
|
const asDealt = buttonIndex === dealt.dealerSeatIndex
|
||||||
|
? dealt
|
||||||
|
: { ...dealt, dealerSeatIndex: buttonIndex };
|
||||||
|
|
||||||
|
const satOut = this.brokeSeats(state);
|
||||||
const { sbIndex, bbIndex, headsUp, manual } = HoldemState.getBlindSeats(asDealt);
|
const { sbIndex, bbIndex, headsUp, manual } = HoldemState.getBlindSeats(asDealt);
|
||||||
if (sbIndex === null || bbIndex === null) return null;
|
if (sbIndex === null || bbIndex === null) return null;
|
||||||
return {
|
return {
|
||||||
sbIndex, bbIndex, headsUp, manual: !!manual,
|
sbIndex, bbIndex, headsUp, manual: !!manual,
|
||||||
buttonIndex,
|
buttonIndex,
|
||||||
buttonMoves: buttonIndex !== state.dealerSeatIndex,
|
buttonMoves: buttonIndex !== state.dealerSeatIndex,
|
||||||
|
satOut,
|
||||||
smallBlind: this.round(state.smallBlind || 0),
|
smallBlind: this.round(state.smallBlind || 0),
|
||||||
bigBlind: this.round(state.bigBlind || 0),
|
bigBlind: this.round(state.bigBlind || 0),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// "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);
|
||||||
},
|
},
|
||||||
@@ -303,6 +355,11 @@ const Betting = {
|
|||||||
// True when the hand is still live but nobody can bet again — the dealer is just
|
// True when the hand is still live but nobody can bet again — the dealer is just
|
||||||
// running the board out.
|
// running the board out.
|
||||||
isRunningOut(state) {
|
isRunningOut(state) {
|
||||||
|
// Only while a hand is actually being played. Before the deal, a table of
|
||||||
|
// busted players who haven't rebought looks exactly like a table of all-ins to
|
||||||
|
// this test, and the bar was announcing a run-out over a felt with no cards and
|
||||||
|
// no chips on it.
|
||||||
|
if (!state.bettingOpen) return false;
|
||||||
return this.liveSeats(state).length > 1 && !this.canStillBet(state);
|
return this.liveSeats(state).length > 1 && !this.canStillBet(state);
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -340,6 +397,21 @@ const Betting = {
|
|||||||
state.currentBet = 0;
|
state.currentBet = 0;
|
||||||
state.lastRaiseSize = 0;
|
state.lastRaiseSize = 0;
|
||||||
|
|
||||||
|
// Deal around anyone with no chips, before anything else reads the table.
|
||||||
|
//
|
||||||
|
// A busted player who hasn't rebought used to be dealt in anyway, and the
|
||||||
|
// result was a hand nobody could finish: the table promised a small blind from
|
||||||
|
// a seat with nothing to post it with, took it from nobody, put the action on
|
||||||
|
// the big blind with only their own money in front of them, and announced
|
||||||
|
// "3 players — all-in, running out the board" before a card was dealt.
|
||||||
|
//
|
||||||
|
// Sitting them out is what a dealer does. Seat, name and buy-in record are
|
||||||
|
// untouched, so dealing them back in is one tap when they buy chips.
|
||||||
|
this.brokeSeats(state).forEach(index => {
|
||||||
|
state.seats[index].inHand = false;
|
||||||
|
state.seats[index].folded = false;
|
||||||
|
});
|
||||||
|
|
||||||
// Position is read off the button all through the hand, so it has to be on
|
// Position is read off the button all through the hand, so it has to be on
|
||||||
// somebody before the blinds are worked out — not on the empty seat 1 a
|
// somebody before the blinds are worked out — not on the empty seat 1 a
|
||||||
// brand-new table starts on.
|
// brand-new table starts on.
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
// Dealing around a player who busted and hasn't bought back in.
|
||||||
|
//
|
||||||
|
// A home game produces one of these about once an hour. The table used to deal them
|
||||||
|
// in anyway, promise a blind from a seat with nothing to post it with, take it from
|
||||||
|
// nobody, and then sit on a hand that could never finish.
|
||||||
|
const test = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load");
|
||||||
|
|
||||||
|
const postedBy = state =>
|
||||||
|
state.seats
|
||||||
|
.map((player, index) => (player.committed > 0 ? { seat: index + 1, amount: player.committed } : null))
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
test("a seat with no chips and nothing in the pot is broke", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
assert.deepEqual(Betting.brokeSeats(state), [2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("an all-in player is not broke — they have money riding on the hand", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].stack = 12;
|
||||||
|
Betting.beginHand(state);
|
||||||
|
Betting.allIn(state, 2);
|
||||||
|
|
||||||
|
assert.equal(Betting.stackOf(state, state.seats[2]), 0);
|
||||||
|
assert.equal(Betting.isAllIn(state, state.seats[2]), true);
|
||||||
|
assert.deepEqual(Betting.brokeSeats(state), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a seat with no stack recorded falls back to the table's and isn't broke", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.defaultStack = 200;
|
||||||
|
state.seats[2].stack = null;
|
||||||
|
assert.deepEqual(Betting.brokeSeats(state), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a chair nobody is sitting in isn't broke, it's empty", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
assert.deepEqual(Betting.brokeSeats(state), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dealing sits the broke seat out and leaves everything else about it alone", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
state.seats[2].name = "Cal";
|
||||||
|
state.seats[2].buyIns = 100;
|
||||||
|
|
||||||
|
Betting.beginHand(state);
|
||||||
|
|
||||||
|
assert.equal(state.seats[2].inHand, false);
|
||||||
|
assert.equal(state.seats[2].folded, false, "sat out, not folded — they didn't muck anything");
|
||||||
|
assert.equal(state.seats[2].name, "Cal");
|
||||||
|
assert.equal(state.seats[2].buyIns, 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the blinds skip the broke seat and land on players who can post", () => {
|
||||||
|
// Button on seat 1, so the blinds would be seats 2 and 3. Seat 2 is busted.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
state.seats[1].stack = 0;
|
||||||
|
|
||||||
|
Betting.beginHand(state);
|
||||||
|
|
||||||
|
assert.deepEqual(postedBy(state), [
|
||||||
|
{ seat: 3, amount: state.smallBlind },
|
||||||
|
{ seat: 4, amount: state.bigBlind },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the preview promises what the deal produces", () => {
|
||||||
|
// The fault this whole step exists to prevent: the preview once read a table the
|
||||||
|
// deal was about to change underneath it.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100, dealerSeatIndex: 0 });
|
||||||
|
state.seats[1].stack = 0;
|
||||||
|
state.bettingOpen = false;
|
||||||
|
|
||||||
|
const preview = Betting.blindPreview(state);
|
||||||
|
assert.deepEqual(preview.satOut, [1]);
|
||||||
|
|
||||||
|
Betting.beginHand(state);
|
||||||
|
const posted = postedBy(state);
|
||||||
|
assert.equal(posted.find(p => p.amount === state.smallBlind).seat, preview.sbIndex + 1);
|
||||||
|
assert.equal(posted.find(p => p.amount === state.bigBlind).seat, preview.bbIndex + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("asking for the preview doesn't sit anybody out on its own", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[1].stack = 0;
|
||||||
|
|
||||||
|
Betting.blindPreview(state);
|
||||||
|
|
||||||
|
assert.equal(state.seats[1].inHand, true, "the preview is a view, not a change");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("with only one player left holding chips there is nothing to preview", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[1].stack = 0;
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
|
||||||
|
assert.equal(Betting.blindPreview(state), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("no chips move when the broke seats are dealt around", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
const before = chipsOnTable(state);
|
||||||
|
|
||||||
|
Betting.beginHand(state);
|
||||||
|
|
||||||
|
assert.equal(chipsOnTable(state), before);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the board isn't running out before the hand is dealt", () => {
|
||||||
|
// Two busted players and one with chips looks exactly like a table of all-ins to
|
||||||
|
// the run-out test, and the bar was announcing one over an empty felt.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[1].stack = 0;
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
state.bettingOpen = false;
|
||||||
|
|
||||||
|
assert.equal(Betting.isRunningOut(state), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("buying back in puts them in the next hand", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 100 });
|
||||||
|
state.seats[2].stack = 0;
|
||||||
|
Betting.beginHand(state);
|
||||||
|
assert.equal(state.seats[2].inHand, false);
|
||||||
|
|
||||||
|
// What the dealer does when they hand over cash: chips, and back in.
|
||||||
|
state.seats[2].stack = 100;
|
||||||
|
state.seats[2].inHand = true;
|
||||||
|
HoldemState.startNextHand(state);
|
||||||
|
Betting.beginHand(state);
|
||||||
|
|
||||||
|
assert.equal(state.seats[2].inHand, true);
|
||||||
|
assert.equal(Betting.liveSeats(state).length, 3);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the table dealt around them counts the right number of players", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 100 });
|
||||||
|
state.seats[3].stack = 0;
|
||||||
|
|
||||||
|
assert.equal(Betting.liveSeats(state).length, 4);
|
||||||
|
assert.equal(Betting.liveSeats(Betting.tableAsDealt(state)).length, 3);
|
||||||
|
assert.equal(state.seats[3].inHand, true, "the view didn't touch the table");
|
||||||
|
});
|
||||||
@@ -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