Snapshot diffs surface fold, check, bet, call, raise, all-in, and pot-win text over each seat, leavers announce centrally, and new hands open with a small-blind then big-blind sequence before the first real turn. Popups fire exactly once per event with reduced-motion fallbacks. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
330 lines
10 KiB
TypeScript
330 lines
10 KiB
TypeScript
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> = {}): 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("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);
|
|
});
|
|
});
|