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>
198 lines
5.8 KiB
TypeScript
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([]);
|
|
});
|
|
});
|