Design Chess Game

advanced 4-7 YOE lld chess design-question

Chess is the ultimate LLD interview question. It tests inheritance, polymorphism, encapsulation — basically everything. Six different piece types, each with unique movement rules, all interacting on the same board. If we can design this cleanly, we can design anything.

Let’s build it step by step.

Requirements

Functional:

  • 8x8 board with standard initial piece placement
  • 6 piece types: King, Queen, Rook, Bishop, Knight, Pawn — each with unique movement
  • Two players (White and Black), turns alternate
  • Pieces can capture opponent pieces by moving to their square
  • Detect check (king under attack) and checkmate (no legal moves to escape check)
  • Game ends on checkmate, stalemate, or resignation

Constraints:

  • A move is invalid if it leaves own king in check
  • Pawns move forward but capture diagonally
  • Knights jump over pieces

Key Classes & Relationships

Chess — Class Diagram
Game
- board: Board
- players: Player[2]
- currentTurn: Color
- status: GameStatus
+ makeMove(from, to) → bool
│ has-a
Board
- grid: Square[8][8]
+ getPiece(row, col)
+ movePiece(from, to)
+ isPathClear(from, to)
Player
- name: string
- color: Color
Board contains Squares, each may hold ↓
Piece (abstract)
- color: Color
+ canMove(board, from, to) → bool
+ getSymbol() → string
│ extended by
King
Queen
Rook
Bishop
Knight
Pawn

The core idea: every piece type overrides canMove() with its own movement rules. The Game doesn’t care what type of piece it is — it just calls piece.canMove(). That’s polymorphism doing the heavy lifting.

Core Implementation

from abc import ABC, abstractmethod
from enum import Enum

class Color(Enum):
    WHITE = "white"
    BLACK = "black"

class GameStatus(Enum):
    ACTIVE = "active"
    WHITE_WIN = "white_win"
    BLACK_WIN = "black_win"
    STALEMATE = "stalemate"

# --- Pieces ---

class Piece(ABC):
    def __init__(self, color: Color):
        self.color = color

    @abstractmethod
    def can_move(self, board, fr: int, fc: int, tr: int, tc: int) -> bool:
        pass

    @abstractmethod
    def symbol(self) -> str:
        pass

class King(Piece):
    def can_move(self, board, fr, fc, tr, tc) -> bool:
        # One square in any direction
        return max(abs(tr - fr), abs(tc - fc)) == 1

    def symbol(self): return "K"

class Queen(Piece):
    def can_move(self, board, fr, fc, tr, tc) -> bool:
        # Straight or diagonal, path must be clear
        dr, dc = tr - fr, tc - fc
        is_straight = dr == 0 or dc == 0
        is_diagonal = abs(dr) == abs(dc)
        return (is_straight or is_diagonal) and board.is_path_clear(fr, fc, tr, tc)

    def symbol(self): return "Q"

class Rook(Piece):
    def can_move(self, board, fr, fc, tr, tc) -> bool:
        # Straight lines only
        dr, dc = tr - fr, tc - fc
        return (dr == 0 or dc == 0) and board.is_path_clear(fr, fc, tr, tc)

    def symbol(self): return "R"

class Bishop(Piece):
    def can_move(self, board, fr, fc, tr, tc) -> bool:
        # Diagonals only
        return abs(tr - fr) == abs(tc - fc) and board.is_path_clear(fr, fc, tr, tc)

    def symbol(self): return "B"

class Knight(Piece):
    def can_move(self, board, fr, fc, tr, tc) -> bool:
        # L-shape: 2+1 or 1+2, can jump over pieces
        dr, dc = abs(tr - fr), abs(tc - fc)
        return (dr == 2 and dc == 1) or (dr == 1 and dc == 2)

    def symbol(self): return "N"

