1
0
Fork 0
polaris-task-force/tests/int/poker-sound.int.spec.ts
Z8MB1E 56e3d253c9 feat(poker): add synthesized sfx and generative background jazz
Web audio synth with zero audio assets: deal whooshes, chip clacks, turn chimes, showdown and win stingers, plus a lazy 92bpm jazz loop. Transitions drive playback from pure snapshot mappers with staggered delays for the blind sequence, a header mute and volume toggle persists, and everything no-ops safely without audio support.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-11 17:07:12 -04:00

242 lines
7.5 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { Card, PublicPlayerView, ShowdownOutcome } from "@/lib/minigames/poker";
import {
soundEventsForTransition,
type PokerSoundSnapshot,
} from "@/components/frontend/minigames/poker/pokerSoundEvents";
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}`,
stack: 1000,
committed: 0,
totalCommitted: 0,
folded: false,
allIn: false,
hasActed: false,
eliminated: false,
left: false,
holeCards: null,
cardCount: 2,
...overrides,
};
}
function view(overrides: Partial<PokerSoundSnapshot["you"]> = {}): PokerSoundSnapshot["you"] {
return {
communityCards: [],
config: { startingStack: 1000, smallBlind: 10, bigBlind: 20, turnTimeoutMs: 30000 },
currentBet: 0,
smallBlindSeat: 0,
bigBlindSeat: 1,
finalPlacements: null,
handNumber: 1,
phase: "betting",
players: [player(0), player(1)],
showdown: null,
street: "preflop",
toActSeat: 0,
viewerSeat: 0,
yourTurn: false,
...overrides,
};
}
function snapshot(
version: number,
nextView: PokerSoundSnapshot["you"] = view(),
): PokerSoundSnapshot {
return { version, you: nextView };
}
function showdown(netChange: number): ShowdownOutcome {
return {
pots: [],
results: [
{
seatId: 0,
handRank: null,
bestHand: null,
chipsWon: 100,
netChange,
placement: netChange > 0 ? 1 : 2,
},
],
};
}
describe("poker sound transition events", () => {
it("emits a street deal descriptor for flop, turn, and river transitions", () => {
const flop = soundEventsForTransition(
snapshot(1),
snapshot(
2,
view({ street: "flop", communityCards: [card(2, "s"), card(7, "h"), card(13, "d")] }),
),
);
const turn = soundEventsForTransition(
snapshot(
2,
view({ street: "flop", communityCards: [card(2, "s"), card(7, "h"), card(13, "d")] }),
),
snapshot(
3,
view({
street: "turn",
communityCards: [card(2, "s"), card(7, "h"), card(13, "d"), card(9, "c")],
}),
),
);
const river = soundEventsForTransition(
snapshot(
3,
view({
street: "turn",
communityCards: [card(2, "s"), card(7, "h"), card(13, "d"), card(9, "c")],
}),
),
snapshot(
4,
view({
street: "river",
communityCards: [card(2, "s"), card(7, "h"), card(13, "d"), card(9, "c"), card(4, "h")],
}),
),
);
expect(flop).toEqual([
expect.objectContaining({ type: "street-deal", street: "flop", cardCount: 3 }),
]);
expect(turn).toEqual([
expect.objectContaining({ type: "street-deal", street: "turn", cardCount: 1 }),
]);
expect(river).toEqual([
expect.objectContaining({ type: "street-deal", street: "river", cardCount: 1 }),
]);
});
it("emits turn gained and turn lost descriptors", () => {
const gained = soundEventsForTransition(
snapshot(1),
snapshot(2, view({ yourTurn: true, toActSeat: 0 })),
);
const lost = soundEventsForTransition(
snapshot(2, view({ yourTurn: true, toActSeat: 0 })),
snapshot(3, view({ yourTurn: false, toActSeat: 1 })),
);
expect(gained.map((event) => event.type)).toContain("turn-gained");
expect(lost.map((event) => event.type)).toContain("turn-lost");
});
it("emits wager, check, and fold descriptors from player transitions", () => {
const previous = snapshot(1, view({ players: [player(0), player(1)] }));
const next = snapshot(
2,
view({
currentBet: 20,
players: [
player(0, { committed: 20, totalCommitted: 20, hasActed: true }),
player(1, { folded: true, hasActed: true }),
],
}),
);
const wagerEvents = soundEventsForTransition(previous, next);
const checkEvents = soundEventsForTransition(
previous,
snapshot(3, view({ players: [player(0), player(1, { hasActed: true })] })),
);
expect(wagerEvents.map((event) => event.type)).toEqual(["wager", "fold"]);
expect(checkEvents.map((event) => event.type)).toEqual(["check"]);
});
it("emits win and loss results plus game completion placement", () => {
const win = soundEventsForTransition(
snapshot(1),
snapshot(2, view({ phase: "hand-complete", showdown: showdown(40) })),
);
const loss = soundEventsForTransition(
snapshot(1),
snapshot(2, view({ phase: "hand-complete", showdown: showdown(-40) })),
);
const complete = soundEventsForTransition(
snapshot(1),
snapshot(2, view({ phase: "game-complete", finalPlacements: [{ seatId: 0, placement: 1 }] })),
);
expect(win).toEqual([expect.objectContaining({ type: "hand-result", result: "win" })]);
expect(loss).toEqual([expect.objectContaining({ type: "hand-result", result: "loss" })]);
expect(complete).toEqual([expect.objectContaining({ type: "game-complete", placement: 1 })]);
});
it("returns no events for an identical snapshot version", () => {
const current = snapshot(7, view({ yourTurn: true }));
expect(soundEventsForTransition(current, current)).toEqual([]);
});
it("announces a new hand with a two-card deal and a staggered blind sequence", () => {
const events = soundEventsForTransition(snapshot(5), snapshot(6, view({ handNumber: 2 })));
expect(events).toEqual([
expect.objectContaining({ type: "hand-start", handNumber: 2 }),
expect.objectContaining({
type: "wager",
amount: 10,
seatId: 0,
delay: 0,
}),
expect.objectContaining({
type: "wager",
amount: 20,
seatId: 1,
delay: 700,
}),
]);
});
it("plays the staggered blind sequence for the game's first hand out of the waiting room", () => {
const waitingRoom: PokerSoundSnapshot = { version: 1, you: null };
const events = soundEventsForTransition(waitingRoom, snapshot(2, view({ handNumber: 1 })));
expect(events.map((event) => event.type)).toEqual(["hand-start", "wager", "wager"]);
expect(events[1]).toEqual(expect.objectContaining({ amount: 10, delay: 0 }));
expect(events[2]).toEqual(expect.objectContaining({ amount: 20, delay: 700 }));
});
it("plays the showdown reveal plus the win jingle on showdown entry", () => {
const events = soundEventsForTransition(
snapshot(1),
snapshot(2, view({ phase: "showdown", showdown: showdown(180) })),
);
expect(events.map((event) => event.type)).toEqual(["showdown", "hand-result"]);
expect(events[1]).toEqual(expect.objectContaining({ result: "win", netChange: 180 }));
});
it("still clacks chips when the same player wagers again later in the street", () => {
const before = snapshot(
9,
view({
currentBet: 20,
players: [player(0, { committed: 20, totalCommitted: 20, hasActed: true }), player(1)],
}),
);
const after = snapshot(
10,
view({
currentBet: 60,
players: [
player(0, { committed: 60, totalCommitted: 60, hasActed: true }),
player(1, { committed: 40, totalCommitted: 40, hasActed: true }),
],
}),
);
const events = soundEventsForTransition(before, after);
expect(events.filter((event) => event.type === "wager")).toHaveLength(2);
});
});