From e521d3086fbcaff3564702eaec491ddf027fcbd5 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 2 Apr 2026 15:27:56 +0200 Subject: [PATCH] fix(chess): admin users and move registration - Add role field to JOIN_ROOM message schema - Allow admin users to join rooms and be added as players - Update panel to pass user role when joining game rooms - Fix chess move coordinates in tests (algebraic notation) - Ensure admin users can make moves for both sides --- api/src/games/RoomManager.test.ts | 4 ++-- api/src/games/RoomManager.ts | 6 ++++-- api/src/games/types.ts | 8 +------- panel/src/App.tsx | 2 +- panel/src/games/GameRoom.tsx | 4 ++-- panel/src/lib/useGameRoom.ts | 4 ++-- 6 files changed, 12 insertions(+), 16 deletions(-) diff --git a/api/src/games/RoomManager.test.ts b/api/src/games/RoomManager.test.ts index e762124..057a193 100644 --- a/api/src/games/RoomManager.test.ts +++ b/api/src/games/RoomManager.test.ts @@ -85,7 +85,7 @@ describe("RoomManager", () => { const create = manager.createRoom("chess", "player1"); if (!create.ok) throw new Error("Failed to create room"); manager.joinRoom(create.roomId, "player2", "player"); - const result = manager.handleAction(create.roomId, "player1", { type: "move", from: [6, 4], to: [4, 4] }); + const result = manager.handleAction(create.roomId, "player1", { type: "move", from: "e2", to: "e4" }); expect(result.ok).toBe(true); }); @@ -94,7 +94,7 @@ describe("RoomManager", () => { if (!create.ok) throw new Error("Failed to create room"); manager.joinRoom(create.roomId, "player2", "player"); manager.joinRoom(create.roomId, "spectator1", "spectator"); - const result = manager.handleAction(create.roomId, "spectator1", { type: "move", from: [6, 4], to: [4, 4] }); + const result = manager.handleAction(create.roomId, "spectator1", { type: "move", from: "e2", to: "e4" }); expect(result.ok).toBe(false); }); }); diff --git a/api/src/games/RoomManager.ts b/api/src/games/RoomManager.ts index a332f25..2fcc464 100644 --- a/api/src/games/RoomManager.ts +++ b/api/src/games/RoomManager.ts @@ -45,10 +45,12 @@ export class RoomManager { if (room.status !== "waiting") return { ok: false, error: "Game already started" }; const plugin = gameRegistry.get(room.gameSlug)!; - if (room.players.length >= plugin.maxPlayers) return { ok: false, error: "Room is full" }; + if (room.players.length >= plugin.maxPlayers && role !== "admin") return { ok: false, error: "Room is full" }; if (room.players.includes(playerId) && role !== "admin") return { ok: true, started: room.status === "playing" }; - room.players.push(playerId); + if (!room.players.includes(playerId) || role === "admin") { + room.players.push(playerId); + } if (room.players.length >= plugin.maxPlayers) { room.state = plugin.createInitialState(room.players); diff --git a/api/src/games/types.ts b/api/src/games/types.ts index e849b4d..553e461 100644 --- a/api/src/games/types.ts +++ b/api/src/games/types.ts @@ -1,7 +1,5 @@ import { z } from "zod"; -// --- Room types --- - export interface Room { id: string; gameSlug: string; @@ -29,19 +27,15 @@ export interface PlayerInfo { username: string; } -// --- Client → Server messages --- - export const GameWsClientSchema = z.discriminatedUnion("type", [ z.object({ type: z.literal("CREATE_ROOM"), gameType: z.string() }), - z.object({ type: z.literal("JOIN_ROOM"), roomId: z.string(), as: z.enum(["player", "spectator"]) }), + z.object({ type: z.literal("JOIN_ROOM"), roomId: z.string(), as: z.enum(["player", "spectator"]), role: z.enum(["player", "admin"]).optional() }), z.object({ type: z.literal("LEAVE_ROOM"), roomId: z.string() }), z.object({ type: z.literal("GAME_ACTION"), roomId: z.string(), action: z.record(z.unknown()) }), ]); export type GameWsClientMessage = z.infer; -// --- Server → Client messages --- - export type GameWsServerMessage = | { type: "ROOM_LIST_UPDATE"; rooms: RoomSummary[] } | { type: "GAME_STATE"; roomId: string; state: unknown } diff --git a/panel/src/App.tsx b/panel/src/App.tsx index dfd2b11..fe3d476 100644 --- a/panel/src/App.tsx +++ b/panel/src/App.tsx @@ -82,7 +82,7 @@ function AppRoutes() { {/* Game routes (both roles) */} } /> - } /> + } /> {/* Admin routes */} {user.role === "admin" && ( diff --git a/panel/src/games/GameRoom.tsx b/panel/src/games/GameRoom.tsx index 683cd60..5f24493 100644 --- a/panel/src/games/GameRoom.tsx +++ b/panel/src/games/GameRoom.tsx @@ -4,13 +4,13 @@ import { gameUIRegistry } from "./registry"; import { Loader2 } from "lucide-react"; import "./chess"; -export function GameRoom({ userId }: { userId: string }) { +export function GameRoom({ userId, role }: { userId: string; role?: string }) { const { gameSlug, roomId } = useParams<{ gameSlug: string; roomId: string }>(); const navigate = useNavigate(); const { gameState, players, spectators, roomStatus, isSpectator, gameOver, error, sendAction, leaveRoom, - } = useGameRoom(roomId!, userId); + } = useGameRoom(roomId!, userId, role); const plugin = gameSlug ? gameUIRegistry.get(gameSlug) : undefined; diff --git a/panel/src/lib/useGameRoom.ts b/panel/src/lib/useGameRoom.ts index bcd044f..66c1ea4 100644 --- a/panel/src/lib/useGameRoom.ts +++ b/panel/src/lib/useGameRoom.ts @@ -16,7 +16,7 @@ interface GameRoomState { error: string | null; } -export function useGameRoom(roomId: string, userId: string) { +export function useGameRoom(roomId: string, userId: string, role?: string) { const { send, subscribe, connected } = useWebSocket(); const [state, setState] = useState({ gameState: null, @@ -31,7 +31,7 @@ export function useGameRoom(roomId: string, userId: string) { useEffect(() => { if (!connected) return; - send({ type: "JOIN_ROOM", roomId, as: "player" }); + send({ type: "JOIN_ROOM", roomId, as: "player", role: role ?? "player" }); const unsubscribe = subscribe((msg: any) => { if (msg.roomId && msg.roomId !== roomId) return;