class Pawn(Piece):
    def can_move(self, board, fr, fc, tr, tc) -> bool:
        direction = -1 if self.color == Color.WHITE else 1
        start_row = 6 if self.color == Color.WHITE else 1
        dr, dc = tr - fr, tc - fc

        # Move forward one square (must be empty)
        if dc == 0 and dr == direction and board.get_piece(tr, tc) is None:
            return True
        # Move forward two from start (both squares must be empty)
        if dc == 0 and dr == 2 * direction and fr == start_row:
            mid_r = fr + direction
            if board.get_piece(mid_r, fc) is None and board.get_piece(tr, tc) is None:
                return True
        # Diagonal capture
        if abs(dc) == 1 and dr == direction:
            target = board.get_piece(tr, tc)
            return target is not None and target.color != self.color
        return False

    def symbol(self): return "P"

# --- Board ---

class Board:
    def __init__(self):
        self.grid: list[list[Piece | None]] = [[None] * 8 for _ in range(8)]
        self._setup()

    def _setup(self):
        # Black pieces (rows 0-1), White pieces (rows 6-7)
        order = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]
        for col, PieceClass in enumerate(order):
            self.grid[0][col] = PieceClass(Color.BLACK)
            self.grid[7][col] = PieceClass(Color.WHITE)
        for col in range(8):
            self.grid[1][col] = Pawn(Color.BLACK)
            self.grid[6][col] = Pawn(Color.WHITE)

    def get_piece(self, row: int, col: int) -> Piece | None:
        return self.grid[row][col]

    def move_piece(self, fr: int, fc: int, tr: int, tc: int):
        self.grid[tr][tc] = self.grid[fr][fc]
        self.grid[fr][fc] = None

    def is_path_clear(self, fr: int, fc: int, tr: int, tc: int) -> bool:
        dr = 0 if tr == fr else (1 if tr > fr else -1)
        dc = 0 if tc == fc else (1 if tc > fc else -1)
        r, c = fr + dr, fc + dc
        while (r, c) != (tr, tc):
            if self.grid[r][c] is not None:
                return False
            r += dr
            c += dc
        return True

    def find_king(self, color: Color) -> tuple[int, int]:
        for r in range(8):
            for c in range(8):
                p = self.grid[r][c]
                if isinstance(p, King) and p.color == color:
                    return (r, c)
        raise ValueError("King not found")

    def is_under_attack(self, row: int, col: int, by_color: Color) -> bool:
        for r in range(8):
            for c in range(8):
                p = self.grid[r][c]
                if p and p.color == by_color and p.can_move(self, r, c, row, col):
                    return True
        return False

# --- Game ---

class Player:
    def __init__(self, name: str, color: Color):
        self.name = name
        self.color = color

class Game:
    def __init__(self, white: Player, black: Player):
        self.board = Board()
        self.players = {Color.WHITE: white, Color.BLACK: black}
        self.current_turn = Color.WHITE
        self.status = GameStatus.ACTIVE

    def make_move(self, fr: int, fc: int, tr: int, tc: int) -> bool:
        piece = self.board.get_piece(fr, fc)
        if not piece or piece.color != self.current_turn:
            print("Not your piece!")
            return False

        target = self.board.get_piece(tr, tc)
        if target and target.color == self.current_turn:
            print("Can't capture your own piece!")
            return False

        if not piece.can_move(self.board, fr, fc, tr, tc):
            print("Invalid move for this piece.")
            return False

        # Make the move, then check if it leaves our king in check
        self.board.move_piece(fr, fc, tr, tc)
        opponent = Color.BLACK if self.current_turn == Color.WHITE else Color.WHITE
        kr, kc = self.board.find_king(self.current_turn)

        if self.board.is_under_attack(kr, kc, opponent):
            # Undo -- can't leave own king in check
            self.board.move_piece(tr, tc, fr, fc)
            self.board.grid[tr][tc] = target
            print("Move leaves your king in check!")
            return False

        captured = f" (captured {target.symbol()})" if target else ""
        print(f"{piece.symbol()} {fr},{fc}{tr},{tc}{captured}")
        self.current_turn = opponent
        return True

# Usage
game = Game(Player("Alice", Color.WHITE), Player("Bob", Color.BLACK))
game.make_move(6, 4, 4, 4)  # White pawn e2 → e4
game.make_move(1, 4, 3, 4)  # Black pawn e7 → e5
game.make_move(7, 3, 3, 7)  # White queen d1 → h5
const Color = Object.freeze({ WHITE: "white", BLACK: "black" });

