From 26a0e532f6986a79a02ea57bc69cfcdfbaafe82b Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 2 Apr 2026 15:36:05 +0200 Subject: [PATCH] fix(chess): prevent duplicate players and fix move detection - Prevent same player from joining as both white and black - Add validation to reject duplicate players in RoomManager - Fix spectator status not resetting when joining as player - Use ref to track latest chess state in ChessBoard for accurate move validation --- api/src/games/RoomManager.ts | 3 ++- panel/src/games/chess/ChessBoard.tsx | 8 +++++++- panel/src/lib/useGameRoom.ts | 5 ++++- shared/games/chess/plugin.ts | 7 ++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/api/src/games/RoomManager.ts b/api/src/games/RoomManager.ts index 2fcc464..6bdb37f 100644 --- a/api/src/games/RoomManager.ts +++ b/api/src/games/RoomManager.ts @@ -46,7 +46,8 @@ export class RoomManager { const plugin = gameRegistry.get(room.gameSlug)!; 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") return { ok: true, started: room.status === "waiting" }; + if (room.players.includes(playerId) && role === "admin") return { ok: false, error: "Already a player in this game" }; if (!room.players.includes(playerId) || role === "admin") { room.players.push(playerId); diff --git a/panel/src/games/chess/ChessBoard.tsx b/panel/src/games/chess/ChessBoard.tsx index d1b7b81..791a0d4 100644 --- a/panel/src/games/chess/ChessBoard.tsx +++ b/panel/src/games/chess/ChessBoard.tsx @@ -17,6 +17,12 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players } const [promotionTo, setPromotionTo] = useState(null); const containerRef = useRef(null); const [boardWidth, setBoardWidth] = useState(400); + + // Track latest state in ref to avoid stale closures + const chessRef = useRef(chess); + useEffect(() => { + chessRef.current = chess; + }, [chess]); // Responsive board sizing useEffect(() => { @@ -56,7 +62,7 @@ export function ChessBoard({ state, myPlayerId, isSpectator, onAction, players } function onDrop(sourceSquare: string, targetSquare: string): boolean { if (isSpectator || !isMyTurn) return false; - const testGame = new Chess(chess.fen); + const testGame = new Chess(chessRef.current.fen); // Check if this is a promotion move const piece = testGame.get(sourceSquare as any); diff --git a/panel/src/lib/useGameRoom.ts b/panel/src/lib/useGameRoom.ts index 66c1ea4..1275e5b 100644 --- a/panel/src/lib/useGameRoom.ts +++ b/panel/src/lib/useGameRoom.ts @@ -56,13 +56,16 @@ export function useGameRoom(roomId: string, userId: string, role?: string) { return { ...prev, spectators: [...prev.spectators.filter(s => s.discordId !== msg.player.discordId), msg.player], - isSpectator: isMe ? true : prev.isSpectator, + isSpectator: isMe || prev.isSpectator, roomStatus: prev.roomStatus === "connecting" ? "waiting" : prev.roomStatus, }; } + + const isMe = msg.player.discordId === userId; return { ...prev, players: [...prev.players.filter(p => p.discordId !== msg.player.discordId), msg.player], + isSpectator: isMe ? false : prev.isSpectator, roomStatus: prev.roomStatus === "connecting" ? "waiting" : prev.roomStatus, }; }); diff --git a/shared/games/chess/plugin.ts b/shared/games/chess/plugin.ts index df628c5..2834cdb 100644 --- a/shared/games/chess/plugin.ts +++ b/shared/games/chess/plugin.ts @@ -23,9 +23,14 @@ export const chessPlugin: GamePlugin = { createInitialState(players: string[]): ChessState { const game = new Chess(); + + if (players[0] === players[1]) { + throw new Error("Cannot create chess game with same player for both sides"); + } + return { fen: game.fen(), - players: { white: players[0], black: players[1] }, + players: { white: players[0]!, black: players[1]! }, moveHistory: [], status: "playing", winner: null,