From a5478dce2b22a780ede97a08ed1bf006c8bf9f55 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 2 Apr 2026 13:22:48 +0200 Subject: [PATCH] feat(games): add GamePlugin interface and registry Co-Authored-By: Claude Opus 4.6 (1M context) --- shared/games/registry.ts | 18 ++++++++++++++++++ shared/games/types.ts | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 shared/games/registry.ts create mode 100644 shared/games/types.ts diff --git a/shared/games/registry.ts b/shared/games/registry.ts new file mode 100644 index 0000000..9a997ce --- /dev/null +++ b/shared/games/registry.ts @@ -0,0 +1,18 @@ +import type { GamePlugin } from "./types"; + +const games = new Map(); + +export const gameRegistry = { + register(plugin: GamePlugin) { + if (games.has(plugin.slug)) { + throw new Error(`Game "${plugin.slug}" is already registered`); + } + games.set(plugin.slug, plugin); + }, + get(slug: string): GamePlugin | undefined { + return games.get(slug); + }, + list(): GamePlugin[] { + return Array.from(games.values()); + }, +}; diff --git a/shared/games/types.ts b/shared/games/types.ts new file mode 100644 index 0000000..5da77d2 --- /dev/null +++ b/shared/games/types.ts @@ -0,0 +1,23 @@ +export interface GamePlugin { + slug: string; + name: string; + minPlayers: number; + maxPlayers: number; + + createInitialState(players: string[]): TState; + handleAction(state: TState, action: TAction, playerId: string): GameResult; + getPlayerView(state: TState, playerId: string): unknown; + getSpectatorView(state: TState): unknown; + + isGameOver?(state: TState): GameOverResult | null; + onPlayerDisconnect?(state: TState, playerId: string): TState; +} + +export type GameResult = + | { ok: true; state: TState } + | { ok: false; error: string }; + +export type GameOverResult = { + winner: string | null; + reason: string; +};