Make the pot button offer a pot-sized raise #94
@@ -430,7 +430,6 @@ const ActionBar = {
|
|||||||
const player = this.state.seats[index];
|
const player = this.state.seats[index];
|
||||||
const stack = Betting.stackOf(this.state, player);
|
const stack = Betting.stackOf(this.state, player);
|
||||||
const maxTo = Betting.round(Betting.committed(player) + stack);
|
const maxTo = Betting.round(Betting.committed(player) + stack);
|
||||||
const pot = Betting.potSoFar(this.state);
|
|
||||||
// Two different minimums, and conflating them was a bug. `minTo` is clamped to
|
// Two different minimums, and conflating them was a bug. `minTo` is clamped to
|
||||||
// what this player can actually put in, which is what the Min button should
|
// what this player can actually put in, which is what the Min button should
|
||||||
// offer. `legalMin` is what the rules require — for a short stack the two are
|
// offer. `legalMin` is what the rules require — for a short stack the two are
|
||||||
@@ -446,10 +445,16 @@ const ActionBar = {
|
|||||||
const quick = document.createElement("div");
|
const quick = document.createElement("div");
|
||||||
quick.className = "raiseQuick";
|
quick.className = "raiseQuick";
|
||||||
|
|
||||||
|
// The pot amounts come from Betting, which knows that a pot raise is measured
|
||||||
|
// off the pot as it stands *after* the caller's own call goes in. The pad used
|
||||||
|
// to work it out here as "the bet plus the pot", which left the call out and
|
||||||
|
// made both amounts short: pre-flop at 1/2 it offered "Pot $5" where a pot
|
||||||
|
// raise is $7, and a half-pot that clamped up to the minimum and sat next to
|
||||||
|
// the Min button showing the same $4 twice.
|
||||||
const options = [
|
const options = [
|
||||||
{ label: "Min", value: minTo },
|
{ label: "Min", value: minTo },
|
||||||
{ label: "½ pot", value: Betting.round(this.state.currentBet + pot / 2) },
|
{ label: "½ pot", value: Betting.potRaiseTo(this.state, index, 0.5) },
|
||||||
{ label: "Pot", value: Betting.round(this.state.currentBet + pot) },
|
{ label: "Pot", value: Betting.potRaiseTo(this.state, index, 1) },
|
||||||
{ label: "All-in", value: maxTo },
|
{ label: "All-in", value: maxTo },
|
||||||
];
|
];
|
||||||
options.forEach(option => {
|
options.forEach(option => {
|
||||||
|
|||||||
@@ -518,6 +518,27 @@ const Betting = {
|
|||||||
: this.nextToAct(state, bbIndex);
|
: this.nextToAct(state, bbIndex);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// What a pot-sized raise actually is, as a "raise to" total.
|
||||||
|
//
|
||||||
|
// Not "the bet plus the pot as it stands". The raiser calls first, and their call
|
||||||
|
// is in the pot before the raise is measured off it, so the pot being matched is
|
||||||
|
// the one that exists after the call:
|
||||||
|
//
|
||||||
|
// raise to = the current bet + (pot + what they owe) × fraction
|
||||||
|
//
|
||||||
|
// Pre-flop at 1/2 that gives $7, not $5: call the $2, which makes the pot $5, then
|
||||||
|
// raise $5. Leaving the call out understates every one of these, and players say
|
||||||
|
// "pot" out loud meaning a definite number.
|
||||||
|
//
|
||||||
|
// Facing no bet this is a plain pot-sized bet — toCall is zero and currentBet is
|
||||||
|
// zero, so it comes out as the pot, which is the right answer there too.
|
||||||
|
potRaiseTo(state, index, fraction = 1) {
|
||||||
|
const player = state.seats[index];
|
||||||
|
if (!player) return 0;
|
||||||
|
const potAfterCall = this.round(this.potSoFar(state) + this.toCall(state, player));
|
||||||
|
return this.round((state.currentBet || 0) + potAfterCall * fraction);
|
||||||
|
},
|
||||||
|
|
||||||
// Who turns their hand over first.
|
// Who turns their hand over first.
|
||||||
//
|
//
|
||||||
// A call the dealer makes at every showdown that isn't a fold, and one nobody at
|
// A call the dealer makes at every showdown that isn't a fold, and one nobody at
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// What a pot-sized raise actually is.
|
||||||
|
//
|
||||||
|
// The raise pad worked it out as "the current bet plus the pot as it stands", which
|
||||||
|
// leaves out the fact that the raiser calls first and their call is in the pot before
|
||||||
|
// the raise is measured off it. Every quick amount came out short — pre-flop at 1/2 it
|
||||||
|
// offered "Pot $5" where a pot raise is $7. Players say "pot" out loud meaning a
|
||||||
|
// definite number, and a dealer tapping the button that claims to be it should record
|
||||||
|
// what was actually said.
|
||||||
|
const test = require("node:test");
|
||||||
|
const { describe } = require("node:test");
|
||||||
|
const assert = require("node:assert");
|
||||||
|
const { Betting, makeTable } = require("./load");
|
||||||
|
|
||||||
|
describe("pre-flop at 1/2", () => {
|
||||||
|
const dealt = () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 200, dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("a pot raise from the first player to act is $7", () => {
|
||||||
|
// Call the $2, which makes the pot $5, then raise $5.
|
||||||
|
const state = dealt();
|
||||||
|
assert.equal(Betting.potSoFar(state), 3, "the blinds");
|
||||||
|
assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex), 7);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("half pot is $4.50, which is above the $4 minimum rather than equal to it", () => {
|
||||||
|
// The old arithmetic gave $3.50, which clamped up to the minimum and put the
|
||||||
|
// same $4 on two buttons side by side.
|
||||||
|
const state = dealt();
|
||||||
|
assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex, 0.5), 4.5);
|
||||||
|
assert.notEqual(Betting.potRaiseTo(state, state.actingSeatIndex, 0.5),
|
||||||
|
Betting.minRaiseTo(state), "the two quick buttons say different things");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the small blind owes less, so their pot raise is smaller", () => {
|
||||||
|
// They have $1 in already: call $1, pot becomes $4, raise $4, total $6.
|
||||||
|
const state = dealt();
|
||||||
|
const { sbIndex } = require("./load").HoldemState.getBlindSeats(state);
|
||||||
|
assert.equal(Betting.toCall(state, state.seats[sbIndex]), 1);
|
||||||
|
assert.equal(Betting.potRaiseTo(state, sbIndex), 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the big blind owes nothing, so it is just the pot", () => {
|
||||||
|
const state = dealt();
|
||||||
|
const { bbIndex } = require("./load").HoldemState.getBlindSeats(state);
|
||||||
|
assert.equal(Betting.toCall(state, state.seats[bbIndex]), 0);
|
||||||
|
assert.equal(Betting.potRaiseTo(state, bbIndex), 5, "the $2 bet plus the $3 pot");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("after the flop", () => {
|
||||||
|
const flopWithBet = () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 500, dealerSeatIndex: 0 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
while (state.actingSeatIndex !== null) Betting.call(state, state.actingSeatIndex);
|
||||||
|
Betting.closeStreet(state);
|
||||||
|
return state;
|
||||||
|
};
|
||||||
|
|
||||||
|
test("a pot raise facing a bet is three times the bet plus the dead money", () => {
|
||||||
|
const state = flopWithBet();
|
||||||
|
const potBefore = Betting.potSoFar(state); // $6 carried over
|
||||||
|
const bettor = state.actingSeatIndex;
|
||||||
|
Betting.raiseTo(state, bettor, 10);
|
||||||
|
|
||||||
|
const raiser = state.actingSeatIndex;
|
||||||
|
// Call $10, pot becomes $26, raise $26 → $36. Which is 3×10 + 6.
|
||||||
|
assert.equal(Betting.potRaiseTo(state, raiser), 36);
|
||||||
|
assert.equal(Betting.potRaiseTo(state, raiser), 3 * 10 + potBefore);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("half pot facing that bet is $23", () => {
|
||||||
|
const state = flopWithBet();
|
||||||
|
Betting.raiseTo(state, state.actingSeatIndex, 10);
|
||||||
|
// Call $10, pot becomes $26, raise $13 → $23.
|
||||||
|
assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex, 0.5), 23);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("facing no bet at all it is a plain pot-sized bet", () => {
|
||||||
|
const state = flopWithBet();
|
||||||
|
assert.equal(state.currentBet, 0, "nobody has bet the flop");
|
||||||
|
assert.equal(Betting.toCall(state, state.seats[state.actingSeatIndex]), 0);
|
||||||
|
assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex), Betting.potSoFar(state));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("it stays sane at the edges", () => {
|
||||||
|
test("a seat that doesn't exist gets zero rather than throwing", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1], stack: 100 });
|
||||||
|
assert.equal(Betting.potRaiseTo(state, 99), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("the amount is always at least a call", () => {
|
||||||
|
// A raise-to below the current bet would be nonsense to offer. Every fraction
|
||||||
|
// from a tenth up lands at or above what the player owes.
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 200 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
const index = state.actingSeatIndex;
|
||||||
|
[0.1, 0.25, 0.5, 0.75, 1].forEach(fraction => {
|
||||||
|
assert.ok(Betting.potRaiseTo(state, index, fraction) >= state.currentBet,
|
||||||
|
`${fraction} of the pot should still cover the bet`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("cent-sized blinds don't drift", () => {
|
||||||
|
const state = makeTable({ seatCount: 6, seated: [0, 1, 2], stack: 20, smallBlind: 0.05, bigBlind: 0.1 });
|
||||||
|
Betting.beginHand(state);
|
||||||
|
// Pot $0.15, owes $0.10: call to $0.25, raise $0.25 → $0.35.
|
||||||
|
assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex), 0.35);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user