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>
238 lines
6.7 KiB
TypeScript
238 lines
6.7 KiB
TypeScript
"use server";
|
|
|
|
/**
|
|
* Canonical server-action surface for the poker minigame.
|
|
* The signatures below are the pinned contract shared with the UI layer.
|
|
* All table logic lives in the registry (src/lib/minigames/poker/tables.ts).
|
|
*/
|
|
|
|
import config from "@payload-config";
|
|
import { getPayload } from "payload";
|
|
import type { Card, PokerActionKind, PrivatePlayerView } from "@/lib/minigames/poker/types";
|
|
import { isSuperuser } from "@/utils/access-control/hasPermission";
|
|
import {
|
|
adjustStack,
|
|
applyPlayerAction,
|
|
createTable,
|
|
forceExpireTable,
|
|
getSnapshotFor,
|
|
joinTableWithBuyIn,
|
|
leaveTableWithBuyIn,
|
|
spectateTable,
|
|
listSummaries,
|
|
renameTable,
|
|
setBoardQueue,
|
|
setHoleQueue,
|
|
startGameWithBuyIn,
|
|
type PokerSnapshot,
|
|
type PokerTableSummary,
|
|
} from "@/lib/minigames/poker/tables";
|
|
|
|
export type { PokerSnapshot, PokerTableSummary };
|
|
export type PokerCardInput = Card;
|
|
|
|
interface ActionResult<T = undefined> {
|
|
success: boolean;
|
|
error?: string;
|
|
data?: T;
|
|
}
|
|
|
|
async function authenticate() {
|
|
const payloadConfig = await config;
|
|
const payload = await getPayload({ config: payloadConfig });
|
|
const { headers } = await import("next/headers");
|
|
const hdrs = await headers();
|
|
const { user } = await payload.auth({
|
|
headers: hdrs,
|
|
canSetHeaders: false,
|
|
});
|
|
|
|
if (!user) {
|
|
throw new Error("Unauthorized");
|
|
}
|
|
|
|
return { payload, user };
|
|
}
|
|
|
|
function toError(error: unknown): { success: false; error: string } {
|
|
const message = error instanceof Error ? error.message : "Unknown error";
|
|
if (message === "Unauthorized") {
|
|
return { success: false, error: "You must be logged in." };
|
|
}
|
|
return { success: false, error: message };
|
|
}
|
|
|
|
interface UserLike {
|
|
id: number;
|
|
displayName?: string | null;
|
|
username?: string | null;
|
|
}
|
|
|
|
function userName(user: UserLike): string {
|
|
if (typeof user.displayName === "string" && user.displayName) return user.displayName;
|
|
if (typeof user.username === "string" && user.username) return user.username;
|
|
return `Player ${user.id}`;
|
|
}
|
|
|
|
export async function createPokerTable(): Promise<ActionResult<{ tableId: string }>> {
|
|
try {
|
|
const { user } = await authenticate();
|
|
const tableId = createTable(Number(user.id), userName(user));
|
|
return { success: true, data: { tableId } };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function listPokerTables(): Promise<ActionResult<PokerTableSummary[]>> {
|
|
try {
|
|
await authenticate();
|
|
return { success: true, data: listSummaries() };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function forceExpirePokerTable(tableId: string): Promise<ActionResult<undefined>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const allowed = await isSuperuser(payload, user);
|
|
if (!allowed) {
|
|
return { success: false, error: "Only administrators can force-end tables." };
|
|
}
|
|
forceExpireTable(tableId, payload);
|
|
return { success: true };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function joinPokerTable(tableId: string): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { user } = await authenticate();
|
|
const snapshot = await joinTableWithBuyIn(tableId, Number(user.id), userName(user));
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function spectatePokerTable(tableId: string): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { user } = await authenticate();
|
|
const snapshot = spectateTable(tableId, Number(user.id));
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function setPokerBoardQueue(
|
|
tableId: string,
|
|
cards: PokerCardInput[],
|
|
): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const allowed = await isSuperuser(payload, user);
|
|
if (!allowed) {
|
|
return { success: false, error: "Only administrators can use dev controls." };
|
|
}
|
|
const snapshot = setBoardQueue(tableId, Number(user.id), cards);
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function setPokerHoleQueue(
|
|
tableId: string,
|
|
seatId: number,
|
|
cards: PokerCardInput[],
|
|
): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const allowed = await isSuperuser(payload, user);
|
|
if (!allowed) {
|
|
return { success: false, error: "Only administrators can use dev controls." };
|
|
}
|
|
const snapshot = setHoleQueue(tableId, Number(user.id), seatId, cards);
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function adjustPokerStack(
|
|
tableId: string,
|
|
seatId: number,
|
|
delta: number,
|
|
): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { payload, user } = await authenticate();
|
|
const allowed = await isSuperuser(payload, user);
|
|
if (!allowed) {
|
|
return { success: false, error: "Only administrators can use dev controls." };
|
|
}
|
|
const snapshot = adjustStack(tableId, Number(user.id), seatId, delta);
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function leavePokerTable(tableId: string): Promise<ActionResult<undefined>> {
|
|
try {
|
|
const { user } = await authenticate();
|
|
await leaveTableWithBuyIn(tableId, Number(user.id));
|
|
return { success: true };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function startPokerGame(tableId: string): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { user } = await authenticate();
|
|
const snapshot = await startGameWithBuyIn(tableId, Number(user.id));
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function renamePokerTable(
|
|
tableId: string,
|
|
rawName: string,
|
|
): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { user } = await authenticate();
|
|
const snapshot = renameTable(tableId, Number(user.id), rawName);
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function fetchPokerTable(tableId: string): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { user } = await authenticate();
|
|
const snapshot = getSnapshotFor(tableId, Number(user.id));
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|
|
|
|
export async function pokerAction(
|
|
tableId: string,
|
|
kind: PokerActionKind,
|
|
amount?: number,
|
|
): Promise<ActionResult<PokerSnapshot>> {
|
|
try {
|
|
const { user } = await authenticate();
|
|
const snapshot = applyPlayerAction(tableId, Number(user.id), kind, amount);
|
|
return { success: true, data: snapshot };
|
|
} catch (error) {
|
|
return toError(error);
|
|
}
|
|
}
|