/** * MoveValidator.js - Chess move validation engine * Validates moves including check constraints */ export class MoveValidator { /** * Check if move is legal (including check validation) * @param {Board} board - Game board * @param {Piece} piece - Piece to move * @param {number} toRow - Target row * @param {number} toCol - Target column * @param {GameState} gameState - Game state * @returns {boolean} True if legal */ static isMoveLegal(board, piece, toRow, toCol, gameState) { // 1. Check if move is in piece's valid moves if (!piece.isValidMove(board, toRow, toCol)) { return false; } // 2. Simulate move to check if it leaves king in check const simulatedBoard = this.simulateMove(board, piece, toRow, toCol); // 3. Verify own king is not in check after move if (this.isKingInCheck(simulatedBoard, piece.color)) { return false; } return true; } /** * Simulate a move on a cloned board * @param {Board} board - Original board * @param {Piece} piece - Piece to move * @param {number} toRow - Target row * @param {number} toCol - Target column * @returns {Board} Board with simulated move */ static simulateMove(board, piece, toRow, toCol) { const clonedBoard = board.clone(); const fromRow = piece.position.row; const fromCol = piece.position.col; clonedBoard.movePiece(fromRow, fromCol, toRow, toCol); return clonedBoard; } /** * Check if king is in check * @param {Board} board - Game board * @param {string} color - King color ('white' or 'black') * @returns {boolean} True if in check */ static isKingInCheck(board, color) { // Find king position const kingPos = board.findKing(color); if (!kingPos) return false; // Check if any opponent piece can attack king const opponentColor = color === 'white' ? 'black' : 'white'; for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const piece = board.getPiece(row, col); if (piece && piece.color === opponentColor) { // Get piece's valid moves (without recursion into check validation) const validMoves = piece.getValidMoves(board); // Check if king position is in attack range if (validMoves.some(move => move.row === kingPos.row && move.col === kingPos.col)) { return true; } } } } return false; } /** * Check if position is checkmate * @param {Board} board - Game board * @param {string} color - Player color * @param {GameState} gameState - Game state * @returns {boolean} True if checkmate */ static isCheckmate(board, color, gameState) { // Must be in check for checkmate if (!this.isKingInCheck(board, color)) { return false; } // Check if any legal move exists return !this.hasAnyLegalMove(board, color, gameState); } /** * Check if position is stalemate * @param {Board} board - Game board * @param {string} color - Player color * @param {GameState} gameState - Game state * @returns {boolean} True if stalemate */ static isStalemate(board, color, gameState) { // Must NOT be in check for stalemate if (this.isKingInCheck(board, color)) { return false; } // No legal moves available return !this.hasAnyLegalMove(board, color, gameState); } /** * Check if player has any legal move * @param {Board} board - Game board * @param {string} color - Player color * @param {GameState} gameState - Game state * @returns {boolean} True if at least one legal move exists */ static hasAnyLegalMove(board, color, gameState) { // Check all pieces of this color for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const piece = board.getPiece(row, col); if (piece && piece.color === color) { // Get all valid moves for this piece const validMoves = piece.getValidMoves(board); // Check if any move is legal (doesn't leave king in check) for (const move of validMoves) { if (this.isMoveLegal(board, piece, move.row, move.col, gameState)) { return true; } } // Check special moves for pawns and kings if (piece.type === 'pawn' && piece.getEnPassantMoves) { const enPassantMoves = piece.getEnPassantMoves(board, gameState); for (const move of enPassantMoves) { if (this.isMoveLegal(board, piece, move.row, move.col, gameState)) { return true; } } } if (piece.type === 'king' && piece.getCastlingMoves) { const castlingMoves = piece.getCastlingMoves(board, gameState); for (const move of castlingMoves) { if (this.canCastleToPosition(board, piece, move.col, gameState)) { return true; } } } } } } return false; } /** * Get all legal moves for a piece * @param {Board} board - Game board * @param {Piece} piece - Piece to check * @param {GameState} gameState - Game state * @returns {Position[]} Array of legal positions */ static getLegalMoves(board, piece, gameState) { const legalMoves = []; // Get valid moves (piece-specific rules) const validMoves = piece.getValidMoves(board); // Filter by check constraint for (const move of validMoves) { if (this.isMoveLegal(board, piece, move.row, move.col, gameState)) { legalMoves.push(move); } } // Add special moves if (piece.type === 'pawn' && piece.getEnPassantMoves) { const enPassantMoves = piece.getEnPassantMoves(board, gameState); for (const move of enPassantMoves) { if (this.isMoveLegal(board, piece, move.row, move.col, gameState)) { legalMoves.push(move); } } } if (piece.type === 'king' && piece.getCastlingMoves) { const castlingMoves = piece.getCastlingMoves(board, gameState); for (const move of castlingMoves) { if (this.canCastleToPosition(board, piece, move.col, gameState)) { legalMoves.push(move); } } } return legalMoves; } /** * Check if castling to position is legal * @param {Board} board - Game board * @param {King} king - King piece * @param {number} targetCol - Target column (2 or 6) * @param {GameState} gameState - Game state * @returns {boolean} True if castling is legal */ static canCastleToPosition(board, king, targetCol, gameState) { // King can't be in check if (this.isKingInCheck(board, king.color)) { return false; } const row = king.position.row; const direction = targetCol > king.position.col ? 1 : -1; // King can't pass through check for (let col = king.position.col + direction; col !== targetCol + direction; col += direction) { const simulatedBoard = board.clone(); simulatedBoard.movePiece(king.position.row, king.position.col, row, col); if (this.isKingInCheck(simulatedBoard, king.color)) { return false; } } return true; } /** * Check for insufficient material (automatic draw) * @param {Board} board - Game board * @returns {boolean} True if insufficient material */ static isInsufficientMaterial(board) { const pieces = { white: board.getPiecesByColor('white'), black: board.getPiecesByColor('black') }; // King vs King if (pieces.white.length === 1 && pieces.black.length === 1) { return true; } // King and Bishop vs King or King and Knight vs King for (const color of ['white', 'black']) { if (pieces[color].length === 2) { const nonKing = pieces[color].find(p => p.type !== 'king'); if (nonKing && (nonKing.type === 'bishop' || nonKing.type === 'knight')) { const otherColor = color === 'white' ? 'black' : 'white'; if (pieces[otherColor].length === 1) { return true; } } } } // King and Bishop vs King and Bishop (same color squares) if (pieces.white.length === 2 && pieces.black.length === 2) { const whiteBishop = pieces.white.find(p => p.type === 'bishop'); const blackBishop = pieces.black.find(p => p.type === 'bishop'); if (whiteBishop && blackBishop) { const whiteSquareColor = (whiteBishop.position.row + whiteBishop.position.col) % 2; const blackSquareColor = (blackBishop.position.row + blackBishop.position.col) % 2; if (whiteSquareColor === blackSquareColor) { return true; } } } return false; } }