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 { 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"] { 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); }); });