// Values a dealer can actually produce with a typo, and the ones a corrupted save // can produce on its own. None of these should be able to move chips. const { test, describe } = require("node:test"); const assert = require("node:assert/strict"); const { Betting, HoldemState, makeTable, chipsOnTable } = require("./load"); describe("a stack can never go negative", () => { test("a mistyped negative stack reads as empty, not as owing chips", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2] }); state.seats[0].stack = -50; assert.equal(Betting.stackOf(state, state.seats[0]), 0); }); test("a negative stack can't be bet with", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2] }); state.seats[0].stack = -50; const before = state.seats.reduce((s, p) => s + Math.max(0, Number(p.stack) || 0), 0); const moved = Betting.putIn(state, 0, 25); assert.equal(moved, 0, "nothing moves out of an empty stack"); assert.ok(Betting.stackOf(state, state.seats[0]) >= 0); assert.equal(state.seats[0].committed, 0, "and nothing appears in front of them"); assert.ok(state.seats.reduce((s, p) => s + Math.max(0, Number(p.stack) || 0), 0) <= before); }); test("a negative table default doesn't hand everyone a negative stack", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2] }); state.seats.forEach(p => { p.stack = null; }); state.defaultStack = -100; state.seats.forEach(p => assert.equal(Betting.stackOf(state, p), 0)); }); test("a negative stack doesn't make a player look like they can still bet", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2] }); state.seats[0].stack = -50; assert.ok(!Betting.seatsWithChips(state).some(s => s.index === 0)); }); }); describe("blinds that can't be posted", () => { test("negative blinds post nothing rather than paying players to sit down", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2] }); state.smallBlind = -5; state.bigBlind = -10; const before = chipsOnTable(state); Betting.beginHand(state); state.seats.forEach(p => assert.equal(p.committed, 0)); assert.equal(chipsOnTable(state), before); }); test("an unreadable blind doesn't wedge the hand", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2] }); state.bigBlind = NaN; state.smallBlind = 1; Betting.beginHand(state); assert.ok(Number.isFinite(state.currentBet), "the bet to match is still a number"); assert.ok(!Number.isNaN(Betting.potSoFar(state))); }); test("blinds nobody can cover put those players all-in and move on", () => { const state = makeTable({ seatCount: 4, seated: [0, 1, 2], stack: 10 }); state.smallBlind = 500; state.bigBlind = 1000; const before = chipsOnTable(state); Betting.beginHand(state); assert.equal(chipsOnTable(state), before, "no chips invented covering a blind"); state.seats.forEach(p => assert.ok((Number(p.stack) || 0) >= 0)); assert.ok(state.actingSeatIndex === null || state.seats[state.actingSeatIndex].inHand); }); }); describe("a saved table that comes back damaged", () => { test("unreadable storage falls back to a fresh table", () => { // load() has to survive whatever is in localStorage, because the alternative // is a dealer opening the tool mid-session to a blank screen. const original = globalThis.localStorage; globalThis.localStorage = { getItem: () => "{not json at all", setItem: () => {}, removeItem: () => {}, }; const state = HoldemState.load(); globalThis.localStorage = original; assert.equal(state.seats.length, HoldemState.DEFAULT_SEAT_COUNT); assert.equal(state.handNumber, 1); }); test("a save missing its seats is treated as no save at all", () => { const original = globalThis.localStorage; globalThis.localStorage = { getItem: () => JSON.stringify({ handNumber: 7, smallBlind: 3 }), setItem: () => {}, removeItem: () => {}, }; const state = HoldemState.load(); globalThis.localStorage = original; assert.ok(Array.isArray(state.seats) && state.seats.length > 0); }); test("a save from before a field existed gets the field, not undefined", () => { const original = globalThis.localStorage; globalThis.localStorage = { getItem: () => JSON.stringify({ seats: [{ id: "a", name: "Ann", inHand: true }, { id: "b", name: "Ben", inHand: true }], handNumber: 4, }), setItem: () => {}, removeItem: () => {}, }; const state = HoldemState.load(); globalThis.localStorage = original; assert.equal(state.blindOverride, null); assert.equal(state.street, "preflop"); state.seats.forEach(p => { assert.equal(p.committed, 0); assert.equal(p.hasActed, false); assert.equal(p.buyIns, 0); }); // And it has to be playable, not just parseable. assert.doesNotThrow(() => Betting.beginHand(state)); }); test("storage that refuses to be written doesn't take the table down", () => { const original = globalThis.localStorage; globalThis.localStorage = { getItem: () => null, setItem: () => { throw new Error("QuotaExceededError"); }, removeItem: () => {}, }; const state = HoldemState.createDefault(); assert.doesNotThrow(() => HoldemState.save(state), "a full disk must not break the hand in progress"); globalThis.localStorage = original; }); });