// Dealing and betting are two decisions, and fusing them meant the table guessed. // // The moment between hands is when people sit down, stand up and argue about whose // turn it is. With blinds posted the instant the previous hand ended, the blind // badges recomputed off `inHand` while the committed chips stayed frozen — so seating // somebody produced a big blind who had posted nothing. const { test, describe } = require("node:test"); const assert = require("node:assert/strict"); const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load"); const postedBy = state => state.seats .map((p, i) => (Betting.committed(p) > 0 ? i : null)) .filter(i => i !== null); describe("the table waits to be dealt", () => { test("finishing a hand leaves the next one un-dealt", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2], dealerSeatIndex: 0 }); Betting.beginHand(state); assert.equal(state.bettingOpen, true); HoldemState.startNextHand(state); assert.equal(state.bettingOpen, false, "the next hand hasn't been dealt"); assert.equal(state.actingSeatIndex, null, "nobody is on the clock"); assert.deepEqual(postedBy(state), [], "and no blind has been committed"); }); test("no money moves until the dealer deals", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2], dealerSeatIndex: 0 }); const start = chipsOnTable(state); HoldemState.startNextHand(state); assert.equal(chipsOnTable(state), start); assert.equal(HoldemState.totalPot(state), 0); Betting.beginHand(state); assert.equal(state.bettingOpen, true); assert.equal(postedBy(state).length, 2, "now the blinds are in"); assert.equal(chipsOnTable(state), start, "and nothing was created doing it"); }); }); describe("setting the table costs nothing", () => { test("seating a player before the deal changes who posts", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2], dealerSeatIndex: 0 }); HoldemState.startNextHand(state); const before = Betting.blindPreview(state); state.seats[3].inHand = true; state.seats[3].stack = 100; const after = Betting.blindPreview(state); assert.notDeepEqual( [before.sbIndex, before.bbIndex], [after.sbIndex, after.bbIndex], "the preview follows the new table" ); assert.deepEqual(postedBy(state), [], "and still nothing is committed"); Betting.beginHand(state); assert.deepEqual(postedBy(state).sort(), [after.sbIndex, after.bbIndex].sort(), "the money lands where the preview said it would"); }); test("removing a player before the deal can't strand their blind", () => { // The old failure: take out the big blind after the hand had begun and their // chips stayed in the pot while the badge moved to somebody who'd posted // nothing. Before the deal there is nothing to strand. const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 }); HoldemState.startNextHand(state); const doomed = Betting.blindPreview(state).bbIndex; state.seats[doomed].inHand = false; assert.deepEqual(postedBy(state), [], "nobody has posted, so nothing is stranded"); Betting.beginHand(state); const posted = postedBy(state); assert.ok(!posted.includes(doomed), "the player who left posts nothing"); posted.forEach(i => assert.ok(state.seats[i].inHand, "only players in the hand post")); }); test("moving the button before the deal just moves it", () => { const state = makeTable({ seatCount: 8, seated: [0, 2, 4, 6], dealerSeatIndex: 0 }); const start = chipsOnTable(state); HoldemState.startNextHand(state); [2, 4, 6, 0].forEach(seat => { state.dealerSeatIndex = seat; assert.deepEqual(postedBy(state), [], `passing the button to seat ${seat + 1} posted something`); assert.equal(chipsOnTable(state), start); }); Betting.beginHand(state); assert.equal(chipsOnTable(state), start, "dealing moves chips between stacks, not into thin air"); }); test("what the preview promises is what gets posted", () => { const state = makeTable({ seatCount: 9, seated: [1, 3, 6, 8], dealerSeatIndex: 1 }); HoldemState.startNextHand(state); const preview = Betting.blindPreview(state); Betting.beginHand(state); assert.equal(Betting.committed(state.seats[preview.sbIndex]), preview.smallBlind); assert.equal(Betting.committed(state.seats[preview.bbIndex]), preview.bigBlind); }); test("there's nothing to preview without two players", () => { const alone = makeTable({ seatCount: 6, seated: [2] }); HoldemState.startNextHand(alone); assert.equal(Betting.blindPreview(alone), null); const empty = makeTable({ seatCount: 6, seated: [] }); HoldemState.startNextHand(empty); assert.equal(Betting.blindPreview(empty), null); }); test("a hand-picked blind shows up in the preview", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], dealerSeatIndex: 0 }); HoldemState.startNextHand(state); state.blindOverride = { sbIndex: 3, bbIndex: 0 }; const preview = Betting.blindPreview(state); assert.equal(preview.manual, true); assert.equal(preview.sbIndex, 3); assert.equal(preview.bbIndex, 0); }); }); describe("a table saved mid-hand comes back mid-hand", () => { const loadWith = saved => { const original = globalThis.localStorage; globalThis.localStorage = { getItem: () => JSON.stringify(saved), setItem: () => {}, removeItem: () => {} }; const state = HoldemState.load(); globalThis.localStorage = original; return state; }; test("chips already committed mean the betting was open", () => { const state = loadWith({ seats: [{ id: "a", name: "Ann", inHand: true, committed: 2 }, { id: "b", name: "Ben", inHand: true, committed: 0 }], handNumber: 3, }); assert.equal(state.bettingOpen, true, "otherwise it would offer to deal over a live hand"); }); test("somebody on the clock means the betting was open", () => { const state = loadWith({ seats: [{ id: "a", inHand: true }, { id: "b", inHand: true }], actingSeatIndex: 1, handNumber: 2, }); assert.equal(state.bettingOpen, true); }); test("a quiet table comes back waiting to be dealt", () => { const state = loadWith({ seats: [{ id: "a", inHand: true }, { id: "b", inHand: true }], actingSeatIndex: null, handNumber: 5, }); assert.equal(state.bettingOpen, false); }); }); describe("the preview tells the truth about the button", () => { test("a button parked on an empty chair is answered for where it will be", () => { // The preview used to read the button where it sat, while dealing relocated it // first — so it promised one pair of blinds and produced another. const state = makeTable({ seatCount: 6, seated: [1, 2, 3], dealerSeatIndex: 0 }); state.bettingOpen = false; assert.equal(state.seats[0].inHand, false, "the button is on an empty chair"); const preview = Betting.blindPreview(state); assert.equal(preview.buttonMoves, true, "and the preview knows it will move"); Betting.beginHand(state); assert.equal(Betting.committed(state.seats[preview.sbIndex]), preview.smallBlind); assert.equal(Betting.committed(state.seats[preview.bbIndex]), preview.bigBlind); assert.equal(state.dealerSeatIndex, preview.buttonIndex, "and it moved where it said"); }); test("asking where the button will be doesn't move it", () => { const state = makeTable({ seatCount: 6, seated: [1, 2, 3], dealerSeatIndex: 0 }); state.bettingOpen = false; Betting.blindPreview(state); Betting.blindPreview(state); assert.equal(state.dealerSeatIndex, 0, "the preview is a question, not an action"); }); test("a button already on a player is left alone", () => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2], dealerSeatIndex: 0 }); state.bettingOpen = false; const preview = Betting.blindPreview(state); assert.equal(preview.buttonMoves, false); assert.equal(preview.buttonIndex, 0); }); test("buttonSeatForDeal copes with a table nobody is sitting at", () => { const state = makeTable({ seatCount: 6, seated: [] }); assert.equal(HoldemState.buttonSeatForDeal(state), state.dealerSeatIndex); }); });