diff --git a/panel/src/games/chess/ChessGame.tsx b/panel/src/games/chess/ChessGame.tsx index bd9c43e..bcfa347 100644 --- a/panel/src/games/chess/ChessGame.tsx +++ b/panel/src/games/chess/ChessGame.tsx @@ -17,7 +17,13 @@ const chessPieces: PieceRenderObject = Object.fromEntries( {key} ), @@ -149,7 +155,9 @@ export function ChessGame({ state, myPlayerId, isSpectator, onAction, players }: const view = state as PlayerView | SpectatorView; const playerView = isPlayerView(state) ? state as PlayerView : null; const myColor = playerView?.myColor ?? "white"; - const boardOrientation = isSpectator ? "white" : myColor; + // Solo mode: both players are the same user — myColor flips each turn, so lock orientation to white + const isSoloMode = !isSpectator && players.length === 2 && players[0]?.discordId === players[1]?.discordId; + const boardOrientation = isSpectator || isSoloMode ? "white" : myColor; const isMyTurn = playerView ? view.turn === playerView.myColor : false; const isGameOver = view.result !== null; const moveHistoryRef = useRef(null); diff --git a/shared/games/chess/chess.plugin.test.ts b/shared/games/chess/chess.plugin.test.ts index dc4a610..be5ff26 100644 --- a/shared/games/chess/chess.plugin.test.ts +++ b/shared/games/chess/chess.plugin.test.ts @@ -570,6 +570,38 @@ describe("chessPlugin", () => { }); }); + describe("solo mode (duplicate player IDs)", () => { + it("should allow the same player to move as both colors", () => { + const spy = spyOn(Math, "random").mockReturnValue(0.1); + const state = chessPlugin.createInitialState(["alice", "alice"], { timeControl: "none" }); + spy.mockRestore(); + // Both white and black are "alice" + expect(state.players.white).toBe("alice"); + expect(state.players.black).toBe("alice"); + + // White's turn — alice should be able to move + const s1 = act(state, { type: "move", from: "e2", to: "e4" }, "alice"); + // Black's turn — alice should also be able to move + const s2 = act(s1, { type: "move", from: "e7", to: "e5" }, "alice"); + expect(s2.moveHistory).toHaveLength(2); + }); + + it("should return the current turn color in player view", () => { + const spy = spyOn(Math, "random").mockReturnValue(0.1); + const state = chessPlugin.createInitialState(["alice", "alice"], { timeControl: "none" }); + spy.mockRestore(); + + const view1 = chessPlugin.getPlayerView(state, "alice") as ChessPlayerView; + expect(view1.myColor).toBe("white"); // White's turn + expect(view1.legalMoves.length).toBeGreaterThan(0); + + const s1 = act(state, { type: "move", from: "e2", to: "e4" }, "alice"); + const view2 = chessPlugin.getPlayerView(s1, "alice") as ChessPlayerView; + expect(view2.myColor).toBe("black"); // Black's turn now + expect(view2.legalMoves.length).toBeGreaterThan(0); + }); + }); + describe("plugin metadata", () => { it("should have correct slug and name", () => { expect(chessPlugin.slug).toBe("chess"); diff --git a/shared/games/chess/chess.plugin.ts b/shared/games/chess/chess.plugin.ts index 513e175..7c10f6a 100644 --- a/shared/games/chess/chess.plugin.ts +++ b/shared/games/chess/chess.plugin.ts @@ -7,8 +7,14 @@ import type { import { TIME_CONTROLS } from "./chess.types"; function colorOfPlayer(state: ChessState, playerId: string): "white" | "black" | null { - if (state.players.white === playerId) return "white"; - if (state.players.black === playerId) return "black"; + const isWhite = state.players.white === playerId; + const isBlack = state.players.black === playerId; + if (isWhite && isBlack) { + // Solo test mode — same player controls both sides, return current turn + return currentTurn(state.fen); + } + if (isWhite) return "white"; + if (isBlack) return "black"; return null; }