Vanilla JS Guide - RumenDamyanov/js-chess GitHub Wiki

Vanilla JavaScript Guide

Learn how to build chess applications using vanilla JavaScript with the JS Chess project.

Overview

The vanilla JavaScript implementation provides a clean, framework-free approach to building interactive chess applications. It demonstrates core web development concepts without additional abstractions.

Project Structure

vanilla-js/
├── index.html          # Main HTML page
├── css/
│   ├── style.css      # Component-specific styles
│   └── common.css     # Shared styles
├── js/
│   ├── app.js         # Main application logic
│   ├── chess.js       # Chess game logic
│   ├── board.js       # Board rendering
│   └── utils.js       # Utility functions
└── assets/
    └── pieces/        # Chess piece images

Getting Started

1. Setup Development Environment

# Clone the repository
git clone --recursive https://github.com/RumenDamyanov/js-chess.git
cd js-chess

# Start the vanilla JS application
make run-vanilla

# Or manually
cd vanilla-js
python3 -m http.server 3000

2. Open in Browser

Visit http://localhost:3000 to see the vanilla JavaScript chess application in action.

Core Implementation

HTML Structure

The main HTML file provides the foundation:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JS Chess - Vanilla JavaScript</title>
    <link rel="stylesheet" href="/shared/styles/common.css">
    <link rel="stylesheet" href="css/style.css">
</head>
<body>
    <header class="app-header">
        <nav class="app-nav">
            <div class="nav-brand">
                <h1>JS Chess</h1>
                <span class="framework-badge">Vanilla JS</span>
            </div>
            <!-- Navigation links -->
        </nav>
    </header>

    <main class="container">
        <div class="game-container">
            <div class="game-board" id="chessBoard">
                <!-- Chess board squares generated by JavaScript -->
            </div>
            <div class="game-sidebar">
                <div class="game-controls">
                    <button id="newGameBtn">New Game</button>
                    <button id="undoBtn">Undo Move</button>
                </div>
                <div class="game-status">
                    <div id="gameStatus">Ready to play</div>
                    <div id="currentPlayer">White to move</div>
                </div>
            </div>
        </div>
    </main>

    <script src="js/utils.js"></script>
    <script src="js/chess.js"></script>
    <script src="js/board.js"></script>
    <script src="js/app.js"></script>
</body>
</html>

Main Application Logic

The app.js file orchestrates the application:

// app.js - Main application controller
class ChessApp {
    constructor() {
        this.game = null;
        this.board = null;
        this.selectedSquare = null;
        this.validMoves = [];
        this.isPlayerTurn = true;

        this.init();
    }

    async init() {
        try {
            // Initialize board renderer
            this.board = new ChessBoard('chessBoard');

            // Initialize game logic
            this.game = new ChessGame();

            // Setup event listeners
            this.setupEventListeners();

            // Create new game
            await this.newGame();

            console.log('Chess application initialized');
        } catch (error) {
            console.error('Failed to initialize chess application:', error);
            this.showError('Failed to initialize game');
        }
    }

    setupEventListeners() {
        // New game button
        document.getElementById('newGameBtn').addEventListener('click', () => {
            this.newGame();
        });

        // Undo move button
        document.getElementById('undoBtn').addEventListener('click', () => {
            this.undoMove();
        });

        // Board click events
        document.getElementById('chessBoard').addEventListener('click', (event) => {
            this.handleBoardClick(event);
        });

        // Keyboard shortcuts
        document.addEventListener('keydown', (event) => {
            this.handleKeypress(event);
        });
    }

    async newGame() {
        try {
            // Reset game state
            this.selectedSquare = null;
            this.validMoves = [];
            this.isPlayerTurn = true;

            // Create new game via API
            const gameData = await this.game.createGame();

            // Render initial board
            this.board.render(gameData.board);

            // Update status
            this.updateGameStatus('New game started');
            this.updateCurrentPlayer('white');

        } catch (error) {
            console.error('Failed to create new game:', error);
            this.showError('Failed to create new game');
        }
    }

    handleBoardClick(event) {
        const square = event.target.closest('.square');
        if (!square) return;

        const position = square.dataset.position;

        if (this.selectedSquare === position) {
            // Deselect current square
            this.clearSelection();
            return;
        }

        if (this.selectedSquare && this.validMoves.includes(position)) {
            // Make move
            this.makeMove(this.selectedSquare, position);
        } else {
            // Select new square
            this.selectSquare(position);
        }
    }

