/** * GameState.js - Chess game state management * Manages move history, game status, and metadata */ export class GameState { constructor() { this.moveHistory = []; this.capturedPieces = { white: [], black: [] }; this.currentMove = 0; this.status = 'active'; // 'active', 'check', 'checkmate', 'stalemate', 'draw', 'resigned' this.enPassantTarget = null; this.halfMoveClock = 0; // For 50-move rule this.fullMoveNumber = 1; this.drawOffer = null; } /** * Record a move in history * @param {Move} move - Move object */ recordMove(move) { // Truncate history if we're not at the end if (this.currentMove < this.moveHistory.length) { this.moveHistory = this.moveHistory.slice(0, this.currentMove); } this.moveHistory.push(move); this.currentMove++; // Update half-move clock if (move.piece.type === 'pawn' || move.captured) { this.halfMoveClock = 0; } else { this.halfMoveClock++; } // Update full-move number (after black's move) if (move.piece.color === 'black') { this.fullMoveNumber++; } // Track captured pieces if (move.captured) { this.capturedPieces[move.captured.color].push(move.captured); } } /** * Get the last move * @returns {Move|null} Last move or null */ getLastMove() { if (this.moveHistory.length === 0) { return null; } return this.moveHistory[this.currentMove - 1]; } /** * Undo to previous state * @returns {Move|null} Undone move or null */ undo() { if (this.currentMove === 0) { return null; } this.currentMove--; const move = this.moveHistory[this.currentMove]; // Remove captured piece from list if (move.captured) { const capturedList = this.capturedPieces[move.captured.color]; const index = capturedList.indexOf(move.captured); if (index > -1) { capturedList.splice(index, 1); } } return move; } /** * Redo to next state * @returns {Move|null} Redone move or null */ redo() { if (this.currentMove >= this.moveHistory.length) { return null; } const move = this.moveHistory[this.currentMove]; this.currentMove++; // Re-add captured piece if (move.captured) { this.capturedPieces[move.captured.color].push(move.captured); } return move; } /** * Check if 50-move rule applies * @returns {boolean} True if 50 moves without capture or pawn move */ isFiftyMoveRule() { return this.halfMoveClock >= 100; // 50 moves = 100 half-moves } /** * Check for threefold repetition * @param {string} currentFEN - Current position FEN * @returns {boolean} True if position repeated 3 times */ isThreefoldRepetition(currentFEN) { if (this.moveHistory.length < 8) { return false; // Need at least 4 moves per side } let count = 0; // Count occurrences of current position in history for (const move of this.moveHistory) { if (move.fen === currentFEN) { count++; if (count >= 3) { return true; } } } return false; } /** * Export full game state to FEN notation * @param {Board} board - Game board * @param {string} currentTurn - Current turn ('white' or 'black') * @returns {string} Complete FEN string */ toFEN(board, currentTurn) { // 1. Piece placement const piecePlacement = board.toFEN(); // 2. Active color const activeColor = currentTurn === 'white' ? 'w' : 'b'; // 3. Castling availability let castling = ''; const whiteKing = board.getPiece(7, 4); const blackKing = board.getPiece(0, 4); if (whiteKing && !whiteKing.hasMoved) { const kingsideRook = board.getPiece(7, 7); if (kingsideRook && !kingsideRook.hasMoved) { castling += 'K'; } const queensideRook = board.getPiece(7, 0); if (queensideRook && !queensideRook.hasMoved) { castling += 'Q'; } } if (blackKing && !blackKing.hasMoved) { const kingsideRook = board.getPiece(0, 7); if (kingsideRook && !kingsideRook.hasMoved) { castling += 'k'; } const queensideRook = board.getPiece(0, 0); if (queensideRook && !queensideRook.hasMoved) { castling += 'q'; } } if (castling === '') { castling = '-'; } // 4. En passant target const enPassant = this.enPassantTarget ? this.positionToAlgebraic(this.enPassantTarget.row, this.enPassantTarget.col) : '-'; // 5. Halfmove clock const halfmove = this.halfMoveClock; // 6. Fullmove number const fullmove = this.fullMoveNumber; return `${piecePlacement} ${activeColor} ${castling} ${enPassant} ${halfmove} ${fullmove}`; } /** * Export game to PGN notation * @param {Object} metadata - Game metadata * @returns {string} PGN formatted string */ toPGN(metadata = {}) { const { event = 'Casual Game', site = 'Web Browser', date = new Date().toISOString().split('T')[0].replace(/-/g, '.'), white = 'Player 1', black = 'Player 2', result = this.status === 'checkmate' ? '1-0' : '*' } = metadata; let pgn = `[Event "${event}"]\n`; pgn += `[Site "${site}"]\n`; pgn += `[Date "${date}"]\n`; pgn += `[White "${white}"]\n`; pgn += `[Black "${black}"]\n`; pgn += `[Result "${result}"]\n\n`; // Add moves let moveNumber = 1; for (let i = 0; i < this.moveHistory.length; i++) { const move = this.moveHistory[i]; if (move.piece.color === 'white') { pgn += `${moveNumber}. ${move.notation} `; } else { pgn += `${move.notation} `; moveNumber++; } // Add line break every 6 full moves for readability if (i % 12 === 11) { pgn += '\n'; } } pgn += ` ${result}`; return pgn; } /** * Convert position to algebraic notation * @param {number} row - Row index * @param {number} col - Column index * @returns {string} Algebraic notation (e.g., "e4") */ positionToAlgebraic(row, col) { const files = 'abcdefgh'; const ranks = '87654321'; return files[col] + ranks[row]; } /** * Reset game state to initial */ reset() { this.moveHistory = []; this.capturedPieces = { white: [], black: [] }; this.currentMove = 0; this.status = 'active'; this.enPassantTarget = null; this.halfMoveClock = 0; this.fullMoveNumber = 1; this.drawOffer = null; } /** * Update en passant target after a move * @param {Piece} piece - Moved piece * @param {number} fromRow - Source row * @param {number} toRow - Target row */ updateEnPassantTarget(piece, fromRow, toRow) { if (piece.type === 'pawn' && Math.abs(toRow - fromRow) === 2) { const targetRow = (fromRow + toRow) / 2; this.enPassantTarget = { row: targetRow, col: piece.position.col }; } else { this.enPassantTarget = null; } } }