import { describe, expect, it } from "vitest"; import type { PublicDealerView, PublicPlayerView } from "@/lib/minigames/blackjack"; import { actionLogText, popupEventsForTransition, type BlackjackPopupEvent, } from "@/components/frontend/minigames/blackjack/blackjackPopups"; import { isTopSeatPosition, seatPosition, } from "@/components/frontend/minigames/blackjack/seatPosition"; import type { BlackjackSoundSnapshot } from "@/components/frontend/minigames/blackjack/blackjackSoundEvents"; function hand( overrides: Partial = {}, ): PublicPlayerView["hands"][number] { return { cards: [], bet: 25, status: "active", doubled: false, fromSplit: false, splitAces: false, total: 21, outcome: null, netChange: null, ...overrides, }; } function player(seatId: number, overrides: Partial = {}): 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 dealerView(overrides: Partial = {}): PublicDealerView { return { cards: [], holeHidden: true, total: null, hasBlackjack: null, ...overrides, }; } function view( overrides: Partial = {}, ): BlackjackSoundSnapshot["you"] { return { roundNumber: 1, phase: "betting", players: [player(0), player(1)], dealer: dealerView(), roundResult: null, viewerSeat: 0, yourTurn: false, yourHands: [], ...overrides, }; } function snapshot( version: number, nextView: BlackjackSoundSnapshot["you"] = view(), ): BlackjackSoundSnapshot { return { version, you: nextView }; } function bySeat(events: readonly BlackjackPopupEvent[], seatId: number): BlackjackPopupEvent | undefined { return events.find((event) => event.seatId === seatId); } describe("blackjack popup transition events", () => { it("labels a placed bet with its amount over the acting seat", () => { const events = popupEventsForTransition( snapshot(1, view()), snapshot( 2, view({ players: [player(0, { hasBet: true, hands: [hand({ bet: 25 })] }), player(1)] }), ), ); const bettor = bySeat(events, 0); expect(bettor?.tone).toBe("wager"); expect(bettor?.text).toBe("Bet +25"); }); it("labels insurance, splits, and doubles over the acting seat", () => { const events = popupEventsForTransition( snapshot( 1, view({ players: [ player(0, { hasBet: true, hands: [hand()] }), player(1, { hasBet: true, hands: [hand()] }), ], }), ), snapshot( 2, view({ players: [ player(0, { hasBet: true, insuranceBet: 5, hands: [hand({ doubled: true, bet: 50 })], }), player(1, { hasBet: true, hands: [hand(), hand({ fromSplit: true })] }), ], }), ), ); expect(events.some((event) => event.text === "Insurance +5")).toBe(true); expect(events.some((event) => event.text.startsWith("Double +"))).toBe(true); const splitter = events.find((event) => event.seatId === 1 && event.text === "Split"); expect(splitter?.tone).toBe("action"); }); it("labels busts and naturals as hand-status changes", () => { const events = popupEventsForTransition( snapshot( 1, view({ phase: "player", players: [player(0, { hasBet: true, hands: [hand()] }), player(1, { hasBet: true, hands: [hand()] })], }), ), snapshot( 2, view({ phase: "player", players: [ player(0, { hasBet: true, hands: [hand({ status: "bust" })] }), player(1, { hasBet: true, hands: [hand({ status: "blackjack" })] }), ], }), ), ); expect(bySeat(events, 0)?.text).toBe("Bust"); expect(bySeat(events, 0)?.tone).toBe("fold"); expect(bySeat(events, 1)?.text).toBe("Blackjack!"); expect(bySeat(events, 1)?.tone).toBe("win"); }); it("fires one aggregated win popup per seat when a round settles", () => { const settledView = view({ phase: "round-complete", roundResult: { roundNumber: 1, dealerTotal: 22, dealerBusted: true, dealerHasBlackjack: false, hands: [ { seatId: 0, handIndex: 0, total: 20, outcome: "win", payout: 50, netChange: 50, }, { seatId: 1, handIndex: 0, total: 18, outcome: "win", payout: 25, netChange: 25, }, ], insurance: [], }, }); const events = popupEventsForTransition(snapshot(1, view()), snapshot(2, settledView)); expect(bySeat(events, 0)?.text).toBe("+50"); expect(bySeat(events, 0)?.tone).toBe("win"); expect(bySeat(events, 1)?.text).toBe("+25"); }); it("announces leavers and bankrupt players with leave tone", () => { const events = popupEventsForTransition( snapshot(1, view()), snapshot( 2, view({ players: [player(0, { left: true }), player(1, { bankrupt: true })], }), ), ); expect(bySeat(events, 0)?.tone).toBe("leave"); expect(bySeat(events, 0)?.text).toBe("Player 1 left the table"); expect(bySeat(events, 1)?.text).toBe("Player 2 is out of chips"); }); it("returns no events for the same version", () => { const current = snapshot(3, view()); expect(popupEventsForTransition(current, current)).toEqual([]); }); }); describe("actionLogText", () => { it("prefixes the acting player's name", () => { const players = [player(0, { name: "Harlan" })]; expect( actionLogText({ key: "k", seatId: 0, text: "Bet +25", tone: "wager" }, players), ).toBe("Harlan: Bet +25"); expect( actionLogText({ key: "k", seatId: 0, text: "+50", tone: "win" }, players), ).toBe("Harlan wins +50"); expect( actionLogText({ key: "k", seatId: 0, text: "Gone", tone: "leave" }, players), ).toBe("Gone"); expect( actionLogText({ key: "k", seatId: 4, text: "Bet +25", tone: "wager" }, players), ).toBe("Seat 5: Bet +25"); }); }); describe("seatPosition", () => { it("spreads one through five opponents across the bottom two-thirds arc", () => { expect(seatPosition(0, 1)).toBe("blackjack-seat-arc-corner-right"); expect(seatPosition(0, 2)).toBe("blackjack-seat-arc-corner-right"); expect(seatPosition(1, 2)).toBe("blackjack-seat-arc-corner-left"); expect(seatPosition(2, 5)).toBe("blackjack-seat-arc-low-right"); expect(seatPosition(4, 5)).toBe("blackjack-seat-arc-corner-left"); }); it("classifies the dealer-adjacent corner seats for popup placement", () => { expect(isTopSeatPosition("blackjack-seat-arc-corner-right")).toBe(true); expect(isTopSeatPosition("blackjack-seat-arc-corner-left")).toBe(true); expect(isTopSeatPosition("blackjack-seat-arc-low-right")).toBe(false); expect(isTopSeatPosition("blackjack-seat-arc-mid-left")).toBe(false); }); });