/** * BoardRenderer.js - Chess board visual rendering * Renders board and pieces to DOM using CSS Grid */ export class BoardRenderer { constructor(boardElement, config = {}) { this.boardElement = boardElement; this.selectedSquare = null; this.highlightedMoves = []; this.config = { showCoordinates: config.showCoordinates !== false, pieceStyle: config.pieceStyle || 'symbols', highlightLastMove: config.highlightLastMove !== false, ...config }; } /** * Render complete board state * @param {Board} board - Game board * @param {GameState} gameState - Game state */ renderBoard(board, gameState) { this.boardElement.innerHTML = ''; // Create 64 squares for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const square = this.createSquare(row, col); const piece = board.getPiece(row, col); if (piece) { const pieceElement = this.createPieceElement(piece); square.appendChild(pieceElement); } this.boardElement.appendChild(square); } } // Add coordinates if enabled if (this.config.showCoordinates) { this.addCoordinates(); } // Highlight last move if enabled if (this.config.highlightLastMove && gameState) { const lastMove = gameState.getLastMove(); if (lastMove) { this.highlightLastMove(lastMove); } } } /** * Create a single square element * @param {number} row - Row index * @param {number} col - Column index * @returns {HTMLElement} Square element */ createSquare(row, col) { const square = document.createElement('div'); square.className = 'square'; square.classList.add((row + col) % 2 === 0 ? 'light' : 'dark'); square.dataset.row = row; square.dataset.col = col; return square; } /** * Create a piece element * @param {Piece} piece - Chess piece * @returns {HTMLElement} Piece element */ createPieceElement(piece) { const pieceEl = document.createElement('div'); pieceEl.className = `piece ${piece.color} ${piece.type}`; pieceEl.draggable = true; if (this.config.pieceStyle === 'symbols') { // Piece symbols are rendered via CSS ::before pseudo-elements // No need to set innerHTML - CSS handles it based on classes } else { // For image-based pieces pieceEl.style.backgroundImage = `url(assets/pieces/${piece.color}-${piece.type}.svg)`; } return pieceEl; } /** * Highlight legal moves for a piece * @param {Position[]} moves - Array of legal positions */ highlightMoves(moves) { this.clearHighlights(); moves.forEach(move => { const square = this.getSquare(move.row, move.col); if (square) { square.classList.add('legal-move'); // Different highlight for captures const piece = this.getPieceElement(square); if (piece) { square.classList.add('has-piece'); } } }); this.highlightedMoves = moves; } /** * Clear all move highlights */ clearHighlights() { this.highlightedMoves.forEach(move => { const square = this.getSquare(move.row, move.col); if (square) { square.classList.remove('legal-move', 'has-piece'); } }); this.highlightedMoves = []; } /** * Highlight last move * @param {Move} move - Last move */ highlightLastMove(move) { const fromSquare = this.getSquare(move.from.row, move.from.col); const toSquare = this.getSquare(move.to.row, move.to.col); if (fromSquare) { fromSquare.classList.add('last-move'); } if (toSquare) { toSquare.classList.add('last-move'); } } /** * Select a square * @param {number} row - Row index * @param {number} col - Column index */ selectSquare(row, col) { this.deselectSquare(); const square = this.getSquare(row, col); if (square) { square.classList.add('selected'); this.selectedSquare = { row, col }; } } /** * Deselect current square */ deselectSquare() { if (this.selectedSquare) { const square = this.getSquare(this.selectedSquare.row, this.selectedSquare.col); if (square) { square.classList.remove('selected'); } this.selectedSquare = null; } } /** * Update a single square * @param {number} row - Row index * @param {number} col - Column index * @param {Piece|null} piece - Piece or null */ updateSquare(row, col, piece) { const square = this.getSquare(row, col); if (!square) return; // Remove existing piece const existingPiece = this.getPieceElement(square); if (existingPiece) { existingPiece.remove(); } // Add new piece if provided if (piece) { const pieceElement = this.createPieceElement(piece); square.appendChild(pieceElement); } } /** * Get square element at position * @param {number} row - Row index * @param {number} col - Column index * @returns {HTMLElement|null} Square element */ getSquare(row, col) { return this.boardElement.querySelector( `.square[data-row="${row}"][data-col="${col}"]` ); } /** * Get piece element within a square * @param {HTMLElement} square - Square element * @returns {HTMLElement|null} Piece element */ getPieceElement(square) { return square.querySelector('.piece'); } /** * Add rank and file coordinates to board */ addCoordinates() { const files = 'abcdefgh'; const ranks = '87654321'; // Add file labels (a-h) at bottom for (let col = 0; col < 8; col++) { const square = this.getSquare(7, col); if (square) { const label = document.createElement('div'); label.className = 'coordinate file-label'; label.textContent = files[col]; square.appendChild(label); } } // Add rank labels (1-8) on left for (let row = 0; row < 8; row++) { const square = this.getSquare(row, 0); if (square) { const label = document.createElement('div'); label.className = 'coordinate rank-label'; label.textContent = ranks[row]; square.appendChild(label); } } } /** * Show check indicator on king * @param {string} color - King color * @param {Board} board - Game board */ showCheckIndicator(color, board) { const kingPos = board.findKing(color); if (!kingPos) return; const square = this.getSquare(kingPos.row, kingPos.col); if (square) { square.classList.add('in-check'); } } /** * Clear check indicator * @param {string} color - King color * @param {Board} board - Game board */ clearCheckIndicator(color, board) { const kingPos = board.findKing(color); if (!kingPos) return; const square = this.getSquare(kingPos.row, kingPos.col); if (square) { square.classList.remove('in-check'); } } /** * Animate piece movement * @param {number} fromRow - Source row * @param {number} fromCol - Source column * @param {number} toRow - Target row * @param {number} toCol - Target column * @param {Function} callback - Callback after animation */ animateMove(fromRow, fromCol, toRow, toCol, callback) { const fromSquare = this.getSquare(fromRow, fromCol); const toSquare = this.getSquare(toRow, toCol); if (!fromSquare || !toSquare) { if (callback) callback(); return; } const piece = this.getPieceElement(fromSquare); if (!piece) { if (callback) callback(); return; } // Calculate positions const fromRect = fromSquare.getBoundingClientRect(); const toRect = toSquare.getBoundingClientRect(); const deltaX = toRect.left - fromRect.left; const deltaY = toRect.top - fromRect.top; // Apply animation piece.style.transform = `translate(${deltaX}px, ${deltaY}px)`; piece.style.transition = 'transform 0.3s ease-out'; // Complete animation setTimeout(() => { piece.style.transform = ''; piece.style.transition = ''; if (callback) callback(); }, 300); } /** * Clear all visual highlights and selections */ clearAllHighlights() { this.clearHighlights(); this.deselectSquare(); // Remove last-move highlights this.boardElement.querySelectorAll('.last-move').forEach(square => { square.classList.remove('last-move'); }); // Remove check indicators this.boardElement.querySelectorAll('.in-check').forEach(square => { square.classList.remove('in-check'); }); } }