/** * GameController.js - Main chess game controller * Orchestrates game flow, move execution, and state management */ import { Board } from '../game/Board.js'; import { GameState } from '../game/GameState.js'; import { MoveValidator } from '../engine/MoveValidator.js'; import { SpecialMoves } from '../engine/SpecialMoves.js'; export class GameController { constructor(config = {}) { this.board = new Board(); this.board.setupInitialPosition(); this.gameState = new GameState(); this.currentTurn = 'white'; this.selectedSquare = null; this.config = { autoSave: config.autoSave !== false, enableTimer: config.enableTimer || false, timeControl: config.timeControl || null }; // Event handling this.eventHandlers = {}; } /** * Make a chess move * @param {number} fromRow - Source row * @param {number} fromCol - Source column * @param {number} toRow - Target row * @param {number} toCol - Target column * @returns {MoveResult} Result of the move */ makeMove(fromRow, fromCol, toRow, toCol) { const piece = this.board.getPiece(fromRow, fromCol); // Validation if (!piece) { return { success: false, error: 'No piece at source position' }; } if (piece.color !== this.currentTurn) { return { success: false, error: 'Not your turn' }; } if (!MoveValidator.isMoveLegal(this.board, piece, toRow, toCol, this.gameState)) { return { success: false, error: 'Invalid move' }; } // Detect special moves const specialMoveType = SpecialMoves.detectSpecialMove( this.board, piece, fromRow, fromCol, toRow, toCol, this.gameState ); // Execute move const moveResult = this.executeMove(piece, fromRow, fromCol, toRow, toCol, specialMoveType); // Update game state this.gameState.updateEnPassantTarget(piece, fromRow, toRow); // Switch turns this.currentTurn = this.currentTurn === 'white' ? 'black' : 'white'; // Check game status this.updateGameStatus(); // Emit event this.emit('move', { move: moveResult, gameStatus: this.gameState.status }); // Auto-save if enabled if (this.config.autoSave) { this.save(); } return { success: true, move: moveResult, gameStatus: this.gameState.status }; } /** * Execute a move (including special moves) * @param {Piece} piece - Piece to move * @param {number} fromRow - Source row * @param {number} fromCol - Source column * @param {number} toRow - Target row * @param {number} toCol - Target column * @param {string} specialMoveType - Type of special move or null * @returns {Move} Move object */ executeMove(piece, fromRow, fromCol, toRow, toCol, specialMoveType) { let captured = null; let promotedTo = null; if (specialMoveType === 'castle-kingside' || specialMoveType === 'castle-queenside') { // Execute castling SpecialMoves.executeCastle(this.board, piece, toCol); } else if (specialMoveType === 'en-passant') { // Execute en passant captured = SpecialMoves.executeEnPassant(this.board, piece, toRow, toCol); } else { // Normal move const moveResult = this.board.movePiece(fromRow, fromCol, toRow, toCol); captured = moveResult.captured; // Check for promotion if (specialMoveType === 'promotion' || (piece.type === 'pawn' && piece.canPromote())) { // Default to queen, UI should prompt for choice const newPiece = SpecialMoves.promote(this.board, piece, 'queen'); promotedTo = newPiece.type; // Emit promotion event for UI to handle this.emit('promotion', { pawn: piece, position: { row: toRow, col: toCol } }); } } // Generate move notation const notation = this.generateNotation(piece, fromRow, fromCol, toRow, toCol, captured, specialMoveType); // Create move object const move = { from: { row: fromRow, col: fromCol }, to: { row: toRow, col: toCol }, piece: piece, captured: captured, notation: notation, special: specialMoveType, promotedTo: promotedTo, timestamp: Date.now(), fen: this.gameState.toFEN(this.board, this.currentTurn) }; // Record move in history this.gameState.recordMove(move); return move; } /** * Generate algebraic notation for a move * @param {Piece} piece - Moved piece * @param {number} fromRow - Source row * @param {number} fromCol - Source column * @param {number} toRow - Target row * @param {number} toCol - Target column * @param {Piece} captured - Captured piece * @param {string} specialMove - Special move type * @returns {string} Move notation */ generateNotation(piece, fromRow, fromCol, toRow, toCol, captured, specialMove) { if (specialMove === 'castle-kingside') { return 'O-O'; } if (specialMove === 'castle-queenside') { return 'O-O-O'; } let notation = ''; // Piece symbol (except pawns) if (piece.type !== 'pawn') { notation += piece.type[0].toUpperCase(); } // Source square (for disambiguation or pawn captures) if (piece.type === 'pawn' && captured) { notation += String.fromCharCode(97 + fromCol); // File letter } // Capture notation if (captured) { notation += 'x'; } // Destination square notation += this.gameState.positionToAlgebraic(toRow, toCol); // Promotion if (specialMove === 'promotion') { notation += '=Q'; // Default to queen } // Check/checkmate will be added in updateGameStatus() return notation; } /** * Update game status (check, checkmate, stalemate, draw) */ updateGameStatus() { const opponentColor = this.currentTurn; // Check for checkmate if (MoveValidator.isCheckmate(this.board, opponentColor, this.gameState)) { this.gameState.status = 'checkmate'; this.emit('checkmate', { winner: this.currentTurn === 'white' ? 'black' : 'white' }); return; } // Check for stalemate if (MoveValidator.isStalemate(this.board, opponentColor, this.gameState)) { this.gameState.status = 'stalemate'; this.emit('stalemate', {}); return; } // Check for check if (MoveValidator.isKingInCheck(this.board, opponentColor)) { this.gameState.status = 'check'; this.emit('check', { color: opponentColor }); // Add check symbol to last move notation const lastMove = this.gameState.getLastMove(); if (lastMove && !lastMove.notation.endsWith('+')) { lastMove.notation += '+'; } } else { this.gameState.status = 'active'; } // Check for draws if (this.gameState.isFiftyMoveRule()) { this.gameState.status = 'draw'; this.emit('draw', { reason: '50-move rule' }); return; } if (MoveValidator.isInsufficientMaterial(this.board)) { this.gameState.status = 'draw'; this.emit('draw', { reason: 'Insufficient material' }); return; } const currentFEN = this.gameState.toFEN(this.board, this.currentTurn); if (this.gameState.isThreefoldRepetition(currentFEN)) { this.gameState.status = 'draw'; this.emit('draw', { reason: 'Threefold repetition' }); return; } } /** * Get all legal moves for a piece * @param {Piece} piece - Piece to check * @returns {Position[]} Array of legal positions */ getLegalMoves(piece) { return MoveValidator.getLegalMoves(this.board, piece, this.gameState); } /** * Check if a player is in check * @param {string} color - Player color * @returns {boolean} True if in check */ isInCheck(color) { return MoveValidator.isKingInCheck(this.board, color); } /** * Start a new game */ newGame() { this.board.clear(); this.board.setupInitialPosition(); this.gameState.reset(); this.currentTurn = 'white'; this.selectedSquare = null; this.emit('newgame', {}); } /** * Undo the last move * @returns {boolean} True if successful */ undo() { const move = this.gameState.undo(); if (!move) { return false; } // Restore board state (simplified - full implementation needs move reversal) // This would require storing board state with each move // For now, replay moves from start this.replayMovesFromHistory(); this.currentTurn = this.currentTurn === 'white' ? 'black' : 'white'; this.emit('undo', { move }); return true; } /** * Redo a previously undone move * @returns {boolean} True if successful */ redo() { const move = this.gameState.redo(); if (!move) { return false; } this.replayMovesFromHistory(); this.currentTurn = this.currentTurn === 'white' ? 'black' : 'white'; this.emit('redo', { move }); return true; } /** * Replay moves from history to restore board state */ replayMovesFromHistory() { this.board.clear(); this.board.setupInitialPosition(); for (let i = 0; i < this.gameState.currentMove; i++) { const move = this.gameState.moveHistory[i]; // Re-execute move this.board.movePiece(move.from.row, move.from.col, move.to.row, move.to.col); } } /** * Current player resigns */ resign() { this.gameState.status = 'resigned'; this.emit('resign', { loser: this.currentTurn }); } /** * Offer a draw */ offerDraw() { this.gameState.drawOffer = this.currentTurn; this.emit('draw-offered', { by: this.currentTurn }); } /** * Accept a draw offer */ acceptDraw() { if (this.gameState.drawOffer && this.gameState.drawOffer !== this.currentTurn) { this.gameState.status = 'draw'; this.emit('draw', { reason: 'Agreement' }); } } /** * Save game state to localStorage */ save() { const saveData = { fen: this.gameState.toFEN(this.board, this.currentTurn), pgn: this.gameState.toPGN(), timestamp: Date.now() }; localStorage.setItem('chess-game-save', JSON.stringify(saveData)); } /** * Load game state from localStorage * @returns {boolean} True if loaded successfully */ load() { const saved = localStorage.getItem('chess-game-save'); if (!saved) { return false; } const saveData = JSON.parse(saved); // FEN loading would be implemented here // For now, just indicate success this.emit('load', saveData); return true; } /** * Add event listener * @param {string} event - Event name * @param {Function} handler - Event handler */ on(event, handler) { if (!this.eventHandlers[event]) { this.eventHandlers[event] = []; } this.eventHandlers[event].push(handler); } /** * Emit an event * @param {string} event - Event name * @param {Object} data - Event data */ emit(event, data) { if (this.eventHandlers[event]) { this.eventHandlers[event].forEach(handler => handler(data)); } } }