import { beforeEach, describe, expect, it } from "vitest"; import { applyBlackjackAction, clearTablesForTests, createTable, getSnapshotFor, joinTable, leaveTable, renameTable, spectateTable, startGame, } from "@/lib/minigames/blackjack/tables"; /** * Regression tests for the blackjack registry: game start, seating, lobby * rules, and snapshots. Database-free except for the fire-and-forget award * path, which skips users who never played a round. */ beforeEach(() => { clearTablesForTests(); }); describe("blackjack table registry: startGame", () => { it("starts a solo game immediately against the dealer", () => { const tableId = createTable(9001, "Host"); const snapshot = startGame(tableId, 9001); expect(snapshot.inGame).toBe(true); expect(snapshot.you?.phase).toBe("betting"); expect(snapshot.you?.activeSeat).toBe(0); expect(snapshot.you?.yourTurn).toBe(true); expect(snapshot.you?.legalActions.canBet).toBe(true); }); it("starts a six-player game with every seat backed by a user", () => { const tableId = createTable(9101, "Host"); joinTable(tableId, 9102, "Second"); joinTable(tableId, 9103, "Third"); joinTable(tableId, 9104, "Fourth"); joinTable(tableId, 9105, "Fifth"); joinTable(tableId, 9106, "Sixth"); const snapshot = startGame(tableId, 9101); const seatedIds = snapshot.seats.filter((id) => id !== null); expect(seatedIds).toHaveLength(6); const players = snapshot.you?.players ?? []; expect(players).toHaveLength(6); for (const player of players) { expect(player.name).not.toBe("Player null"); expect(player.name.length).toBeGreaterThan(0); const userId = snapshot.seats[player.seatId]; expect(userId).not.toBeNull(); } expect(snapshot.seats).toHaveLength(6); }); it("keeps the seat map padded to six with trailing nulls", () => { const tableId = createTable(9201, "Host"); joinTable(tableId, 9202, "Second"); startGame(tableId, 9201); const snapshot = getSnapshotFor(tableId, 9201); expect(snapshot.seats).toEqual([9201, 9202, null, null, null, null]); }); }); describe("blackjack table registry: lobby rules", () => { it("rejects a second table for a user already seated elsewhere", () => { const first = createTable(9301, "Host"); createTable(9302, "Other"); expect(() => joinTable(first, 9302, "Other")).toThrow("already seated at another table"); }); it("lets the host start with a single player and rejects unstarted actions", () => { const tableId = createTable(9401, "Host"); expect(() => applyBlackjackAction(tableId, 9401, "bet", 10)).toThrow( "The game has not started yet.", ); startGame(tableId, 9401); expect(() => startGame(tableId, 9999)).toThrow("Only the host can start the game."); }); it("queues mid-game joins for the next round", () => { const tableId = createTable(9501, "Host"); joinTable(tableId, 9502, "Second"); startGame(tableId, 9501); const latecomer = joinTable(tableId, 9503, "Latecomer"); // Pending joiners have a seat in the map but no engine hand until the // next round starts, so their snapshot carries no private view yet. expect(latecomer.you).toBeNull(); expect(latecomer.seats).toContain(9503); expect(() => applyBlackjackAction(tableId, 9503, "bet", 10)).toThrow( "You will join the game at the next round.", ); }); it("spectates a running game with the public view and no seat", () => { const tableId = createTable(9601, "Host"); joinTable(tableId, 9602, "Guest"); startGame(tableId, 9601); const snapshot = spectateTable(tableId, 9603); expect(snapshot.spectator).toBe(true); expect(snapshot.you?.viewerSeat).toBe(-1); expect(snapshot.you?.players).toHaveLength(2); expect(snapshot.you?.yourTurn).toBe(false); expect(snapshot.you?.legalActions.canBet).toBe(false); expect(() => spectateTable(createTable(9604, "Idle"), 9603)).toThrow( "No game is running at this table.", ); }); it("compacts the seat map and promotes the host when leaving pre-game", () => { const tableId = createTable(9701, "Host"); joinTable(tableId, 9702, "Second"); joinTable(tableId, 9703, "Third"); leaveTable(tableId, 9701); const snapshot = getSnapshotFor(tableId, 9702); expect(snapshot.seats).toEqual([9702, 9703, null, null, null, null]); expect(snapshot.hostSeatId).toBe(0); }); it("marks mid-game leavers as left in every snapshot", () => { const tableId = createTable(9801, "Host"); joinTable(tableId, 9802, "Guest"); startGame(tableId, 9801); leaveTable(tableId, 9802); const host = getSnapshotFor(tableId, 9801); expect(host.you?.players.find((player) => player.seatId === 1)?.left).toBe(true); expect(host.you?.players.find((player) => player.seatId === 0)?.left).toBe(false); }); }); describe("blackjack table registry: gameplay through the registry", () => { it("runs a solo betting round end to end", () => { const tableId = createTable(9901, "Host"); startGame(tableId, 9901); let afterBet = applyBlackjackAction(tableId, 9901, "bet", 25); // The shoe is random: an ace upcard opens an insurance offer, and a // dealt natural settles the round on the spot. Walk whichever path comes. if (afterBet.you?.phase === "insurance") { afterBet = applyBlackjackAction(tableId, 9901, "decline-insurance"); } if (afterBet.you?.phase === "player") { afterBet = applyBlackjackAction(tableId, 9901, "stand"); } expect(afterBet.you?.phase).toBe("round-complete"); expect(afterBet.you?.yourHands).toHaveLength(1); expect(afterBet.you?.yourHands[0]?.bet).toBe(25); expect(afterBet.version).toBeGreaterThan(1); expect(afterBet.you?.roundResult).not.toBeNull(); expect(afterBet.you?.roundResult?.hands).toHaveLength(1); expect(afterBet.nextRoundAt).not.toBeNull(); // The next round has not auto-started yet (the registry timer owns that). expect(() => applyBlackjackAction(tableId, 9901, "bet", 10)).toThrow( "not accepting bets", ); }); it("validates bet limits through the registry", () => { const tableId = createTable(9911, "Host"); startGame(tableId, 9911); expect(() => applyBlackjackAction(tableId, 9911, "bet", 9)).toThrow("minimum bet"); expect(() => applyBlackjackAction(tableId, 9911, "bet", 501)).toThrow("maximum bet"); }); }); describe("blackjack table registry: table management", () => { it("renames tables as host with length truncation", () => { const tableId = createTable(9921, "Host"); const renamed = renameTable(tableId, 9921, " High Roller Room "); expect(renamed.name).toBe("High Roller Room"); const longName = "X".repeat(60); expect(renameTable(tableId, 9921, longName).name).toHaveLength(48); expect(() => renameTable(tableId, 9999, "Nope")).toThrow("Only the host"); }); it("caps live tables at the registry maximum", () => { for (let index = 0; index < 20; index += 1) { createTable(10000 + index, `Host ${index}`); } expect(() => createTable(10999, "Overflow")).toThrow("maximum number of blackjack tables"); }); it("disposes a table once everyone leaves", () => { const tableId = createTable(11001, "Host"); leaveTable(tableId, 11001); expect(() => getSnapshotFor(tableId, 11001)).toThrow("Table not found."); }); });