// Somebody who walks up and sits down after the blinds have gone past them. // // Dealing them straight in is a free hand in late position — which is what posting // exists to stop, and it comes out of everybody else's night a couple of dollars at a // time. The tool used to do it silently. It now says so, and takes the post if the // dealer says to. const test = require("node:test"); const { describe } = require("node:test"); const assert = require("node:assert"); const { HoldemState, Betting, makeTable } = require("./load"); const NAMES = ["Ann", "Ben", "Cal", "Dee"]; const table = (dealerSeatIndex = 0) => { const state = makeTable({ seatCount: 6, seated: [0, 1, 2, 3], stack: 200, dealerSeatIndex }); NAMES.forEach((name, i) => { state.seats[i].name = name; }); return state; }; // Eve walks up and takes seat 5 while the table is running. const seatEve = state => { state.seats[4].name = "Eve"; state.seats[4].inHand = true; state.seats[4].stack = 200; state.seats[4].buyIns = 200; return 4; }; const debtors = state => { const { bbIndex } = HoldemState.getBlindSeats(state); return HoldemState.blindDebtors(state, bbIndex).map(i => state.seats[i].name); }; describe("who is owed a blind", () => { test("nobody, at a table that hasn't dealt a hand yet", () => { // Everyone is new on the first hand. Asking the whole table to post would be // the tool mistaking the start of the night for five people walking up late. const state = table(); assert.deepEqual(debtors(state), []); }); test("nobody, on an ordinary hand where the same people are still sitting there", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); assert.deepEqual(debtors(state), []); }); test("the player who has just sat down", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); seatEve(state); assert.deepEqual(debtors(state), ["Eve"]); }); test("not once they've been dealt a hand", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); seatEve(state); Betting.beginHand(state); HoldemState.startNextHand(state); assert.deepEqual(debtors(state), [], "she's one of the regulars now"); }); test("not somebody who is the big blind this hand — they're paying already", () => { const state = table(0); // blinds Ben and Cal Betting.beginHand(state); HoldemState.startNextHand(state); // blinds move to Cal and Dee // Dee sits out for a hand and comes straight back into her own big blind. state.seats[3].inHand = false; Betting.beginHand(state); HoldemState.startNextHand(state); state.seats[3].inHand = true; const { bbIndex } = HoldemState.getBlindSeats(state); assert.ok(!HoldemState.blindDebtors(state, bbIndex).includes(bbIndex), "the big blind is never asked to post on top of their blind"); }); test("somebody back from sitting out is owed one just like a newcomer", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); state.seats[3].inHand = false; // Dee stands up for a hand Betting.beginHand(state); HoldemState.startNextHand(state); state.seats[3].inHand = true; // and sits back down assert.ok(debtors(state).includes("Dee"), "the blinds went past her while she was up"); }); test("a seat sat out for having no chips isn't owed anything", () => { // They aren't being dealt in, so there's no free hand to charge for. const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); const eve = seatEve(state); state.seats[eve].stack = 0; const preview = Betting.blindPreview(state); assert.ok(!preview.debtors.includes(eve), "she's sitting out until she buys chips"); }); }); describe("posting to come in", () => { test("the post goes in the middle and buys a hand", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); const eve = seatEve(state); state.postingIn = [state.seats[eve].id]; const before = Betting.stackOf(state, state.seats[eve]); Betting.beginHand(state); assert.equal(Betting.committed(state.seats[eve]), state.bigBlind, "she's posted a big blind"); assert.equal(Betting.stackOf(state, state.seats[eve]), before - state.bigBlind, "out of her own stack, not conjured"); assert.equal(state.currentBet, state.bigBlind, "and it doesn't raise the hand"); }); test("not posting leaves her stack alone", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); const eve = seatEve(state); const before = Betting.stackOf(state, state.seats[eve]); Betting.beginHand(state); assert.equal(Betting.committed(state.seats[eve]), 0); assert.equal(Betting.stackOf(state, state.seats[eve]), before); }); test("the pot is what the blinds and the post add up to", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); const eve = seatEve(state); state.postingIn = [state.seats[eve].id]; Betting.beginHand(state); const middle = state.seats.reduce((sum, p) => sum + Betting.committed(p), 0); assert.equal(middle, state.smallBlind + state.bigBlind + state.bigBlind); }); test("it's spent after the hand, not a standing charge", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); const eve = seatEve(state); state.postingIn = [state.seats[eve].id]; Betting.beginHand(state); HoldemState.startNextHand(state); assert.deepEqual(state.postingIn, [], "or she'd pay an extra blind every hand all night"); Betting.beginHand(state); assert.ok(Betting.committed(state.seats[eve]) === 0 || state.seats[eve].id === state.seats[HoldemState.getBlindSeats(state).bbIndex]?.id, "she pays nothing extra on the next hand"); }); test("moving the button after a post doesn't lose it", () => { // repostBlinds takes every posted chip back and deals again. A post that // cleared itself on the way in would be refunded and never retaken. const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); const eve = seatEve(state); state.postingIn = [state.seats[eve].id]; Betting.beginHand(state); const chips = () => state.seats.reduce( (sum, p) => sum + Betting.stackOf(state, p) + Betting.handTotal(p), 0); const before = chips(); state.dealerSeatIndex = 2; Betting.repostBlinds(state); assert.equal(Betting.committed(state.seats[eve]), state.bigBlind, "still posted"); assert.equal(chips(), before, "and no chips made or lost in the shuffle"); }); test("a post from somebody who isn't owed one is not taken", () => { // Only seats actually being dealt in can post, and only for their own chips. const state = table(); state.postingIn = [state.seats[3].id]; state.seats[3].inHand = false; Betting.beginHand(state); assert.equal(Betting.committed(state.seats[3]), 0, "she isn't even in the hand"); }); }); describe("what the dealer is shown", () => { test("the prompt has something to say about it, and something to press", () => { const state = table(); Betting.beginHand(state); HoldemState.startNextHand(state); const eve = seatEve(state); const preview = Betting.blindPreview(state); assert.deepEqual(preview.debtors, [eve]); assert.deepEqual(preview.posting, [], "not posting until the dealer says so"); state.postingIn = [state.seats[eve].id]; assert.deepEqual(Betting.blindPreview(state).posting, [state.seats[eve].id]); }); test("an old save reads as a table nobody has just walked up to", () => { const state = table(); Betting.beginHand(state); const parsed = JSON.parse(JSON.stringify(state)); delete parsed.dealtLastHand; delete parsed.postingIn; const store = new Map([[HoldemState.STORAGE_KEY, JSON.stringify(parsed)]]); globalThis.localStorage = { getItem: key => (store.has(key) ? store.get(key) : null), setItem: (key, value) => store.set(key, value), removeItem: key => store.delete(key), }; const loaded = HoldemState.load(); assert.deepEqual(loaded.dealtLastHand, []); assert.deepEqual(loaded.postingIn, []); assert.deepEqual(debtors(loaded), [], "reopening the tab doesn't ask the whole table to post"); delete globalThis.localStorage; }); });