// --- Pieces ---
class Piece {
  constructor(color) { this.color = color; }
  canMove(board, fr, fc, tr, tc) { throw new Error("Override me"); }
}

class King extends Piece {
  canMove(board, fr, fc, tr, tc) {
    return Math.max(Math.abs(tr - fr), Math.abs(tc - fc)) === 1;
  }
  get symbol() { return "K"; }
}

class Queen extends Piece {
  canMove(board, fr, fc, tr, tc) {
    const [dr, dc] = [tr - fr, tc - fc];
    const straight = dr === 0 || dc === 0;
    const diagonal = Math.abs(dr) === Math.abs(dc);
    return (straight || diagonal) && board.isPathClear(fr, fc, tr, tc);
  }
  get symbol() { return "Q"; }
}

class Rook extends Piece {
  canMove(board, fr, fc, tr, tc) {
    const [dr, dc] = [tr - fr, tc - fc];
    return (dr === 0 || dc === 0) && board.isPathClear(fr, fc, tr, tc);
  }
  get symbol() { return "R"; }
}

class Bishop extends Piece {
  canMove(board, fr, fc, tr, tc) {
    return Math.abs(tr - fr) === Math.abs(tc - fc) && board.isPathClear(fr, fc, tr, tc);
  }
  get symbol() { return "B"; }
}

class Knight extends Piece {
  canMove(board, fr, fc, tr, tc) {
    const [dr, dc] = [Math.abs(tr - fr), Math.abs(tc - fc)];
    return (dr === 2 && dc === 1) || (dr === 1 && dc === 2);
  }
  get symbol() { return "N"; }
}

class Pawn extends Piece {
  canMove(board, fr, fc, tr, tc) {
    const dir = this.color === Color.WHITE ? -1 : 1;
    const startRow = this.color === Color.WHITE ? 6 : 1;
    const [dr, dc] = [tr - fr, tc - fc];

    if (dc === 0 && dr === dir && !board.getPiece(tr, tc)) return true;
    if (dc === 0 && dr === 2 * dir && fr === startRow
        && !board.getPiece(fr + dir, fc) && !board.getPiece(tr, tc)) return true;
    if (Math.abs(dc) === 1 && dr === dir) {
      const target = board.getPiece(tr, tc);
      return target !== null && target.color !== this.color;
    }
    return false;
  }
  get symbol() { return "P"; }
}

// --- Board ---
class Board {
  constructor() {
    this.grid = Array.from({ length: 8 }, () => Array(8).fill(null));
    this._setup();
  }

  _setup() {
    const order = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook];
    order.forEach((Cls, col) => {
      this.grid[0][col] = new Cls(Color.BLACK);
      this.grid[7][col] = new Cls(Color.WHITE);
    });
    for (let c = 0; c < 8; c++) {
      this.grid[1][c] = new Pawn(Color.BLACK);
      this.grid[6][c] = new Pawn(Color.WHITE);
    }
  }

  getPiece(r, c) { return this.grid[r][c]; }

  movePiece(fr, fc, tr, tc) {
    this.grid[tr][tc] = this.grid[fr][fc];
    this.grid[fr][fc] = null;
  }

  isPathClear(fr, fc, tr, tc) {
    const dr = tr === fr ? 0 : (tr > fr ? 1 : -1);
    const dc = tc === fc ? 0 : (tc > fc ? 1 : -1);
    let [r, c] = [fr + dr, fc + dc];
    while (r !== tr || c !== tc) {
      if (this.grid[r][c]) return false;
      r += dr; c += dc;
    }
    return true;
  }

  findKing(color) {
    for (let r = 0; r < 8; r++)
      for (let c = 0; c < 8; c++) {
        const p = this.grid[r][c];
        if (p instanceof King && p.color === color) return [r, c];
      }
  }

  isUnderAttack(row, col, byColor) {
    for (let r = 0; r < 8; r++)
      for (let c = 0; c < 8; c++) {
        const p = this.grid[r][c];
        if (p && p.color === byColor && p.canMove(this, r, c, row, col))
          return true;
      }
    return false;
  }
}

