feat(chess): replace custom engine with chess.js and react-chessboard
Some checks failed
Deploy to Production / test (push) Failing after 39s

Swap the custom move validation and Unicode piece grid for chess.js
(full rules engine with check/checkmate/castling/en passant/promotion)
and react-chessboard (drag-and-drop SVG board). Board styled to match
the purple dark theme and auto-orients to the player's color.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
syntaxbullet
2026-04-02 14:22:27 +02:00
parent 5527981fff
commit 0dadc82f84
7 changed files with 195 additions and 212 deletions

View File

@@ -1,87 +1,18 @@
import { Chess } from "chess.js";
import type { GamePlugin, GameResult, GameOverResult } from "../types";
import type { ChessState, ChessAction, Piece, PieceColor, PieceType } from "./types";
import type { ChessState, ChessAction } from "./types";
const BACK_ROW: PieceType[] = ["rook", "knight", "bishop", "queen", "king", "bishop", "knight", "rook"];
function createStartingBoard(): (Piece | null)[][] {
const board: (Piece | null)[][] = Array.from({ length: 8 }, () => Array(8).fill(null));
for (let col = 0; col < 8; col++) {
board[0][col] = { type: BACK_ROW[col], color: "black" };
board[1][col] = { type: "pawn", color: "black" };
board[6][col] = { type: "pawn", color: "white" };
board[7][col] = { type: BACK_ROW[col], color: "white" };
}
return board;
}
function inBounds(row: number, col: number): boolean {
return row >= 0 && row < 8 && col >= 0 && col < 8;
}
function getPlayerColor(state: ChessState, playerId: string): PieceColor | null {
function getPlayerColor(state: ChessState, playerId: string): "white" | "black" | null {
if (state.players.white === playerId) return "white";
if (state.players.black === playerId) return "black";
return null;
}
function isValidMove(board: (Piece | null)[][], from: [number, number], to: [number, number], piece: Piece): boolean {
const [fromRow, fromCol] = from;
const [toRow, toCol] = to;
const target = board[toRow][toCol];
if (target && target.color === piece.color) return false;
const rowDiff = toRow - fromRow;
const colDiff = toCol - fromCol;
const absRow = Math.abs(rowDiff);
const absCol = Math.abs(colDiff);
switch (piece.type) {
case "pawn": {
const direction = piece.color === "white" ? -1 : 1;
const startRow = piece.color === "white" ? 6 : 1;
if (colDiff === 0 && rowDiff === direction && !target) return true;
if (colDiff === 0 && rowDiff === 2 * direction && fromRow === startRow && !target && !board[fromRow + direction][fromCol]) return true;
if (absCol === 1 && rowDiff === direction && target) return true;
return false;
}
case "rook":
if (fromRow !== toRow && fromCol !== toCol) return false;
return isPathClear(board, from, to);
case "knight":
return (absRow === 2 && absCol === 1) || (absRow === 1 && absCol === 2);
case "bishop":
if (absRow !== absCol) return false;
return isPathClear(board, from, to);
case "queen":
if (fromRow !== toRow && fromCol !== toCol && absRow !== absCol) return false;
return isPathClear(board, from, to);
case "king":
return absRow <= 1 && absCol <= 1;
default:
return false;
}
}
function isPathClear(board: (Piece | null)[][], from: [number, number], to: [number, number]): boolean {
const [fromRow, fromCol] = from;
const [toRow, toCol] = to;
const rowStep = Math.sign(toRow - fromRow);
const colStep = Math.sign(toCol - fromCol);
let row = fromRow + rowStep;
let col = fromCol + colStep;
while (row !== toRow || col !== toCol) {
if (board[row][col]) return false;
row += rowStep;
col += colStep;
}
return true;
}
function toAlgebraic(from: [number, number], to: [number, number], piece: Piece, captured: boolean): string {
const files = "abcdefgh";
const prefix = piece.type === "pawn" ? "" : piece.type[0].toUpperCase();
const cap = captured ? "x" : "";
const fromStr = piece.type === "pawn" && captured ? files[from[1]] : "";
return `${prefix}${fromStr}${cap}${files[to[1]]}${8 - to[0]}`;
function deriveStatus(game: Chess): ChessState["status"] {
if (game.isCheckmate()) return "checkmate";
if (game.isStalemate()) return "stalemate";
if (game.isDraw()) return "draw";
return "playing";
}
export const chessPlugin: GamePlugin<ChessState, ChessAction> = {
@@ -91,9 +22,9 @@ export const chessPlugin: GamePlugin<ChessState, ChessAction> = {
maxPlayers: 2,
createInitialState(players: string[]): ChessState {
const game = new Chess();
return {
board: createStartingBoard(),
currentTurn: "white",
fen: game.fen(),
players: { white: players[0], black: players[1] },
moveHistory: [],
status: "playing",
@@ -105,41 +36,56 @@ export const chessPlugin: GamePlugin<ChessState, ChessAction> = {
if (state.status !== "playing") {
return { ok: false, error: "Game is already over" };
}
if (action.type === "forfeit") {
const color = getPlayerColor(state, playerId);
if (!color) return { ok: false, error: "You are not a player in this game" };
const winner = color === "white" ? state.players.black : state.players.white;
return { ok: true, state: { ...state, status: "forfeit", winner } };
}
if (action.type === "move") {
const { from, to } = action;
if (!inBounds(from[0], from[1]) || !inBounds(to[0], to[1])) {
return { ok: false, error: "Coordinates out of bounds" };
}
const piece = state.board[from[0]][from[1]];
if (!piece) return { ok: false, error: "No piece at source square" };
const playerColor = getPlayerColor(state, playerId);
if (!playerColor) return { ok: false, error: "You are not a player in this game" };
if (playerColor !== state.currentTurn) return { ok: false, error: "It is not your turn" };
if (piece.color !== playerColor) return { ok: false, error: "That is not your piece" };
if (!isValidMove(state.board, from, to, piece)) return { ok: false, error: "Invalid move" };
const newBoard = state.board.map(row => [...row]);
const captured = newBoard[to[0]][to[1]];
newBoard[to[0]][to[1]] = piece;
newBoard[from[0]][from[1]] = null;
const notation = toAlgebraic(from, to, piece, captured !== null);
const nextTurn: PieceColor = state.currentTurn === "white" ? "black" : "white";
const game = new Chess(state.fen);
const turn = game.turn() === "w" ? "white" : "black";
if (playerColor !== turn) return { ok: false, error: "It is not your turn" };
let move;
try {
move = game.move({
from: action.from,
to: action.to,
promotion: action.promotion ?? "q",
});
} catch {
return { ok: false, error: "Invalid move" };
}
if (!move) return { ok: false, error: "Invalid move" };
const status = deriveStatus(game);
const winner = status === "checkmate"
? (turn === "white" ? state.players.white : state.players.black)
: null;
return {
ok: true,
state: { ...state, board: newBoard, currentTurn: nextTurn, moveHistory: [...state.moveHistory, notation] },
state: {
...state,
fen: game.fen(),
moveHistory: [...state.moveHistory, move.san],
status,
winner,
},
};
}
return { ok: false, error: "Unknown action type" };
},
getPlayerView(state: ChessState, _playerId: string): ChessState { return state; },
getPlayerView(state: ChessState): ChessState { return state; },
getSpectatorView(state: ChessState): ChessState { return state; },
isGameOver(state: ChessState): GameOverResult | null {