feat(games): implement chess game plugin with full UI
Some checks failed
Deploy to Production / test (push) Failing after 32s

Add chess as the first game plugin using the existing multiplayer framework.
Server-side game logic uses chess.js with server-authoritative clock management.
Client uses react-chessboard v5 with cburnett piece set, drag-and-drop + click-to-move,
configurable time controls (bullet/blitz/rapid/classical/none), draw offers,
resignation, and timeout detection. Extends the game framework with room creation
options to support per-game configuration. Includes 57 tests covering all code paths.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-04-05 16:59:26 +02:00
parent 9e95194627
commit a29bb63a1d
26 changed files with 1644 additions and 19 deletions

View File

@@ -117,7 +117,7 @@ export class GameServer {
switch (msg.type) {
case "CREATE_ROOM": {
const result = this.roomManager.createRoom(msg.gameType, discordId);
const result = this.roomManager.createRoom(msg.gameType, discordId, msg.options);
if (!result.ok) {
ws.send(JSON.stringify({ type: "ERROR", message: result.error }));
return;

View File

@@ -33,7 +33,7 @@ export class RoomManager {
private cleanupTimers = new Map<string, Timer>();
readonly emitter = mitt<RoomEvents>();
createRoom(gameSlug: string, hostId: string): CreateResult {
createRoom(gameSlug: string, hostId: string, options?: Record<string, unknown>): CreateResult {
const plugin = gameRegistry.get(gameSlug);
if (!plugin) return { ok: false, error: `Unknown game type: ${gameSlug}` };
@@ -47,6 +47,7 @@ export class RoomManager {
state: null,
status: "waiting",
createdAt: Date.now(),
options,
};
this.rooms.set(id, room);
@@ -85,7 +86,7 @@ export class RoomManager {
room.players.push(playerId);
if (room.players.length >= plugin.maxPlayers) {
room.state = plugin.createInitialState(room.players);
room.state = plugin.createInitialState(room.players, room.options);
room.status = "playing";
this.scheduleCleanup(roomId, ROOM_CONFIG.PLAYING_MAX_MS);
@@ -185,7 +186,7 @@ export class RoomManager {
room.players.push(adminId);
}
room.state = plugin.createInitialState(room.players);
room.state = plugin.createInitialState(room.players, room.options);
room.status = "playing";
this.scheduleCleanup(roomId, ROOM_CONFIG.PLAYING_MAX_MS);

View File

@@ -9,6 +9,7 @@ export interface Room {
state: unknown;
status: "waiting" | "playing" | "finished";
createdAt: number;
options?: Record<string, unknown>;
}
export interface RoomSummary {
@@ -28,7 +29,7 @@ export interface PlayerInfo {
}
export const GameWsClientSchema = z.discriminatedUnion("type", [
z.object({ type: z.literal("CREATE_ROOM"), gameType: z.string() }),
z.object({ type: z.literal("CREATE_ROOM"), gameType: z.string(), options: z.looseObject({}).optional() }),
z.object({
type: z.literal("JOIN_ROOM"),
roomId: z.string(),

View File

@@ -18,6 +18,11 @@ import type { WsConnectionData } from "./games/GameServer";
import { getSession } from "./routes/auth.routes";
import { GameWsClientSchema } from "./games/types";
// Register game plugins
import { gameRegistry } from "@shared/games/registry";
import { chessPlugin } from "@shared/games/chess/chess.plugin";
gameRegistry.register(chessPlugin);
const WS_CONFIG = {
MAX_CONNECTIONS: 200,
MAX_PAYLOAD_BYTES: 16384,