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