    selectSquare(position) {
        // Clear previous selection
        this.clearSelection();

        // Set new selection
        this.selectedSquare = position;

        // Highlight selected square
        const square = document.querySelector(`[data-position="${position}"]`);
        square.classList.add('selected');

        // Get valid moves for this piece
        this.validMoves = this.game.getValidMoves(position);

        // Highlight valid moves
        this.validMoves.forEach(move => {
            const moveSquare = document.querySelector(`[data-position="${move}"]`);
            moveSquare.classList.add('valid-move');
        });
    }

    clearSelection() {
        // Remove selection highlighting
        document.querySelectorAll('.square.selected').forEach(square => {
            square.classList.remove('selected');
        });

        // Remove move highlighting
        document.querySelectorAll('.square.valid-move').forEach(square => {
            square.classList.remove('valid-move');
        });

        this.selectedSquare = null;
        this.validMoves = [];
    }

    async makeMove(from, to) {
        if (!this.isPlayerTurn) return;

        try {
            // Disable player input
            this.isPlayerTurn = false;

            // Make move via API
            const result = await this.game.makeMove(from, to);

            if (result.success) {
                // Update board
                this.board.render(result.board);

                // Clear selection
                this.clearSelection();

                // Update game status
                this.updateGameStatus(`Move: ${from}${to}`);

                // Check for game end
                if (result.gameOver) {
                    this.handleGameEnd(result);
                    return;
                }

                // Switch to AI turn
                this.updateCurrentPlayer('black');
                await this.makeAIMove();

            } else {
                this.showError(result.error || 'Invalid move');
                this.isPlayerTurn = true;
            }

        } catch (error) {
            console.error('Failed to make move:', error);
            this.showError('Failed to make move');
            this.isPlayerTurn = true;
        }
    }

    async makeAIMove() {
        try {
            // Show thinking indicator
            this.updateGameStatus('AI is thinking...');

            // Get AI move
            const aiResult = await this.game.getAIMove();

            if (aiResult.success) {
                // Update board
                this.board.render(aiResult.board);

                // Update status
                this.updateGameStatus(`AI move: ${aiResult.move.from}${aiResult.move.to}`);

                // Check for game end
                if (aiResult.gameOver) {
                    this.handleGameEnd(aiResult);
                    return;
                }

                // Switch back to player
                this.updateCurrentPlayer('white');
                this.isPlayerTurn = true;

            } else {
                this.showError('AI failed to make a move');
                this.isPlayerTurn = true;
            }

        } catch (error) {
            console.error('AI move failed:', error);
            this.showError('AI move failed');
            this.isPlayerTurn = true;
        }
    }

    handleGameEnd(result) {
        this.isPlayerTurn = false;

        if (result.checkmate) {
            const winner = result.winner === 'white' ? 'White' : 'Black';
            this.updateGameStatus(`Checkmate! ${winner} wins!`);
        } else if (result.stalemate) {
            this.updateGameStatus('Stalemate! Game is a draw.');
        } else if (result.draw) {
            this.updateGameStatus('Draw!');
        }
    }

    updateGameStatus(message) {
        document.getElementById('gameStatus').textContent = message;
    }

    updateCurrentPlayer(player) {
        const playerName = player === 'white' ? 'White' : 'Black';
        document.getElementById('currentPlayer').textContent = `${playerName} to move`;
    }

    showError(message) {
        // Simple error display
        const errorDiv = document.createElement('div');
        errorDiv.className = 'error-message';
        errorDiv.textContent = message;

        document.body.appendChild(errorDiv);

        // Remove after 3 seconds
        setTimeout(() => {
            errorDiv.remove();
        }, 3000);
    }

    handleKeypress(event) {
        switch (event.key) {
            case 'n':
                if (event.ctrlKey || event.metaKey) {
                    event.preventDefault();
                    this.newGame();
                }
                break;
            case 'z':
                if (event.ctrlKey || event.metaKey) {
                    event.preventDefault();
                    this.undoMove();
                }
                break;
            case 'Escape':
                this.clearSelection();
                break;
        }
    }

