Half the pot lands on a half chip constantly. Pre-flop at 1/2 the honest half-pot is $4.50, and the smallest chip in the default legend is a dollar — the tool's own chip breakdown calls it "4×$1, $0.50 no chip covers". The button offered it anyway, so the dealer rounded it themselves every single time. The pot-derived quick amounts now round up to something the table can build. Up, not down or nearest: these are bet sizes, a pot-sized bet that has been shaved is no longer a pot-sized bet, and rounding up cannot drop an amount below a legal minimum the way rounding the other way can. Only when the legend describes this game. A smallest chip larger than the big blind means the denominations were never set for these stakes, and rounding a 5c/10c table up to whole dollars would be the tool doing real damage on the strength of a field somebody forgot to fill in. Those tables keep exact amounts, and the wrong legend stays visible rather than being quietly applied. A table with no legend at all is told nothing about chips and so has nothing applied either. Verified across five tables: default dollar chips at 1/2 give $4/$5/$7, cent chips at 5c/10c keep $0.20/$0.23/$0.35, five-dollar chips at 5/10 give $20/$25/$35, and both the stale-legend and no-legend tables are left exact. The minimum raise is deliberately not rounded. It is what the rules require, and moving it to suit the chip tray would change what is legal rather than what is convenient. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
181 lines
8.5 KiB
JavaScript
181 lines
8.5 KiB
JavaScript
// 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`);
|
||
});
|
||
});
|
||
});
|