Vanilla JS Guide - RumenDamyanov/js-chess GitHub Wiki
Learn how to build chess applications using vanilla JavaScript with the JS Chess project.
The vanilla JavaScript implementation provides a clean, framework-free approach to building interactive chess applications. It demonstrates core web development concepts without additional abstractions.
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
# 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
Visit http://localhost:3000
to see the vanilla JavaScript chess application in action.
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>
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();
});
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;
}
}
}
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);
}
}
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;
}
}
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;
}
}
}
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();
}
}
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';
});
}
}
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
- Debounce rapid clicks
- Use requestAnimationFrame for animations
- Minimize DOM queries
- Cache API responses when appropriate
- Lazy load piece images
The vanilla JavaScript implementation supports:
- Chrome 60+
- Firefox 55+
- Safari 11+
- Edge 79+
- Review the API Integration Guide for backend communication
- Check the Chess Features Guide for advanced chess logic
- See the UI/UX Guide for design improvements
- Explore other framework implementations for comparison
The vanilla JavaScript approach provides a solid foundation for understanding web chess applications without framework complexity.