    async undoMove() {
        try {
            const result = await this.game.undoMove();
            if (result.success) {
                this.board.render(result.board);
                this.clearSelection();
                this.updateGameStatus('Move undone');
                this.isPlayerTurn = true;
            }
        } catch (error) {
            console.error('Failed to undo move:', error);
            this.showError('Failed to undo move');
        }
    }
}

// Initialize application when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
    new ChessApp();
});

Chess Game Logic

The chess.js file handles game state and API communication:

// chess.js - Chess game logic and API communication
class ChessGame {
    constructor() {
        this.gameId = null;
        this.baseURL = 'http://localhost:8080';
        this.gameState = null;
    }

    async createGame() {
        try {
            const response = await fetch(`${this.baseURL}/api/games`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    ai_enabled: true,
                    difficulty: 'medium'
                })
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const gameData = await response.json();
            this.gameId = gameData.id;
            this.gameState = gameData;

            return gameData;
        } catch (error) {
            console.error('Failed to create game:', error);
            throw error;
        }
    }

    async makeMove(from, to) {
        if (!this.gameId) {
            throw new Error('No active game');
        }

        try {
            const response = await fetch(`${this.baseURL}/api/games/${this.gameId}/moves`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({ from, to })
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();

            if (result.success) {
                this.gameState = result.gameState;
            }

            return result;
        } catch (error) {
            console.error('Failed to make move:', error);
            throw error;
        }
    }

    async getAIMove() {
        if (!this.gameId) {
            throw new Error('No active game');
        }

        try {
            const response = await fetch(`${this.baseURL}/api/games/${this.gameId}/ai-move`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                }
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();

            if (result.success) {
                this.gameState = result.gameState;
            }

            return result;
        } catch (error) {
            console.error('Failed to get AI move:', error);
            throw error;
        }
    }

    getValidMoves(position) {
        // This would typically come from the API
        // For now, return empty array or implement basic logic
        if (!this.gameState) return [];

        // Simple implementation - in real app, get from API
        return this.calculateValidMoves(position);
    }

    calculateValidMoves(position) {
        // Basic move validation
        // In a real implementation, this would be more sophisticated
        const moves = [];
        const piece = this.getPieceAt(position);

        if (!piece) return moves;

        // Add basic move logic here
        return moves;
    }

    getPieceAt(position) {
        if (!this.gameState || !this.gameState.board) return null;

        // Parse board string and find piece at position
        // This is a simplified implementation
        return null;
    }

    async undoMove() {
        if (!this.gameId) {
            throw new Error('No active game');
        }

        try {
            const response = await fetch(`${this.baseURL}/api/games/${this.gameId}/undo`, {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                }
            });

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            const result = await response.json();

            if (result.success) {
                this.gameState = result.gameState;
            }

            return result;
        } catch (error) {
            console.error('Failed to undo move:', error);
            throw error;
        }
    }
}

Board Rendering

The board.js file handles chess board visualization:

// board.js - Chess board rendering
class ChessBoard {
    constructor(containerId) {
        this.container = document.getElementById(containerId);
        this.squares = new Map();
        this.pieceSymbols = {
            'white': {
                'king': '♔', 'queen': '♕', 'rook': '♖',
                'bishop': '♗', 'knight': '♘', 'pawn': '♙'
            },
            'black': {
                'king': '♚', 'queen': '♛', 'rook': '♜',
                'bishop': '♝', 'knight': '♞', 'pawn': '♟'
            }
        };

        this.createBoard();
    }

    createBoard() {
        this.container.innerHTML = '';
        this.container.className = 'chess-board';

        for (let rank = 8; rank >= 1; rank--) {
            for (let file = 0; file < 8; file++) {
                const fileChar = String.fromCharCode(97 + file); // a-h
                const position = fileChar + rank;

                const square = document.createElement('div');
                square.className = 'square';
                square.dataset.position = position;

                // Alternate colors
                const isLight = (rank + file) % 2 === 1;
                square.classList.add(isLight ? 'light' : 'dark');

                // Add coordinates
                if (file === 0) {
                    const rankLabel = document.createElement('span');
                    rankLabel.className = 'rank-label';
                    rankLabel.textContent = rank;
                    square.appendChild(rankLabel);
                }

                if (rank === 1) {
                    const fileLabel = document.createElement('span');
                    fileLabel.className = 'file-label';
                    fileLabel.textContent = fileChar;
                    square.appendChild(fileLabel);
                }

                this.container.appendChild(square);
                this.squares.set(position, square);
            }
        }
    }

