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.
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;
}
}
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.