// --- Game ---
class Game {
  constructor(whiteName, blackName) {
    this.board = new Board();
    this.currentTurn = Color.WHITE;
  }

  makeMove(fr, fc, tr, tc) {
    const piece = this.board.getPiece(fr, fc);
    if (!piece || piece.color !== this.currentTurn) return false;

    const target = this.board.getPiece(tr, tc);
    if (target && target.color === this.currentTurn) return false;
    if (!piece.canMove(this.board, fr, fc, tr, tc)) return false;

    this.board.movePiece(fr, fc, tr, tc);
    const opponent = this.currentTurn === Color.WHITE ? Color.BLACK : Color.WHITE;
    const [kr, kc] = this.board.findKing(this.currentTurn);

    if (this.board.isUnderAttack(kr, kc, opponent)) {
      this.board.movePiece(tr, tc, fr, fc);
      this.board.grid[tr][tc] = target;
      return false;
    }

    console.log(`${piece.symbol} ${fr},${fc} → ${tr},${tc}`);
    this.currentTurn = opponent;
    return true;
  }
}

const game = new Game("Alice", "Bob");
game.makeMove(6, 4, 4, 4); // e2 → e4
game.makeMove(1, 4, 3, 4); // e7 → e5
import java.util.*;

enum Color { WHITE, BLACK }

abstract class Piece {
    final Color color;
    Piece(Color color) { this.color = color; }
    abstract boolean canMove(Board board, int fr, int fc, int tr, int tc);
    abstract String symbol();
}

class King extends Piece {
    King(Color c) { super(c); }
    boolean canMove(Board b, int fr, int fc, int tr, int tc) {
        return Math.max(Math.abs(tr - fr), Math.abs(tc - fc)) == 1;
    }
    String symbol() { return "K"; }
}

class Queen extends Piece {
    Queen(Color c) { super(c); }
    boolean canMove(Board b, int fr, int fc, int tr, int tc) {
        int dr = tr - fr, dc = tc - fc;
        boolean straight = dr == 0 || dc == 0;
        boolean diagonal = Math.abs(dr) == Math.abs(dc);
        return (straight || diagonal) && b.isPathClear(fr, fc, tr, tc);
    }
    String symbol() { return "Q"; }
}

class Rook extends Piece {
    Rook(Color c) { super(c); }
    boolean canMove(Board b, int fr, int fc, int tr, int tc) {
        return (tr - fr == 0 || tc - fc == 0) && b.isPathClear(fr, fc, tr, tc);
    }
    String symbol() { return "R"; }
}

class Bishop extends Piece {
    Bishop(Color c) { super(c); }
    boolean canMove(Board b, int fr, int fc, int tr, int tc) {
        return Math.abs(tr - fr) == Math.abs(tc - fc) && b.isPathClear(fr, fc, tr, tc);
    }
    String symbol() { return "B"; }
}

class Knight extends Piece {
    Knight(Color c) { super(c); }
    boolean canMove(Board b, int fr, int fc, int tr, int tc) {
        int dr = Math.abs(tr - fr), dc = Math.abs(tc - fc);
        return (dr == 2 && dc == 1) || (dr == 1 && dc == 2);
    }
    String symbol() { return "N"; }
}

class Pawn extends Piece {
    Pawn(Color c) { super(c); }
    boolean canMove(Board b, int fr, int fc, int tr, int tc) {
        int dir = color == Color.WHITE ? -1 : 1;
        int startRow = color == Color.WHITE ? 6 : 1;
        int dr = tr - fr, dc = tc - fc;

        if (dc == 0 && dr == dir && b.getPiece(tr, tc) == null) return true;
        if (dc == 0 && dr == 2 * dir && fr == startRow
            && b.getPiece(fr + dir, fc) == null && b.getPiece(tr, tc) == null) return true;
        if (Math.abs(dc) == 1 && dr == dir) {
            Piece target = b.getPiece(tr, tc);
            return target != null && target.color != this.color;
        }
        return false;
    }
    String symbol() { return "P"; }
}