    render(boardData) {
        // Clear all pieces
        this.squares.forEach(square => {
            const piece = square.querySelector('.piece');
            if (piece) {
                piece.remove();
            }
        });

        if (!boardData) return;

        // Parse board data and place pieces
        this.parseBoardData(boardData);
    }

    parseBoardData(boardData) {
        // Parse FEN or board string format
        if (typeof boardData === 'string') {
            if (boardData.includes('/')) {
                // FEN format
                this.renderFromFEN(boardData);
            } else {
                // ASCII board format
                this.renderFromASCII(boardData);
            }
        } else if (Array.isArray(boardData)) {
            // 2D array format
            this.renderFromArray(boardData);
        }
    }

    renderFromFEN(fen) {
        const ranks = fen.split(' ')[0].split('/');

        for (let rankIndex = 0; rankIndex < 8; rankIndex++) {
            const rank = 8 - rankIndex;
            const rankData = ranks[rankIndex];
            let fileIndex = 0;

            for (const char of rankData) {
                if (char >= '1' && char <= '8') {
                    // Empty squares
                    fileIndex += parseInt(char);
                } else {
                    // Piece
                    const file = String.fromCharCode(97 + fileIndex);
                    const position = file + rank;
                    this.placePiece(position, char);
                    fileIndex++;
                }
            }
        }
    }

    renderFromASCII(asciiBoard) {
        const lines = asciiBoard.split('\n');

        for (let i = 0; i < lines.length; i++) {
            const line = lines[i];
            if (!line.includes('|')) continue;

            const rank = 8 - (i - 1); // Adjust for header
            if (rank < 1 || rank > 8) continue;

            const squares = line.split('|').slice(1, -1);

            for (let fileIndex = 0; fileIndex < squares.length && fileIndex < 8; fileIndex++) {
                const piece = squares[fileIndex].trim();
                if (piece && piece !== ' ') {
                    const file = String.fromCharCode(97 + fileIndex);
                    const position = file + rank;
                    this.placePieceFromSymbol(position, piece);
                }
            }
        }
    }

    renderFromArray(boardArray) {
        for (let rank = 0; rank < 8; rank++) {
            for (let file = 0; file < 8; file++) {
                const piece = boardArray[rank][file];
                if (piece) {
                    const fileChar = String.fromCharCode(97 + file);
                    const rankNum = 8 - rank;
                    const position = fileChar + rankNum;
                    this.placePiece(position, piece);
                }
            }
        }
    }

    placePiece(position, pieceCode) {
        const square = this.squares.get(position);
        if (!square) return;

        const piece = document.createElement('div');
        piece.className = 'piece';

        // Determine piece type and color
        const isUpperCase = pieceCode === pieceCode.toUpperCase();
        const color = isUpperCase ? 'white' : 'black';
        const pieceType = this.getPieceType(pieceCode.toLowerCase());

        piece.textContent = this.pieceSymbols[color][pieceType];
        piece.dataset.piece = pieceCode;
        piece.dataset.color = color;
        piece.dataset.type = pieceType;

        square.appendChild(piece);
    }

    placePieceFromSymbol(position, symbol) {
        const square = this.squares.get(position);
        if (!square) return;

        const piece = document.createElement('div');
        piece.className = 'piece';
        piece.textContent = symbol;

        // Determine color from Unicode symbol
        const isWhite = symbol.charCodeAt(0) >= 9812 && symbol.charCodeAt(0) <= 9817;
        piece.dataset.color = isWhite ? 'white' : 'black';

        square.appendChild(piece);
    }

    getPieceType(pieceCode) {
        const types = {
            'k': 'king', 'q': 'queen', 'r': 'rook',
            'b': 'bishop', 'n': 'knight', 'p': 'pawn'
        };
        return types[pieceCode] || 'pawn';
    }

    highlightSquare(position, className = 'highlighted') {
        const square = this.squares.get(position);
        if (square) {
            square.classList.add(className);
        }
    }

    clearHighlights() {
        this.squares.forEach(square => {
            square.classList.remove('highlighted', 'selected', 'valid-move', 'last-move');
        });
    }

