// The straddle: a blind raise put out before the cards by the player left of the big // blind, for twice it. // // Home games run them constantly, and it is three rules at once — the amount, who is // first to act, and what the minimum raise becomes. Faking one by dealing and having // the straddler "raise" gets the last of those right and the other two wrong. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load"); // Six seats, everyone in, button on seat 0: small blind seat 1, big blind seat 2, // straddle seat 3. const table = (straddles = 0, extra = {}) => { const state = makeTable({ seatCount: 6, stack: 200, smallBlind: 1, bigBlind: 2, dealerSeatIndex: 0, ...extra }); state.straddleCount = straddles; return state; }; const dealt = (straddles = 0, extra = {}) => { const state = table(straddles, extra); Betting.beginHand(state); return state; }; describe("what goes in the middle", () => { test("the seat left of the big blind puts out twice it", () => { const state = dealt(1); assert.deepEqual(state.seats.map(p => p.committed), [0, 1, 2, 4, 0, 0]); }); test("a re-straddle doubles again, one seat further round", () => { const state = dealt(2); assert.deepEqual(state.seats.map(p => p.committed), [0, 1, 2, 4, 8, 0]); }); test("none of it comes from anywhere but the straddler's stack", () => { const before = chipsOnTable(table(2)); assert.equal(chipsOnTable(dealt(2)), before); }); test("the felt is told who straddled, not left to work it out", () => { assert.deepEqual(dealt(2).postedStraddles, [3, 4]); assert.deepEqual(dealt(0).postedStraddles, []); }); }); describe("who is on the clock", () => { test("action starts left of the straddler, not left of the big blind", () => { assert.equal(dealt(0).actingSeatIndex, 3, "without a straddle it is the seat after the big blind"); assert.equal(dealt(1).actingSeatIndex, 4); assert.equal(dealt(2).actingSeatIndex, 5); }); test("the straddler still has the option when it comes back to them", () => { const state = dealt(1); // Everyone calls the straddle round to the big blind. [4, 5, 0, 1, 2].forEach(seat => { assert.equal(state.actingSeatIndex, seat, `expected seat ${seat} to be up`); Betting.call(state, seat); }); assert.equal(state.actingSeatIndex, 3, "back to the straddler"); assert.equal(Betting.toCall(state, state.seats[3]), 0, "nothing to call"); assert.equal(Betting.isStreetComplete(state), false, "and the street isn't over until they say so"); }); test("the straddler checking closes it", () => { const state = dealt(1); [4, 5, 0, 1, 2].forEach(seat => Betting.call(state, seat)); Betting.check(state, 3); assert.equal(Betting.isStreetComplete(state), true); assert.equal(state.actingSeatIndex, null); }); test("the big blind acts in turn, not last", () => { const state = dealt(1); Betting.call(state, 4); Betting.call(state, 5); Betting.call(state, 0); Betting.call(state, 1); assert.equal(state.actingSeatIndex, 2, "the big blind is up before the straddler"); assert.equal(Betting.toCall(state, state.seats[2]), 2, "and owes the difference to the straddle"); }); }); describe("what it costs to raise", () => { test("the minimum raise is twice the straddle, not the straddle plus a blind", () => { // $2 blinds, $4 straddle: the next raise is to $8. Measured off the big blind // instead it comes out at $6, and every player at the table would argue. assert.equal(Betting.minRaiseTo(dealt(1)), 8); assert.equal(Betting.minRaiseTo(dealt(2)), 16); assert.equal(Betting.minRaiseTo(dealt(0)), 4); }); test("what the table owes is measured off the straddle", () => { const state = dealt(1); assert.equal(state.currentBet, 4); assert.equal(Betting.toCall(state, state.seats[4]), 4, "a fresh player calls the straddle"); assert.equal(Betting.toCall(state, state.seats[1]), 3, "the small blind owes the rest"); assert.equal(Betting.toCall(state, state.seats[2]), 2, "so does the big blind"); assert.equal(Betting.toCall(state, state.seats[3]), 0, "the straddler is already good"); }); test("a raise over the straddle is a raise, and the straddler can still re-raise", () => { const state = dealt(1); Betting.raiseTo(state, 4, 12); assert.equal(state.currentBet, 12); assert.equal(Betting.minRaiseTo(state), 20, "twice again over a raise of eight"); assert.equal(state.seats[3].hasActed, false, "the straddler has not acted yet"); }); }); describe("how many a table can take", () => { test("everyone but the two blinds", () => { assert.equal(Betting.maxStraddles(table(0)), 4); assert.equal(Betting.maxStraddles(table(0, { seatCount: 3, seated: [0, 1, 2] })), 1); }); test("heads-up, none — there is no seat left of the big blind to straddle from", () => { const heads = table(1, { seatCount: 6, seated: [0, 1] }); assert.equal(Betting.maxStraddles(heads), 0); assert.deepEqual(Betting.straddleSeats(heads), []); Betting.beginHand(heads); assert.deepEqual(heads.seats.map(p => p.committed), [1, 2, 0, 0, 0, 0]); }); test("asking for more than the table has stops at the last seat", () => { const state = table(0, { seatCount: 4, seated: [0, 1, 2, 3] }); state.straddleCount = 99; assert.equal(Betting.straddleSeats(state).length, 2); Betting.beginHand(state); assert.deepEqual(state.seats.map(p => p.committed), [8, 1, 2, 4]); assert.equal(state.actingSeatIndex, 1, "round to the small blind, who is first to act"); }); test("a seat with no chips is never handed one", () => { const state = table(1); state.seats[3].stack = 0; // Seat 3 is sat out for the deal, so the straddle passes to seat 4. assert.deepEqual(Betting.straddleSeats(state), [{ index: 4, amount: 4 }]); }); }); describe("what it doesn't outlive", () => { test("the hand it was called for", () => { const state = dealt(1); HoldemState.startNextHand(state); assert.equal(state.straddleCount, 0, "nobody straddles again by accident"); assert.deepEqual(state.postedStraddles, []); }); test("but it survives the button being pushed around before the deal", () => { // repostBlinds takes every posted chip back and deals again; a straddle that // vanished there would have been paid once and never returned. const state = dealt(1); const before = chipsOnTable(state); Betting.repostBlinds(state); assert.deepEqual(state.seats.map(p => p.committed), [0, 1, 2, 4, 0, 0]); assert.equal(chipsOnTable(state), before); }); test("a table saved before straddles existed reads as having none", () => { const parsed = JSON.parse(JSON.stringify(makeTable({ seatCount: 6 }))); delete parsed.straddleCount; delete parsed.postedStraddles; const migrated = HoldemState.migrate(parsed); assert.equal(migrated.straddleCount, 0); assert.deepEqual(migrated.postedStraddles, []); }); }); describe("what the dealer is shown before dealing", () => { test("the straddles that are coming, and how many more it could take", () => { const preview = Betting.blindPreview(table(1)); assert.deepEqual(preview.straddles, [{ index: 3, amount: 4 }]); assert.equal(preview.maxStraddles, 4); }); test("and the preview matches what dealing actually does", () => { const state = table(2); const preview = Betting.blindPreview(state); Betting.beginHand(state); assert.deepEqual(preview.straddles.map(s => s.index), state.postedStraddles); preview.straddles.forEach(({ index, amount }) => { assert.equal(state.seats[index].committed, amount, `seat ${index + 1}`); }); }); });