JavaScript
Chess
Game Development
Programming
Web Development

Chess game in JavaScript

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Building a chess game in JavaScript is a good test of architecture because the project is half rule engine and half UI. The easiest way to make it unmaintainable is to mix board state, move validation, and DOM updates into one giant function.

Start with a Board Model, Not the DOM

The board should live in plain JavaScript data so it can be tested, cloned, and manipulated without any rendering code. A two-dimensional array is a reasonable starting point.

javascript
1const initialBoard = [
2  ["br", "bn", "bb", "bq", "bk", "bb", "bn", "br"],
3  ["bp", "bp", "bp", "bp", "bp", "bp", "bp", "bp"],
4  [null, null, null, null, null, null, null, null],
5  [null, null, null, null, null, null, null, null],
6  [null, null, null, null, null, null, null, null],
7  [null, null, null, null, null, null, null, null],
8  ["wp", "wp", "wp", "wp", "wp", "wp", "wp", "wp"],
9  ["wr", "wn", "wb", "wq", "wk", "wb", "wn", "wr"]
10];

The first character marks side and the second marks piece type. This encoding is compact and works well for an initial engine. You can move to richer objects later if needed.

Write Small Rule Helpers First

Before generating legal moves, create tiny utilities for bounds checks, piece lookup, and side comparison. Those helpers keep move generation readable.

javascript
1function isInside(row, col) {
2  return row >= 0 && row < 8 && col >= 0 && col < 8;
3}
4
5function getPiece(board, row, col) {
6  return isInside(row, col) ? board[row][col] : null;
7}
8
9function isEnemy(board, row, col, side) {
10  const piece = getPiece(board, row, col);
11  return piece !== null && piece[0] !== side;
12}

These may look trivial, but they prevent index and ownership checks from being duplicated across every piece implementation.

Generate Moves Per Piece Type

Avoid one huge isValidMove function. Give each piece its own move generator and dispatch based on the piece code. A pawn implementation shows the pattern clearly.

javascript
1function pawnMoves(board, row, col, side) {
2  const direction = side === "w" ? -1 : 1;
3  const startRow = side === "w" ? 6 : 1;
4  const moves = [];
5
6  if (getPiece(board, row + direction, col) === null) {
7    moves.push([row + direction, col]);
8
9    if (row === startRow && getPiece(board, row + 2 * direction, col) === null) {
10      moves.push([row + 2 * direction, col]);
11    }
12  }
13
14  for (const dc of [-1, 1]) {
15    if (isEnemy(board, row + direction, col + dc, side)) {
16      moves.push([row + direction, col + dc]);
17    }
18  }
19
20  return moves.filter(([r, c]) => isInside(r, c));
21}

Once this pattern works for one piece, you can add rook rays, bishop diagonals, knight jumps, king steps, and queen combination moves in the same style.

Keep Move Application Pure

A move function should update board state without touching the UI. Returning a new board rather than mutating the old one makes simulation easier.

javascript
1function applyMove(board, fromRow, fromCol, toRow, toCol) {
2  const next = board.map((row) => row.slice());
3  next[toRow][toCol] = next[fromRow][fromCol];
4  next[fromRow][fromCol] = null;
5  return next;
6}

Pure board transitions are useful for undo, move previews, and check detection because you can simulate many candidate moves without risking hidden shared state.

Add Rules in Layers

A sensible implementation order is:

  1. board state and rendering
  2. piece movement rules
  3. turn enforcement
  4. capture handling
  5. check detection
  6. castling, en passant, and promotion
  7. checkmate and stalemate

The key transition is check detection. Until the engine rejects moves that leave your own king attacked, it is only generating pseudo-legal moves, not real chess moves.

Test the Engine Without Clicking the UI

Rule bugs are easier to detect in unit tests than through manual browser interaction.

javascript
1import assert from "node:assert/strict";
2
3const moves = pawnMoves(initialBoard, 6, 4, "w");
4assert.deepEqual(moves, [[5, 4], [4, 4]]);

With this approach, you can test blocked movement, pinned pieces, castling rights, and special captures directly. The UI then becomes a rendering and input layer on top of a separately trusted engine.

Common Pitfalls

  • Mixing DOM updates and rule validation in the same function.
  • Mutating the live board while simulating hypothetical moves.
  • Stopping at pseudo-legal piece movement and forgetting king safety.
  • Implementing special rules before basic movement and turn order are stable.
  • Starting AI work before the rules engine is trustworthy.

Summary

  • Keep the board model in plain JavaScript data, not in the DOM.
  • Build small helpers before writing full move generators.
  • Give each piece its own move logic instead of one giant validator.
  • Apply moves in a pure, testable way.
  • Treat the UI as a view over state and verify the engine with tests first.

Course illustration
Course illustration

All Rights Reserved.