    animateMove(from, to, callback) {
        const fromSquare = this.squares.get(from);
        const toSquare = this.squares.get(to);

        if (!fromSquare || !toSquare) {
            if (callback) callback();
            return;
        }

        const piece = fromSquare.querySelector('.piece');
        if (!piece) {
            if (callback) callback();
            return;
        }

        // Get positions for animation
        const fromRect = fromSquare.getBoundingClientRect();
        const toRect = toSquare.getBoundingClientRect();

        // Clone piece for animation
        const animPiece = piece.cloneNode(true);
        animPiece.className = 'piece animating';
        animPiece.style.position = 'fixed';
        animPiece.style.left = fromRect.left + 'px';
        animPiece.style.top = fromRect.top + 'px';
        animPiece.style.zIndex = '1000';

        document.body.appendChild(animPiece);

        // Hide original piece
        piece.style.opacity = '0';

        // Animate to destination
        requestAnimationFrame(() => {
            animPiece.style.transition = 'all 0.3s ease-in-out';
            animPiece.style.left = toRect.left + 'px';
            animPiece.style.top = toRect.top + 'px';
        });

        // Complete animation
        setTimeout(() => {
            animPiece.remove();
            piece.style.opacity = '1';

            // Move piece to destination
            const existingPiece = toSquare.querySelector('.piece');
            if (existingPiece) {
                existingPiece.remove();
            }

            toSquare.appendChild(piece);

            if (callback) callback();
        }, 300);
    }
}

Styling

The CSS provides a clean, responsive design:

/* style.css - Vanilla JS specific styles */
.chess-board {
    display: grid;
    grid-template-columns: repeat(8, 1fr);
    grid-template-rows: repeat(8, 1fr);
    width: 480px;
    height: 480px;
    border: 2px solid var(--border-color);
    border-radius: 8px;
    overflow: hidden;
    position: relative;
}

.square {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: background-color 0.2s ease;
}

.square.light {
    background-color: #f0d9b5;
}

.square.dark {
    background-color: #b58863;
}

.square:hover {
    background-color: rgba(255, 255, 0, 0.3);
}

.square.selected {
    background-color: rgba(255, 255, 0, 0.5);
    box-shadow: inset 0 0 0 3px #ffff00;
}

.square.valid-move {
    background-color: rgba(0, 255, 0, 0.3);
}

.square.valid-move::after {
    content: '';
    position: absolute;
    width: 20px;
    height: 20px;
    background-color: rgba(0, 255, 0, 0.6);
    border-radius: 50%;
}

.square.last-move {
    background-color: rgba(255, 255, 0, 0.4);
}

.piece {
    font-size: 36px;
    line-height: 1;
    cursor: pointer;
    user-select: none;
    transition: transform 0.1s ease;
}

.piece:hover {
    transform: scale(1.1);
}

.piece.animating {
    pointer-events: none;
}

.rank-label,
.file-label {
    position: absolute;
    font-size: 12px;
    font-weight: bold;
    color: rgba(0, 0, 0, 0.6);
}

.rank-label {
    top: 2px;
    left: 2px;
}

.file-label {
    bottom: 2px;
    right: 2px;
}

.game-container {
    display: flex;
    gap: 2rem;
    align-items: flex-start;
    justify-content: center;
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
}

.game-sidebar {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    min-width: 200px;
}

.game-controls {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
}

.game-controls button {
    padding: 0.75rem 1rem;
    border: none;
    border-radius: 4px;
    background-color: var(--primary-color);
    color: white;
    font-size: 1rem;
    cursor: pointer;
    transition: background-color 0.2s ease;
}

.game-controls button:hover {
    background-color: var(--primary-color-dark);
}

.game-controls button:disabled {
    background-color: var(--gray-400);
    cursor: not-allowed;
}

.game-status {
    padding: 1rem;
    background-color: var(--card-background);
    border-radius: 8px;
    border: 1px solid var(--border-color);
}

.error-message {
    position: fixed;
    top: 20px;
    right: 20px;
    background-color: #f44336;
    color: white;
    padding: 1rem;
    border-radius: 4px;
    z-index: 1000;
    animation: slideIn 0.3s ease-out;
}

@keyframes slideIn {
    from {
        transform: translateX(100%);
        opacity: 0;
    }
    to {
        transform: translateX(0);
        opacity: 1;
    }
}

@media (max-width: 768px) {
    .game-container {
        flex-direction: column;
        align-items: center;
    }

    .chess-board {
        width: 320px;
        height: 320px;
    }

    .piece {
        font-size: 24px;
    }
}

