1
0
Fork 0
polaris-task-force/tests/int/blackjack-sound.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

198 lines
5.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Card, PublicPlayerView } from "@/lib/minigames/blackjack";
import {
soundEventsForTransition,
type BlackjackSoundEvent,
type BlackjackSoundSnapshot,
} from "@/components/frontend/minigames/blackjack/blackjackSoundEvents";
const card = (rank: Card["rank"], suit: Card["suit"]): Card => ({ rank, suit });
function player(seatId: number, overrides: Partial<PublicPlayerView> = {}): PublicPlayerView {
return {
seatId,
name: `Player ${seatId + 1}`,
bankroll: 1000,
hands: [],
insuranceBet: 0,
insuranceAnswered: false,
hasBet: false,
sittingOut: false,
bankrupt: false,
left: false,
roundsPlayed: 0,
roundsWon: 0,
blackjacksHit: 0,
...overrides,
};
}
function hand(
overrides: Partial<PublicPlayerView["hands"][number]> = {},
): PublicPlayerView["hands"][number] {
return {
cards: [],
bet: 25,
status: "active",
doubled: false,
fromSplit: false,
splitAces: false,
total: 21,
outcome: null,
netChange: null,
...overrides,
};
}
function view(
overrides: Partial<BlackjackSoundSnapshot["you"]> = {},
): BlackjackSoundSnapshot["you"] {
return {
roundNumber: 1,
phase: "betting",
players: [player(0), player(1)],
dealer: { cards: [], holeHidden: true, total: null, hasBlackjack: null },
roundResult: null,
viewerSeat: 0,
yourTurn: false,
yourHands: [],
...overrides,
};
}
function snapshot(
version: number,
nextView: BlackjackSoundSnapshot["you"] = view(),
): BlackjackSoundSnapshot {
return { version, you: nextView };
}
function byType(events: readonly BlackjackSoundEvent[], type: BlackjackSoundEvent["type"]) {
return events.find((event) => event.type === type);
}
describe("blackjack sound transition events", () => {
it("plays a round-start chime when the round counter advances", () => {
const events = soundEventsForTransition(
snapshot(1, view({ roundNumber: 1, phase: "round-complete" })),
snapshot(2, view({ roundNumber: 2, phase: "betting" })),
);
expect(byType(events, "round-start")).toBeDefined();
});
it("announces turn changes", () => {
const events = soundEventsForTransition(
snapshot(1, view({ yourTurn: false })),
snapshot(2, view({ yourTurn: true, phase: "player" })),
);
expect(byType(events, "turn-gained")).toBeDefined();
const back = soundEventsForTransition(
snapshot(2, view({ yourTurn: true, phase: "player" })),
snapshot(3, view({ yourTurn: false, phase: "player" })),
);
expect(byType(back, "turn-lost")).toBeDefined();
});
it("announces cards hitting the felt", () => {
const dealtCards = [card(10, "s"), card(9, "h")];
const events = soundEventsForTransition(
snapshot(1, view()),
snapshot(
2,
view({
phase: "player",
players: [player(0, { hasBet: true, hands: [hand({ cards: dealtCards })] }), player(1)],
dealer: { cards: [card(6, "d"), card(7, "c")], holeHidden: true, total: null, hasBlackjack: null },
}),
),
);
const deal = byType(events, "cards-dealt");
expect(deal).toBeDefined();
if (deal?.type === "cards-dealt") expect(deal.cardCount).toBe(4);
});
it("announces a wager and a bust", () => {
const events = soundEventsForTransition(
snapshot(1, view({ players: [player(0, { hands: [hand()] }), player(1)] })),
snapshot(
2,
view({
players: [player(0, { hasBet: true, hands: [hand({ cards: [card(5, "s"), card(6, "s")] })] }), player(1)],
}),
),
);
expect(byType(events, "wager")).toBeDefined();
const bustEvents = soundEventsForTransition(
snapshot(
3,
view({
phase: "player",
players: [player(0, { hasBet: true, hands: [hand({ cards: [card(5, "s"), card(6, "s")] })] }), player(1)],
}),
),
snapshot(
4,
view({
phase: "player",
players: [
player(0, {
hasBet: true,
hands: [hand({ status: "bust", cards: [card(5, "s"), card(6, "s"), card(10, "d")] })],
}),
player(1),
],
}),
),
);
expect(byType(bustEvents, "bust")).toBeDefined();
});
it("settles with a viewer result event", () => {
const settled = view({
phase: "round-complete",
roundResult: {
roundNumber: 1,
dealerTotal: 19,
dealerBusted: false,
dealerHasBlackjack: false,
hands: [
{ seatId: 0, handIndex: 0, total: 20, outcome: "win", payout: 25, netChange: 25 },
],
insurance: [],
},
});
const events = soundEventsForTransition(snapshot(1, view()), snapshot(2, settled));
expect(byType(events, "settle")).toBeDefined();
const result = byType(events, "hand-result");
expect(result).toBeDefined();
if (result?.type === "hand-result") {
expect(result.result).toBe("win");
expect(result.netChange).toBe(25);
}
});
it("reports a push result on a tie", () => {
const settled = view({
phase: "round-complete",
roundResult: {
roundNumber: 1,
dealerTotal: 20,
dealerBusted: false,
dealerHasBlackjack: false,
hands: [{ seatId: 0, handIndex: 0, total: 20, outcome: "push", payout: 0, netChange: 0 }],
insurance: [],
},
});
const events = soundEventsForTransition(snapshot(1, view()), snapshot(2, settled));
const result = byType(events, "hand-result");
expect(result).toBeDefined();
if (result?.type === "hand-result") expect(result.result).toBe("push");
});
it("returns no events for the same version", () => {
const current = snapshot(5, view());
expect(soundEventsForTransition(current, current)).toEqual([]);
});
});