class Board {
    Piece[][] grid = new Piece[8][8];

    Board() { setup(); }

    private void setup() {
        Piece[] blackBack = {new Rook(Color.BLACK), new Knight(Color.BLACK),
            new Bishop(Color.BLACK), new Queen(Color.BLACK), new King(Color.BLACK),
            new Bishop(Color.BLACK), new Knight(Color.BLACK), new Rook(Color.BLACK)};
        Piece[] whiteBack = {new Rook(Color.WHITE), new Knight(Color.WHITE),
            new Bishop(Color.WHITE), new Queen(Color.WHITE), new King(Color.WHITE),
            new Bishop(Color.WHITE), new Knight(Color.WHITE), new Rook(Color.WHITE)};
        grid[0] = blackBack;
        grid[7] = whiteBack;
        for (int c = 0; c < 8; c++) {
            grid[1][c] = new Pawn(Color.BLACK);
            grid[6][c] = new Pawn(Color.WHITE);
        }
    }

    Piece getPiece(int r, int c) { return grid[r][c]; }

    void movePiece(int fr, int fc, int tr, int tc) {
        grid[tr][tc] = grid[fr][fc];
        grid[fr][fc] = null;
    }

    boolean isPathClear(int fr, int fc, int tr, int tc) {
        int dr = Integer.compare(tr, fr);
        int dc = Integer.compare(tc, fc);
        int r = fr + dr, c = fc + dc;
        while (r != tr || c != tc) {
            if (grid[r][c] != null) return false;
            r += dr; c += dc;
        }
        return true;
    }

    int[] findKing(Color color) {
        for (int r = 0; r < 8; r++)
            for (int c = 0; c < 8; c++)
                if (grid[r][c] instanceof King && grid[r][c].color == color)
                    return new int[]{r, c};
        throw new RuntimeException("King not found");
    }

    boolean isUnderAttack(int row, int col, Color byColor) {
        for (int r = 0; r < 8; r++)
            for (int c = 0; c < 8; c++) {
                Piece p = grid[r][c];
                if (p != null && p.color == byColor && p.canMove(this, r, c, row, col))
                    return true;
            }
        return false;
    }
}

Design Patterns Used

Polymorphism (the star of the show) — Every piece overrides canMove(). The Game calls piece.canMove() without knowing or caring what type of piece it is. Adding a new piece type? Just create a new class and override canMove(). Zero changes to Game or Board.

Template Method (implicit) — The move validation follows a fixed sequence: check ownership → check destination → check piece movement → check king safety. The variable step is canMove() which each piece customizes.

Encapsulation — Board encapsulates grid operations. isPathClear() is a helper that pieces like Queen, Rook, and Bishop delegate to. Knight doesn’t need it because it jumps.

SRP — Each piece only knows its own movement rules. The Board handles grid operations. The Game handles turns, validation, and win detection.

Extensions

These are the follow-up questions that separate good from great:

“Add castling” — Track whether King and Rook have moved (add a hasMoved flag to Piece). Castling is valid only if: neither piece has moved, path is clear, king isn’t in check, and king doesn’t pass through check.

“Add en passant” — Track the last move made. If a pawn just moved two squares, the adjacent opponent pawn can capture it diagonally as if it only moved one square. Needs a lastMove field on Game.

“Add pawn promotion” — When a pawn reaches the opposite end (row 0 for white, row 7 for black), the player picks a new piece type (usually Queen). Add a promotePawn(row, col, PieceType) method to Game.

“Add game history and undo” — Use the Command pattern. Each move becomes a MoveCommand with execute() and undo(). Store captured pieces so we can restore them on undo. Memento pattern for full board state snapshots.

“Detect checkmate” — For each of the current player’s pieces, try every possible move. If no move results in the king NOT being in check, it’s checkmate. Brute force but correct.

In simple language, Chess is a polymorphism showpiece. One abstract Piece class, six subclasses, each with its own canMove(). The Board and Game don’t care which piece is which — they just call the method and trust the piece to know its own rules. That’s the whole point of OOP right there.