Advanced Features

1. Move Validation

Implement client-side move validation:

class MoveValidator {
    static isValidMove(board, from, to, piece) {
        // Basic validation logic
        const fromSquare = this.parseSquare(from);
        const toSquare = this.parseSquare(to);

        // Check bounds
        if (!this.isInBounds(fromSquare) || !this.isInBounds(toSquare)) {
            return false;
        }

        // Piece-specific validation
        switch (piece.type) {
            case 'pawn': return this.validatePawnMove(board, fromSquare, toSquare, piece.color);
            case 'rook': return this.validateRookMove(board, fromSquare, toSquare);
            case 'knight': return this.validateKnightMove(fromSquare, toSquare);
            case 'bishop': return this.validateBishopMove(board, fromSquare, toSquare);
            case 'queen': return this.validateQueenMove(board, fromSquare, toSquare);
            case 'king': return this.validateKingMove(fromSquare, toSquare);
            default: return false;
        }
    }
}

2. Game History

Track and display move history:

class GameHistory {
    constructor() {
        this.moves = [];
        this.currentIndex = -1;
    }

    addMove(move) {
        // Remove any moves after current position
        this.moves = this.moves.slice(0, this.currentIndex + 1);

        // Add new move
        this.moves.push({
            ...move,
            timestamp: new Date(),
            moveNumber: Math.floor(this.moves.length / 2) + 1
        });

        this.currentIndex = this.moves.length - 1;
    }

    undo() {
        if (this.currentIndex > 0) {
            this.currentIndex--;
            return this.moves[this.currentIndex];
        }
        return null;
    }

    redo() {
        if (this.currentIndex < this.moves.length - 1) {
            this.currentIndex++;
            return this.moves[this.currentIndex];
        }
        return null;
    }

    toPGN() {
        // Convert to PGN format
        let pgn = '';
        for (let i = 0; i < this.moves.length; i += 2) {
            const moveNumber = Math.floor(i / 2) + 1;
            const whiteMove = this.moves[i];
            const blackMove = this.moves[i + 1];

            pgn += `${moveNumber}. ${whiteMove.san}`;
            if (blackMove) {
                pgn += ` ${blackMove.san}`;
            }
            pgn += ' ';
        }
        return pgn.trim();
    }
}

3. Settings and Preferences

Add user preferences:

class GameSettings {
    constructor() {
        this.settings = this.loadSettings();
    }

    loadSettings() {
        const saved = localStorage.getItem('chessSettings');
        return saved ? JSON.parse(saved) : {
            boardTheme: 'classic',
            pieceSet: 'standard',
            showCoordinates: true,
            enableSounds: true,
            aiDifficulty: 'medium',
            autoPromoteQueen: false
        };
    }

    saveSettings() {
        localStorage.setItem('chessSettings', JSON.stringify(this.settings));
    }

    get(key) {
        return this.settings[key];
    }

    set(key, value) {
        this.settings[key] = value;
        this.saveSettings();
        this.applySettings();
    }

    applySettings() {
        // Apply visual settings
        document.body.className = `theme-${this.settings.boardTheme}`;

        // Apply coordinate display
        const coordinates = document.querySelectorAll('.rank-label, .file-label');
        coordinates.forEach(coord => {
            coord.style.display = this.settings.showCoordinates ? 'block' : 'none';
        });
    }
}

Testing

Test your vanilla JavaScript implementation:

# Run tests
make test-vanilla

# Manual testing checklist
- Board renders correctly
- Pieces can be selected and moved
- AI makes moves
- Game state updates properly
- Error handling works
- Responsive design functions

Performance Optimization

  1. Debounce rapid clicks
  2. Use requestAnimationFrame for animations
  3. Minimize DOM queries
  4. Cache API responses when appropriate
  5. Lazy load piece images

Browser Compatibility

The vanilla JavaScript implementation supports:

  • Chrome 60+
  • Firefox 55+
  • Safari 11+
  • Edge 79+

Next Steps

  1. Review the API Integration Guide for backend communication
  2. Check the Chess Features Guide for advanced chess logic
  3. See the UI/UX Guide for design improvements
  4. Explore other framework implementations for comparison

The vanilla JavaScript approach provides a solid foundation for understanding web chess applications without framework complexity.

⚠️ **GitHub.com Fallback** ⚠️