fix(chess): admin users and move registration
Some checks failed
Deploy to Production / test (push) Failing after 29s

- 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
This commit is contained in:
syntaxbullet
2026-04-02 15:27:56 +02:00
parent 9c4da51cfb
commit e521d3086f
6 changed files with 12 additions and 16 deletions

View File

@@ -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);
});
});

View File

@@ -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" };
if (!room.players.includes(playerId) || role === "admin") {
room.players.push(playerId);
}
if (room.players.length >= plugin.maxPlayers) {
room.state = plugin.createInitialState(room.players);

View File

@@ -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<typeof GameWsClientSchema>;
// --- Server → Client messages ---
export type GameWsServerMessage =
| { type: "ROOM_LIST_UPDATE"; rooms: RoomSummary[] }
| { type: "GAME_STATE"; roomId: string; state: unknown }

View File

@@ -82,7 +82,7 @@ function AppRoutes() {
{/* Game routes (both roles) */}
<Route path="/games" element={<GameLobby />} />
<Route path="/:gameSlug/:roomId" element={<GameRoom userId={user.discordId} />} />
<Route path="/:gameSlug/:roomId" element={<GameRoom userId={user.discordId} role={user.role} />} />
{/* Admin routes */}
{user.role === "admin" && (

View File

@@ -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;

View File

@@ -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<GameRoomState>({
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;