import { describe, expect, it } from "vitest"; import { createTable, getSnapshotFor, joinTable, leaveTable, spectateTable, startGame, } from "@/lib/minigames/poker/tables"; /** * Regression tests for the registry's game-start path. Guards against the * seat-map aliasing bug that once spawned phantom "Player null" engine seats * when a table started with fewer than four players. */ describe("poker table registry: startGame", () => { it("starts a heads-up game with exactly the two seated players", () => { const tableId = createTable(9001, "Host"); joinTable(tableId, 9002, "Guest"); const snapshot = startGame(tableId, 9001); const seatedIds = snapshot.seats.filter((id) => id !== null); expect(seatedIds).toHaveLength(2); const players = snapshot.you?.players ?? []; expect(players).toHaveLength(2); for (const player of players) { expect(player.name).not.toBe("Player null"); expect(player.name.length).toBeGreaterThan(0); expect(player.eliminated).toBe(false); } }); it("starts a three-handed game with exactly the three seated players", () => { const tableId = createTable(9101, "Host"); joinTable(tableId, 9102, "Second"); joinTable(tableId, 9103, "Third"); const snapshot = startGame(tableId, 9101); const seatedIds = snapshot.seats.filter((id) => id !== null); expect(seatedIds).toHaveLength(3); const players = snapshot.you?.players ?? []; expect(players).toHaveLength(3); const names = players.map((player) => player.name); expect(names).not.toContain("Player null"); expect(new Set(names).size).toBe(3); }); it("keeps every engine player backed by a real seated user", () => { const tableId = createTable(9201, "Host"); joinTable(tableId, 9202, "Second"); const snapshot = startGame(tableId, 9201); const players = snapshot.you?.players ?? []; for (const player of players) { const userId = snapshot.seats[player.seatId]; expect(userId).not.toBeNull(); expect(typeof userId).toBe("number"); } }); }); describe("poker table registry: last player standing", () => { it("ends the game immediately when everyone else leaves mid-hand", () => { const tableId = createTable(9301, "Host"); joinTable(tableId, 9302, "Guest"); startGame(tableId, 9301); leaveTable(tableId, 9302); const remaining = getSnapshotFor(tableId, 9301); expect(remaining.you?.phase).toBe("game-complete"); const players = remaining.you?.players ?? []; const hostPlayer = players.find((player) => player.seatId === remaining.you?.viewerSeat); const guestPlayer = players.find((player) => player.seatId !== remaining.you?.viewerSeat); expect(hostPlayer?.eliminated).toBe(false); expect(hostPlayer?.left).toBe(false); // The leaver's committed blind is forfeited (leavers take nothing from // the pot), so the host only receives their own small blind back. expect(hostPlayer?.stack).toBe(1000); expect(guestPlayer?.eliminated).toBe(true); expect(guestPlayer?.left).toBe(true); }); it("does not end the game while two or more players remain seated", () => { const tableId = createTable(9401, "Host"); joinTable(tableId, 9402, "Second"); joinTable(tableId, 9403, "Third"); startGame(tableId, 9401); leaveTable(tableId, 9403); const snapshot = getSnapshotFor(tableId, 9401); expect(snapshot.you?.phase).not.toBe("game-complete"); const third = getSnapshotFor(tableId, 9403); expect(third.you).toBeNull(); }); }); describe("poker table registry: spectators", () => { it("gives a running game's spectator the public view with no seat and no actions", () => { const tableId = createTable(9501, "Host"); joinTable(tableId, 9502, "Guest"); startGame(tableId, 9501); const snapshot = spectateTable(tableId, 9503); expect(snapshot.spectator).toBe(true); expect(snapshot.you?.viewerSeat).toBe(-1); expect(snapshot.you?.players).toHaveLength(2); expect(snapshot.you?.yourHoleCards).toHaveLength(0); expect(snapshot.you?.yourTurn).toBe(false); expect(snapshot.you?.legalActions.canFold).toBe(false); expect(snapshot.you?.legalActions.canCheck).toBe(false); expect(snapshot.you?.legalActions.canRaise).toBe(false); }); it("refuses to spectate a table without a running game", () => { const tableId = createTable(9511, "Host"); expect(() => spectateTable(tableId, 9512)).toThrow("No game is running at this table."); }); it("drops the spectator flag when they take a seat through a mid-game join", () => { const tableId = createTable(9521, "Host"); joinTable(tableId, 9522, "Guest"); startGame(tableId, 9521); spectateTable(tableId, 9523); joinTable(tableId, 9523, "Third"); const snapshot = getSnapshotFor(tableId, 9523); expect(snapshot.spectator).toBeUndefined(); const seatIndex = snapshot.seats.findIndex((id) => id === 9523); expect(seatIndex).toBeGreaterThanOrEqual(0); }); it("stops watching on leave and returns to no seat", () => { const tableId = createTable(9531, "Host"); joinTable(tableId, 9532, "Guest"); startGame(tableId, 9531); spectateTable(tableId, 9533); leaveTable(tableId, 9533); const snapshot = getSnapshotFor(tableId, 9533); expect(snapshot.spectator).toBeUndefined(); expect(snapshot.you).toBeNull(); }); });