// 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, Money, 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 $5 — $4.50 rounded up to something the chips can make", () => { // The old arithmetic gave $3.50, which clamped up to the minimum and put the // same $4 on two buttons side by side. The honest half-pot is $4.50, and the // smallest chip in the default legend is a dollar, so the button would have // offered an amount the dealer had to round themselves every single time. const state = dealt(); assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex, 0.5), 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); }); }); describe("the amounts are ones the table can actually build", () => { const CENTS = [{ value: 0.25 }, { value: 0.05 }, { value: 0.01 }]; const DOLLARS = [{ value: 1 }, { value: 5 }, { value: 25 }, { value: 100 }]; const table = (legend, smallBlind, bigBlind, stack = 200) => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack, smallBlind, bigBlind }); state.chipLegend = legend; Betting.beginHand(state); return state; }; test("a half-pot that lands on a half chip is rounded up, not offered as it is", () => { const state = table(DOLLARS, 1, 2); // Honestly $4.50. No dollar chip makes fifty cents. assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex, 0.5), 5); assert.equal(Money.chipBreakdown(5, DOLLARS).remainder, 0); }); test("up rather than down — a shaved pot bet is not a pot bet", () => { const state = table(DOLLARS, 1, 2); assert.ok(Betting.potRaiseTo(state, state.actingSeatIndex, 0.5) > 4.5); }); test("an amount the chips already make is left exactly alone", () => { const state = table(DOLLARS, 1, 2); assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex), 7, "the pot raise was already whole"); }); test("a cent table keeps its cents", () => { const state = table(CENTS, 0.05, 0.1); // Pot $0.15, owes $0.10: call to $0.25, raise $0.25 → $0.35, which cent chips make. assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex), 0.35); assert.equal(Money.chipBreakdown(0.35, CENTS).remainder, 0); }); test("a legend that plainly isn't for these stakes is not applied", () => { // Dollar chips on a 5c/10c game means somebody never filled the legend in. // Rounding those blinds up to whole dollars would be the tool doing real // damage on the strength of a stale field, so the amounts stay exact and the // wrong legend stays visible. const state = table(DOLLARS, 0.05, 0.1); assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex), 0.35); }); test("a table with no legend at all gets exact amounts", () => { const state = table([], 1, 2); assert.equal(Betting.potRaiseTo(state, state.actingSeatIndex, 0.5), 4.5); }); test("every fraction comes out buildable, and never shaved", () => { const state = table(DOLLARS, 1, 2); const index = state.actingSeatIndex; const potAfterCall = Betting.potSoFar(state) + Betting.toCall(state, state.seats[index]); [0.1, 0.25, 0.5, 0.75, 1].forEach(fraction => { const value = Betting.potRaiseTo(state, index, fraction); const exact = state.currentBet + potAfterCall * fraction; assert.ok(value >= exact, `${fraction} of the pot should never be rounded down`); assert.ok(value - exact < 1, `${fraction} of the pot should round by less than a chip`); assert.equal(Money.chipBreakdown(value, DOLLARS).remainder, 0, `${fraction} of the pot should be buildable from the table's chips`); }); }); });