mafia / frontend /src /game /BackendClient.ts
Alfaxad's picture
Migrate Mafia game to ZeroGPU Gradio Space
c670567 verified
Raw
History Blame Contribute Delete
6.52 kB
export type Role = 'Mafia' | 'Detective' | 'Doctor' | 'Villager';
export type Phase =
| 'night'
| 'dawn'
| 'discussion'
| 'hot_seat'
| 'vote'
| 'resolution'
| 'game_over';
export interface PlayerView {
id: string;
name: string;
seat: number;
alive: boolean;
isHuman: boolean;
persona: string;
modelSpec: string;
architecture: string;
team: 'town' | 'mafia' | null;
role: Role | null;
claimedRole: Role | null;
claimConfidence: string;
keyQuote: string;
lastVote: string | null;
publicStatus: string;
avatar?: string | null;
}
export interface EventView {
seq: number;
type: string;
phase: Phase;
day: number;
actor: string | null;
payload: Record<string, unknown>;
}
export interface Suggestion {
id: string;
intent: string;
tone: string;
message: string;
}
export interface MafiaGameView {
gameId: string;
seed: number;
mode: string;
phase: Phase;
phaseLabel: string;
day: number;
winner: 'town' | 'mafia' | null;
aliveCount: number;
human: {
id: string;
name: string;
role: Role;
team: 'town' | 'mafia';
alive: boolean;
legalActions: string[];
privateInfo: Record<string, unknown>;
};
players: PlayerView[];
events: EventView[];
scene: {
id: string;
sceneKey: string;
title: string;
subtitle: string;
objective: string;
soundCue: string;
};
suggestions: Suggestion[];
lastReceipt: EventView | null;
public: Record<string, unknown>;
votes: Record<string, string>;
lockedVotes: string[];
hotSeatTarget: string | null;
dawnMessage: string;
pendingHumanFloor: boolean;
targetChoices: { id: string; label: string }[];
roleChoices: Role[];
metrics: Record<string, unknown>;
humanAvatar: string;
}
export interface ReadyView {
ready: boolean;
agentMode: string;
moderator: string;
playerArchitecture: string;
checks: { name: string; target?: string; ready: boolean; error?: string }[];
}
export interface NewGameOptions {
seed: number;
humanName: string;
humanRole?: string;
agentMode: string;
humanAvatar?: string;
}
export const seatTexturePrefix: Record<string, string> = {
p1: 'player',
p2: 'nora',
p3: 'kai',
p4: 'mira',
p5: 'jules',
p6: 'lena',
p7: 'owen',
};
export const roleCardKey: Record<Role, string> = {
Mafia: 'role_card_mafia',
Detective: 'role_card_detective',
Doctor: 'role_card_doctor',
Villager: 'role_card_villager',
};
declare global {
interface Window {
__MAFIA_VIEW__?: MafiaGameView;
__MAFIA_READY__?: ReadyView;
__MAFIA_PLAYER_NAME__?: string;
__MAFIA_AGENT_MODE__?: string;
__MAFIA_AVATAR_ID__?: string;
}
}
async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
const response = await fetch(path, {
...init,
headers: {
'content-type': 'application/json',
...(init?.headers ?? {}),
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || `${response.status} ${response.statusText}`);
}
return response.json() as Promise<T>;
}
export class BackendClient {
async ready(agentMode: string): Promise<ReadyView> {
return requestJson<ReadyView>('/api/ready', {
method: 'POST',
body: JSON.stringify({ agent_mode: agentMode }),
});
}
async newGame(options: NewGameOptions): Promise<MafiaGameView> {
return requestJson<MafiaGameView>('/api/game', {
method: 'POST',
body: JSON.stringify({
seed: options.seed,
human_name: options.humanName,
human_role: options.humanRole ?? 'Random',
agent_mode: options.agentMode,
human_avatar: options.humanAvatar ?? 'player',
}),
});
}
async advance(gameId: string, maxSteps = 1): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/advance`, {
method: 'POST',
body: JSON.stringify({ max_steps: maxSteps }),
});
}
async message(gameId: string, message: string): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/message`, {
method: 'POST',
body: JSON.stringify({ message, source: 'human_typed' }),
});
}
async approveSuggestion(gameId: string, suggestion: Suggestion): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/approve-suggestion`, {
method: 'POST',
body: JSON.stringify({
suggestion_id: suggestion.id,
message: suggestion.message,
}),
});
}
async claim(gameId: string, role: Role): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/claim`, {
method: 'POST',
body: JSON.stringify({ role }),
});
}
async accuse(gameId: string, target: string): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/accuse`, {
method: 'POST',
body: JSON.stringify({ target }),
});
}
async startVote(gameId: string): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/start-vote`, {
method: 'POST',
body: JSON.stringify({}),
});
}
async vote(gameId: string, target: string): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/vote`, {
method: 'POST',
body: JSON.stringify({ target }),
});
}
async nightAction(gameId: string, target: string): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/night-action`, {
method: 'POST',
body: JSON.stringify({ target }),
});
}
async passFloor(gameId: string): Promise<MafiaGameView> {
return requestJson<MafiaGameView>(`/api/game/${gameId}/pass-floor`, {
method: 'POST',
body: JSON.stringify({}),
});
}
async suggestions(gameId: string, target?: string | null): Promise<Suggestion[]> {
const data = await requestJson<{ suggestions: Suggestion[] }>(`/api/game/${gameId}/suggestions`, {
method: 'POST',
body: JSON.stringify({ target }),
});
return data.suggestions ?? [];
}
}
export function actorName(view: MafiaGameView, id: string | null | undefined): string {
if (!id) return 'Moderator';
return view.players.find((player) => player.id === id)?.name ?? id;
}
export function targetLabel(view: MafiaGameView, id: string | null | undefined): string {
if (!id) return 'Unknown';
const player = view.players.find((item) => item.id === id);
return player ? `${player.seat}. ${player.name}` : id;
}