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>
242 lines
7.5 KiB
TypeScript
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);
|
|
});
|
|
});
|