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>
150 lines
4.8 KiB
TypeScript
150 lines
4.8 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
advanceState,
|
|
applyAction,
|
|
createTableState,
|
|
type PokerTableState,
|
|
} from "@/lib/minigames/poker";
|
|
import type { Card } from "@/lib/minigames/poker";
|
|
import {
|
|
adjustStack,
|
|
createTable,
|
|
getSnapshotFor,
|
|
joinTable,
|
|
setBoardQueue,
|
|
setHoleQueue,
|
|
startGame,
|
|
} from "@/lib/minigames/poker/tables";
|
|
|
|
function card(rank: Card["rank"], suit: Card["suit"]): Card {
|
|
return { rank, suit };
|
|
}
|
|
|
|
function withQueues(
|
|
state: PokerTableState,
|
|
queues: Partial<Pick<PokerTableState, "queuedBoard" | "queuedHoles">>,
|
|
): PokerTableState {
|
|
return { ...state, ...queues };
|
|
}
|
|
|
|
/** Apply an action and unwrap the result; throws on illegal actions. */
|
|
function act(
|
|
state: PokerTableState,
|
|
seatId: number,
|
|
action: Parameters<typeof applyAction>[2],
|
|
amount?: number,
|
|
): PokerTableState {
|
|
const result = applyAction(state, seatId, action, amount);
|
|
if (!result.success) throw new Error(result.error);
|
|
return result.state;
|
|
}
|
|
|
|
describe("dev board queue", () => {
|
|
it("deals the queued flop and turn cards in order before falling back to the deck", () => {
|
|
let state = createTableState([{ name: "A" }, { name: "B" }], {}, () => 0.5);
|
|
const queued = [card(14, "s"), card(13, "s"), card(12, "s"), card(11, "s"), card(10, "s")];
|
|
state = withQueues(state, { queuedBoard: queued });
|
|
|
|
// Preflop: the small blind calls to the big blind, the big blind checks,
|
|
// which completes the round and deals the flop from the queue.
|
|
state = act(state, 0, "call", undefined);
|
|
state = act(state, 1, "check", undefined);
|
|
|
|
expect(state.communityCards).toEqual([card(14, "s"), card(13, "s"), card(12, "s")]);
|
|
expect(state.queuedBoard).toEqual([card(11, "s"), card(10, "s")]);
|
|
|
|
// Flop: both check, dealing the turn from the queue.
|
|
state = act(state, 1, "check", undefined);
|
|
state = act(state, 0, "check", undefined);
|
|
|
|
expect(state.communityCards[3]).toEqual(card(11, "s"));
|
|
expect(state.queuedBoard).toEqual([card(10, "s")]);
|
|
});
|
|
|
|
it("consumes queued cards inside runOutAndShowdown when betting is impossible", () => {
|
|
let state = createTableState(
|
|
[
|
|
{ name: "A", stack: 50 },
|
|
{ name: "B", stack: 50 },
|
|
],
|
|
{},
|
|
() => 0.5,
|
|
);
|
|
state = withQueues(state, {
|
|
queuedBoard: [card(5, "h"), card(6, "h"), card(7, "h"), card(8, "h"), card(9, "h")],
|
|
});
|
|
|
|
// Two quick all-ins leave nobody able to act: the board runs out.
|
|
state = act(state, 0, "raise", 50);
|
|
state = act(state, 1, "call", undefined);
|
|
|
|
expect(state.communityCards).toEqual([
|
|
card(5, "h"),
|
|
card(6, "h"),
|
|
card(7, "h"),
|
|
card(8, "h"),
|
|
card(9, "h"),
|
|
]);
|
|
expect(state.queuedBoard).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("dev hole queues", () => {
|
|
it("replaces the next hand's hole cards for queued seats only", () => {
|
|
let state = createTableState([{ name: "A" }, { name: "B" }, { name: "C" }], {}, () => 0.5);
|
|
// Queue while hand 1 is live, then fold it out so hand 2 deals fresh.
|
|
state = withQueues(state, {
|
|
queuedHoles: {
|
|
0: [card(14, "s"), card(14, "h")],
|
|
1: [card(2, "c"), card(3, "c")],
|
|
},
|
|
});
|
|
state = act(state, 0, "fold", undefined);
|
|
state = act(state, 1, "fold", undefined);
|
|
const dealt = advanceState(state);
|
|
|
|
expect(dealt.players[0].holeCards).toEqual([card(14, "s"), card(14, "h")]);
|
|
expect(dealt.players[1].holeCards).toEqual([card(2, "c"), card(3, "c")]);
|
|
expect(dealt.players[2].holeCards.length).toBe(2);
|
|
expect(dealt.queuedHoles).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe("dev stack adjustment", () => {
|
|
it("adds and subtracts chips, clamped at zero", () => {
|
|
const tableId = createTable(9601, "Host");
|
|
joinTable(tableId, 9602, "Guest");
|
|
startGame(tableId, 9601);
|
|
|
|
let snapshot = getSnapshotFor(tableId, 9601);
|
|
const hostSeat = snapshot.you?.viewerSeat ?? 0;
|
|
const before = snapshot.you?.players.find((p) => p.seatId === hostSeat)?.stack ?? 0;
|
|
|
|
adjustStack(tableId, 9601, hostSeat, 250);
|
|
snapshot = getSnapshotFor(tableId, 9601);
|
|
expect(snapshot.you?.players.find((p) => p.seatId === hostSeat)?.stack).toBe(before + 250);
|
|
|
|
adjustStack(tableId, 9601, hostSeat, -(before + 250) - 500);
|
|
snapshot = getSnapshotFor(tableId, 9601);
|
|
expect(snapshot.you?.players.find((p) => p.seatId === hostSeat)?.stack).toBe(0);
|
|
});
|
|
|
|
it("rejects invalid seats and oversized queues", () => {
|
|
const tableId = createTable(9611, "Host");
|
|
joinTable(tableId, 9612, "Guest");
|
|
startGame(tableId, 9611);
|
|
|
|
expect(() =>
|
|
setBoardQueue(tableId, 9611, [
|
|
card(2, "s"),
|
|
card(3, "s"),
|
|
card(4, "s"),
|
|
card(5, "s"),
|
|
card(6, "s"),
|
|
card(7, "s"),
|
|
]),
|
|
).toThrow();
|
|
expect(() => setHoleQueue(tableId, 9611, 9, [card(2, "s"), card(3, "s")])).toThrow();
|
|
expect(() => adjustStack(tableId, 9611, 9, 100)).toThrow();
|
|
});
|
|
});
|