1
0
Fork 0
polaris-task-force/tests/int/blackjack-tables.int.spec.ts
Z8MB1E 474aff734d feat(blackjack): add blackjack minigame with realtime table wiring
Engine, felt, controls, sounds, card skins, and dev popups; SSE events in the realtime hook, nav and command palette entries, stats migration, and the six blackjack test suites.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-14 15:38:05 -04:00

189 lines
7.3 KiB
TypeScript

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.");
});
});