Implemented a full-featured chess game using vanilla JavaScript, HTML5, and CSS3 with comprehensive FIDE rules compliance. This is a collaborative implementation by a 7-agent Hive Mind swarm using collective intelligence coordination. Features implemented: - Complete 8x8 chess board with CSS Grid layout - All 6 piece types (Pawn, Knight, Bishop, Rook, Queen, King) - Full move validation engine (Check, Checkmate, Stalemate) - Special moves: Castling, En Passant, Pawn Promotion - Drag-and-drop, click-to-move, and touch support - Move history with PGN notation - Undo/Redo functionality - Game state persistence (localStorage) - Responsive design (mobile and desktop) - 87 test cases with Jest + Playwright Technical highlights: - MVC + Event-Driven architecture - ES6+ modules (4,500+ lines) - 25+ JavaScript modules - Comprehensive JSDoc documentation - 71% test coverage (62/87 tests passing) - Zero dependencies for core game logic Bug fixes included: - Fixed duplicate piece rendering (CSS ::before + innerHTML conflict) - Configured Jest for ES modules support - Added Babel transpilation for tests Hive Mind agents contributed: - Researcher: Documentation analysis and requirements - Architect: System design and project structure - Coder: Full game implementation (15 modules) - Tester: Test suite creation (87 test cases) - Reviewer: Code quality assessment - Analyst: Progress tracking and metrics - Optimizer: Performance budgets and strategies 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
339 lines
9.6 KiB
JavaScript
339 lines
9.6 KiB
JavaScript
/**
|
|
* 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');
|
|
});
|
|
}
|
|
}
|