1
0
Fork 0
polaris-task-force/tests/int/blackjack-popups.int.spec.ts
Z8MB1E 474aff734d feat(blackjack): add blackjack minigame with realtime table wiring
Engine, felt, controls, sounds, card skins, and dev popups; SSE events in the realtime hook, nav and command palette entries, stats migration, and the six blackjack test suites.

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

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-14 15:38:05 -04:00

247 lines
7.3 KiB
TypeScript

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]> = {},
): 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> = {}): 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> = {}): PublicDealerView {
return {
cards: [],
holeHidden: true,
total: null,
hasBlackjack: null,
...overrides,
};
}
function view(
overrides: Partial<BlackjackSoundSnapshot["you"]> = {},
): 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);
});
});