import { describe, expect, it } from "vitest"; import type { PublicPlayerView, ShowdownOutcome } from "@/lib/minigames/poker"; import { popupEventsForTransition, type PokerPopupEvent, } from "@/components/frontend/minigames/poker/pokerPopups"; import { isTopSeatPosition, seatPosition, } from "@/components/frontend/minigames/poker/seatPosition"; import type { PokerSoundSnapshot } from "@/components/frontend/minigames/poker/pokerSoundEvents"; 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(results: Array<{ seatId: number; chipsWon: number }>): ShowdownOutcome { return { pots: [], results: results.map((item) => ({ seatId: item.seatId, handRank: null, bestHand: null, chipsWon: item.chipsWon, netChange: item.chipsWon, placement: 1, })), }; } function bySeat(events: readonly PokerPopupEvent[], seatId: number): PokerPopupEvent | undefined { return events.find((event) => event.seatId === seatId); } describe("poker popup transition events", () => { it("labels folds, checks, and wagers with amounts over the acting seat", () => { const events = popupEventsForTransition( snapshot(1, view({ players: [player(0), player(1)] })), snapshot( 2, view({ currentBet: 20, players: [ player(0, { committed: 20, totalCommitted: 20, hasActed: true }), player(1, { folded: true, hasActed: true }), ], }), ), ); const checks = popupEventsForTransition( snapshot(1, view({ players: [player(0), player(1)] })), snapshot(3, view({ players: [player(0), player(1, { hasActed: true })] })), ); expect(bySeat(events, 0)).toEqual( expect.objectContaining({ text: "Bet +20", tone: "wager", seatId: 0 }), ); expect(bySeat(events, 1)).toEqual( expect.objectContaining({ text: "Fold", tone: "fold", seatId: 1 }), ); expect(checks).toEqual([expect.objectContaining({ text: "Check", tone: "action", seatId: 1 })]); }); it("distinguishes calls from raises and calls out all-ins", () => { const raise = popupEventsForTransition( snapshot( 4, view({ currentBet: 20, players: [player(0, { committed: 20, totalCommitted: 20, hasActed: true }), player(1)], }), ), snapshot( 5, view({ currentBet: 60, players: [ player(0, { committed: 20, totalCommitted: 20, hasActed: true }), player(1, { committed: 60, totalCommitted: 60, hasActed: true }), ], }), ), ); const call = popupEventsForTransition( snapshot( 6, view({ currentBet: 60, players: [player(0, { committed: 20, totalCommitted: 20, hasActed: true }), player(1)], }), ), snapshot( 7, view({ currentBet: 60, players: [ player(0, { committed: 20, totalCommitted: 20, hasActed: true }), player(1, { committed: 60, totalCommitted: 60, hasActed: true }), ], }), ), ); const allIn = popupEventsForTransition( snapshot(8, view({ players: [player(0), player(1)] })), snapshot( 9, view({ currentBet: 1000, players: [ player(0), player(1, { committed: 1000, totalCommitted: 1000, hasActed: true, allIn: true }), ], }), ), ); expect(bySeat(raise, 1)?.text).toBe("Raise +60"); expect(bySeat(call, 1)?.text).toBe("Call +60"); expect(bySeat(allIn, 1)?.text).toBe("All-in +1,000"); }); it("pops winnings over every winning seat on showdown and hand-complete", () => { const multiWinner = showdown([ { seatId: 0, chipsWon: 120 }, { seatId: 1, chipsWon: 0 }, { seatId: 2, chipsWon: 340 }, ]); const showdownEvents = popupEventsForTransition( snapshot(1, view({ players: [player(0), player(1), player(2)] })), snapshot( 2, view({ phase: "showdown", players: [player(0), player(1), player(2)], showdown: multiWinner, }), ), ); const foldOutEvents = popupEventsForTransition( snapshot(3, view({ players: [player(0), player(1)] })), snapshot( 4, view({ phase: "hand-complete", players: [player(0), player(1, { folded: true })], showdown: showdown([{ seatId: 0, chipsWon: 45 }]), }), ), ); expect(showdownEvents).toEqual([ expect.objectContaining({ text: "+120", tone: "win", seatId: 0 }), expect.objectContaining({ text: "+340", tone: "win", seatId: 2 }), ]); expect(foldOutEvents).toEqual([ expect.objectContaining({ text: "+45", tone: "win", seatId: 0 }), ]); }); it("stays silent on identical versions and plays the blind sequence at a new hand", () => { const current = snapshot(7, view({ yourTurn: true })); expect(popupEventsForTransition(current, current)).toEqual([]); const blinds = popupEventsForTransition( snapshot(5), snapshot( 6, view({ handNumber: 2, currentBet: 20, players: [ player(0, { committed: 10, totalCommitted: 10 }), player(1, { committed: 20, totalCommitted: 20 }), ], }), ), ); expect(blinds).toEqual([ expect.objectContaining({ text: "Small blind +10", tone: "wager", seatId: 0, delay: 0, }), expect.objectContaining({ text: "Big blind +20", tone: "wager", seatId: 1, delay: 700, }), ]); }); it("plays the blind sequence for the game's first hand out of the waiting room", () => { const waitingRoom: PokerSoundSnapshot = { version: 1, you: null }; const events = popupEventsForTransition(waitingRoom, snapshot(2, view({ handNumber: 1 }))); expect(events).toEqual([ expect.objectContaining({ text: "Small blind +10", seatId: 0, delay: 0 }), expect.objectContaining({ text: "Big blind +20", seatId: 1, delay: 700 }), ]); }); }); describe("player leave detection", () => { it("announces players who vanish from the table between transitions", () => { const events = popupEventsForTransition( snapshot(1, view({ players: [player(0), player(1), player(2)] })), snapshot(2, view({ players: [player(0), player(1)] })), ); expect(events).toEqual([ expect.objectContaining({ text: "Player 3 left the table", tone: "leave", seatId: 2, }), ]); }); it("announces a mid-game leaver the moment the left flag flips", () => { const events = popupEventsForTransition( snapshot(3, view({ players: [player(0), player(1)] })), snapshot(4, view({ players: [player(0), player(1, { left: true })] })), ); expect(events).toEqual([ expect.objectContaining({ text: "Player 2 left the table", tone: "leave", seatId: 1, }), ]); }); it("announces the eliminated forfeit only when the player was not already marked left", () => { const forfeitOnly = popupEventsForTransition( snapshot(5, view({ players: [player(0), player(1)] })), snapshot(6, view({ players: [player(0), player(1, { eliminated: true })] })), ); const alreadyLeft = popupEventsForTransition( snapshot(7, view({ players: [player(0), player(1, { left: true })] })), snapshot(8, view({ players: [player(0), player(1, { left: true, eliminated: true })] })), ); expect(forfeitOnly).toEqual([ expect.objectContaining({ text: "Player 2 left the table", tone: "leave", seatId: 1 }), ]); expect(alreadyLeft).toEqual([]); }); it("never mistakes a busted stack for a leaver", () => { const events = popupEventsForTransition( snapshot(5, view({ players: [player(0), player(1)] })), snapshot(6, view({ players: [player(0), player(1, { stack: 0 })] })), ); expect(events).toEqual([]); }); }); describe("isTopSeatPosition", () => { it("marks top-row arc classes as top seats", () => { expect(isTopSeatPosition("poker-seat-arc-center")).toBe(true); expect(isTopSeatPosition("poker-seat-arc-center-left")).toBe(true); expect(isTopSeatPosition("poker-seat-arc-center-right")).toBe(true); }); it("marks side and corner arc classes as non-top seats", () => { expect(isTopSeatPosition("poker-seat-arc-left")).toBe(false); expect(isTopSeatPosition("poker-seat-arc-right")).toBe(false); expect(isTopSeatPosition("poker-seat-arc-far-left")).toBe(false); expect(isTopSeatPosition("poker-seat-arc-far-right")).toBe(false); }); it("matches real seat layouts: one and three opponents put a seat on top, two do not", () => { expect(isTopSeatPosition(seatPosition(0, 1))).toBe(true); expect(isTopSeatPosition(seatPosition(0, 2))).toBe(false); expect(isTopSeatPosition(seatPosition(1, 2))).toBe(false); expect(isTopSeatPosition(seatPosition(1, 3))).toBe(true); expect(isTopSeatPosition(seatPosition(0, 3))).toBe(false); expect(isTopSeatPosition(seatPosition(2, 3))).toBe(false); expect(isTopSeatPosition(seatPosition(1, 4))).toBe(true); expect(isTopSeatPosition(seatPosition(2, 4))).toBe(true); expect(isTopSeatPosition(seatPosition(0, 4))).toBe(false); expect(isTopSeatPosition(seatPosition(3, 4))).toBe(false); }); });