Hold'em dealer: 55 polish rounds, one merge #101
@@ -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,22 @@ const Betting = {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// "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);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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