1
0
Fork 0
polaris-task-force/tests/int/poker-popups.int.spec.ts
Z8MB1E 34dcefec54 feat(poker): felt UI popups, named table display, and dev controls
Table components, popup system, dev panel, and the actions/page wiring, with popup, table, and dev-control tests.

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

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

360 lines
11 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { PublicPlayerView, ShowdownOutcome } from "@/lib/minigames/poker";
import {
actionLogText,
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> = {}): 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(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("action log text", () => {
const players = [player(0, { name: "Dev" }), player(1, { name: "J. Fraley" })];
it("joins the actor's name for actions and wagers", () => {
const event = (text: string, tone: PokerPopupEvent["tone"]): PokerPopupEvent => ({
key: "k",
seatId: 0,
text,
tone,
});
expect(actionLogText(event("Fold", "fold"), players)).toBe("Dev: Fold");
expect(actionLogText(event("Check", "action"), players)).toBe("Dev: Check");
expect(actionLogText(event("Raise +60", "wager"), players)).toBe("Dev: Raise +60");
expect(actionLogText(event("+120", "win"), players)).toBe("Dev wins +120");
});
it("names a leaver from the event text and falls back to the seat number", () => {
const leave: PokerPopupEvent = {
key: "k",
seatId: 1,
text: "J. Fraley left the table",
tone: "leave",
};
expect(actionLogText(leave, players)).toBe("J. Fraley left the table");
const ghost: PokerPopupEvent = { key: "k", seatId: 3, text: "Fold", tone: "fold" };
expect(actionLogText(ghost, players)).toBe("Seat 4: Fold");
});
});
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);
});
});