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>
189 lines
7.3 KiB
TypeScript
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.");
|
|
});
|
|
});
|