Low-Level Design: Chess Game
Designing a chess game is a classic LLD interview question that tests your ability to model complex rules, state machines, and OOP principles. This guide walks through a complete object-oriented design.
Requirements
- Support standard chess rules: piece movement, check, checkmate, stalemate
- Two-player turn-based gameplay
- Track game history (move log)
- Detect special moves: castling, en passant, pawn promotion
- Support resign and draw offers
Core Classes
Piece Hierarchy
from abc import ABC, abstractmethod
from enum import Enum
class Color(Enum):
WHITE = "white"
BLACK = "black"
class PieceType(Enum):
KING = "king"
QUEEN = "queen"
ROOK = "rook"
BISHOP = "bishop"
KNIGHT = "knight"
PAWN = "pawn"
class Piece(ABC):
def __init__(self, color: Color, piece_type: PieceType):
self.color = color
self.piece_type = piece_type
self.has_moved = False
@abstractmethod
def get_valid_moves(self, board: 'Board', position: 'Position') -> list['Position']:
pass
def __repr__(self):
return f"{self.color.value[0].upper()}{self.piece_type.value[0].upper()}"
class King(Piece):
def __init__(self, color: Color):
super().__init__(color, PieceType.KING)
def get_valid_moves(self, board, position):
moves = []
row, col = position.row, position.col
directions = [(-1,-1),(-1,0),(-1,1),(0,-1),(0,1),(1,-1),(1,0),(1,1)]
for dr, dc in directions:
new_pos = Position(row + dr, col + dc)
if new_pos.is_valid() and not board.has_friendly_piece(new_pos, self.color):
moves.append(new_pos)
# Castling
if not self.has_moved:
moves.extend(self._get_castling_moves(board, position))
return moves
def _get_castling_moves(self, board, position):
castling = []
row = position.row
# Kingside
rook_pos = Position(row, 7)
rook = board.get_piece(rook_pos)
if (rook and rook.piece_type == PieceType.ROOK and not rook.has_moved
and board.is_path_clear(position, rook_pos)):
castling.append(Position(row, 6))
# Queenside
rook_pos = Position(row, 0)
rook = board.get_piece(rook_pos)
if (rook and rook.piece_type == PieceType.ROOK and not rook.has_moved
and board.is_path_clear(position, rook_pos)):
castling.append(Position(row, 2))
return castling
class Queen(Piece):
def __init__(self, color: Color):
super().__init__(color, PieceType.QUEEN)
def get_valid_moves(self, board, position):
# Queen = Rook + Bishop combined
rook = Rook(self.color)
bishop = Bishop(self.color)
return (rook.get_valid_moves(board, position) +
bishop.get_valid_moves(board, position))
class Rook(Piece):
def __init__(self, color: Color):
super().__init__(color, PieceType.ROOK)
def get_valid_moves(self, board, position):
return board.get_sliding_moves(position, self.color,
[(0,1),(0,-1),(1,0),(-1,0)])
class Bishop(Piece):
def __init__(self, color: Color):
super().__init__(color, PieceType.BISHOP)
def get_valid_moves(self, board, position):
return board.get_sliding_moves(position, self.color,
[(1,1),(1,-1),(-1,1),(-1,-1)])
class Knight(Piece):
def __init__(self, color: Color):
super().__init__(color, PieceType.KNIGHT)
def get_valid_moves(self, board, position):
moves = []
row, col = position.row, position.col
jumps = [(-2,-1),(-2,1),(-1,-2),(-1,2),(1,-2),(1,2),(2,-1),(2,1)]
for dr, dc in jumps:
new_pos = Position(row + dr, col + dc)
if new_pos.is_valid() and not board.has_friendly_piece(new_pos, self.color):
moves.append(new_pos)
return moves
class Pawn(Piece):
def __init__(self, color: Color):
super().__init__(color, PieceType.PAWN)
def get_valid_moves(self, board, position):
moves = []
row, col = position.row, position.col
direction = 1 if self.color == Color.WHITE else -1
start_row = 1 if self.color == Color.WHITE else 6
# Forward move
one_forward = Position(row + direction, col)
if one_forward.is_valid() and not board.get_piece(one_forward):
moves.append(one_forward)
# Double move from start
if row == start_row:
two_forward = Position(row + 2 * direction, col)
if not board.get_piece(two_forward):
moves.append(two_forward)
# Diagonal captures
for dc in [-1, 1]:
diag = Position(row + direction, col + dc)
if diag.is_valid():
if board.has_enemy_piece(diag, self.color):
moves.append(diag)
# En passant
elif board.en_passant_target == diag:
moves.append(diag)
return moves
Board and Position
class Position:
def __init__(self, row: int, col: int):
self.row = row
self.col = col
def is_valid(self):
return 0 <= self.row < 8 and 0 <= self.col < 8
def __eq__(self, other):
return self.row == other.row and self.col == other.col
def __hash__(self):
return hash((self.row, self.col))
def to_algebraic(self):
return chr(ord('a') + self.col) + str(self.row + 1)
class Board:
def __init__(self):
self.grid = [[None] * 8 for _ in range(8)]
self.en_passant_target = None
self._setup_pieces()
def _setup_pieces(self):
# White pieces
back_row = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]
for col, piece_cls in enumerate(back_row):
self.grid[0][col] = piece_cls(Color.WHITE)
self.grid[7][col] = piece_cls(Color.BLACK)
for col in range(8):
self.grid[1][col] = Pawn(Color.WHITE)
self.grid[6][col] = Pawn(Color.BLACK)
def get_piece(self, pos: Position):
return self.grid[pos.row][pos.col]
def set_piece(self, pos: Position, piece):
self.grid[pos.row][pos.col] = piece
def has_friendly_piece(self, pos: Position, color: Color):
piece = self.get_piece(pos)
return piece is not None and piece.color == color
def has_enemy_piece(self, pos: Position, color: Color):
piece = self.get_piece(pos)
return piece is not None and piece.color != color
def get_sliding_moves(self, position, color, directions):
moves = []
for dr, dc in directions:
row, col = position.row + dr, position.col + dc
while 0 <= row < 8 and 0 <= col from_pos.row else -1)
dc = 0 if to_pos.col == from_pos.col else (1 if to_pos.col > from_pos.col else -1)
r, c = from_pos.row + dr, from_pos.col + dc
while (r, c) != (to_pos.row, to_pos.col):
if self.grid[r][c]:
return False
r += dr
c += dc
return True
def find_king(self, color: Color) -> Position:
for row in range(8):
for col in range(8):
piece = self.grid[row][col]
if piece and piece.piece_type == PieceType.KING and piece.color == color:
return Position(row, col)
return None
def is_in_check(self, color: Color) -> bool:
king_pos = self.find_king(color)
enemy_color = Color.BLACK if color == Color.WHITE else Color.WHITE
for row in range(8):
for col in range(8):
piece = self.grid[row][col]
if piece and piece.color == enemy_color:
if king_pos in piece.get_valid_moves(self, Position(row, col)):
return True
return False
Game Controller
from dataclasses import dataclass
@dataclass
class Move:
from_pos: Position
to_pos: Position
piece: Piece
captured: Piece = None
promotion: PieceType = None
is_castling: bool = False
is_en_passant: bool = False
class GameStatus(Enum):
ACTIVE = "active"
WHITE_WINS = "white_wins"
BLACK_WINS = "black_wins"
DRAW = "draw"
STALEMATE = "stalemate"
class ChessGame:
def __init__(self):
self.board = Board()
self.current_turn = Color.WHITE
self.move_history: list[Move] = []
self.status = GameStatus.ACTIVE
def make_move(self, from_pos: Position, to_pos: Position,
promotion: PieceType = None) -> bool:
piece = self.board.get_piece(from_pos)
if not piece or piece.color != self.current_turn:
return False
valid_moves = self._get_legal_moves(from_pos)
if to_pos not in valid_moves:
return False
move = self._execute_move(from_pos, to_pos, promotion)
self.move_history.append(move)
self._switch_turn()
self._update_status()
return True
def _get_legal_moves(self, position: Position) -> list[Position]:
"""Filter moves that would leave own king in check."""
piece = self.board.get_piece(position)
pseudo_legal = piece.get_valid_moves(self.board, position)
legal = []
for target in pseudo_legal:
if self._is_legal_move(position, target, piece):
legal.append(target)
return legal
def _is_legal_move(self, from_pos, to_pos, piece):
# Simulate move and check if king is safe
captured = self.board.get_piece(to_pos)
self.board.set_piece(to_pos, piece)
self.board.set_piece(from_pos, None)
in_check = self.board.is_in_check(piece.color)
# Undo
self.board.set_piece(from_pos, piece)
self.board.set_piece(to_pos, captured)
return not in_check
def _execute_move(self, from_pos, to_pos, promotion):
piece = self.board.get_piece(from_pos)
captured = self.board.get_piece(to_pos)
move = Move(from_pos, to_pos, piece, captured)
# Handle castling
if piece.piece_type == PieceType.KING and abs(to_pos.col - from_pos.col) == 2:
move.is_castling = True
rook_col = 7 if to_pos.col == 6 else 0
new_rook_col = 5 if to_pos.col == 6 else 3
rook = self.board.get_piece(Position(from_pos.row, rook_col))
self.board.set_piece(Position(from_pos.row, new_rook_col), rook)
self.board.set_piece(Position(from_pos.row, rook_col), None)
rook.has_moved = True
# Handle en passant
if (piece.piece_type == PieceType.PAWN and
self.board.en_passant_target == to_pos):
move.is_en_passant = True
ep_row = from_pos.row
captured = self.board.get_piece(Position(ep_row, to_pos.col))
self.board.set_piece(Position(ep_row, to_pos.col), None)
move.captured = captured
# Set en passant target for double pawn push
self.board.en_passant_target = None
if piece.piece_type == PieceType.PAWN and abs(to_pos.row - from_pos.row) == 2:
ep_row = (from_pos.row + to_pos.row) // 2
self.board.en_passant_target = Position(ep_row, from_pos.col)
# Move piece
self.board.set_piece(to_pos, piece)
self.board.set_piece(from_pos, None)
piece.has_moved = True
# Pawn promotion
if (piece.piece_type == PieceType.PAWN and
(to_pos.row == 0 or to_pos.row == 7)):
promo_type = promotion or PieceType.QUEEN
promo_map = {PieceType.QUEEN: Queen, PieceType.ROOK: Rook,
PieceType.BISHOP: Bishop, PieceType.KNIGHT: Knight}
self.board.set_piece(to_pos, promo_map[promo_type](piece.color))
move.promotion = promo_type
return move
def _switch_turn(self):
self.current_turn = (Color.BLACK if self.current_turn == Color.WHITE
else Color.WHITE)
def _update_status(self):
color = self.current_turn
has_legal_moves = any(
self._get_legal_moves(Position(r, c))
for r in range(8) for c in range(8)
if self.board.grid[r][c] and self.board.grid[r][c].color == color
)
if not has_legal_moves:
if self.board.is_in_check(color):
self.status = (GameStatus.WHITE_WINS if color == Color.BLACK
else GameStatus.BLACK_WINS)
else:
self.status = GameStatus.STALEMATE
def get_move_notation(self, move: Move) -> str:
"""Generate algebraic notation for a move."""
if move.is_castling:
return "O-O" if move.to_pos.col == 6 else "O-O-O"
piece_symbol = "" if move.piece.piece_type == PieceType.PAWN else move.piece.piece_type.value[0].upper()
capture = "x" if move.captured else ""
dest = move.to_pos.to_algebraic()
promo = "=" + move.promotion.value[0].upper() if move.promotion else ""
return f"{piece_symbol}{capture}{dest}{promo}"
Key Design Decisions
- Abstract Piece class: Each piece type overrides
get_valid_moves()— Open/Closed principle - Pseudo-legal + legality filter: Pieces generate candidate moves; Game filters those that expose the king to check. Separates concerns cleanly.
- Board as service object: Board provides helper methods (
is_in_check,get_sliding_moves) rather than pieces querying the board directly - Move record: Dataclass captures full move context for history, undo, and notation generation
- State machine:
GameStatusenum models game lifecycle transitions cleanly
Interview Follow-Up Discussion Points
- Fifty-move rule: Track half-move clock in Move; draw if no capture or pawn move in 50 moves
- Threefold repetition: Hash board state (piece positions + castling rights + en passant target); draw if same state occurs 3 times
- Undo/redo: Stack of Move objects; reverse each move’s effects including castling rook movement
- AI opponent: Minimax with alpha-beta pruning; evaluation function weights material, center control, piece activity
- Multiplayer online: WebSocket for real-time move broadcast; server validates moves server-side to prevent cheating
- Persistence: PGN (Portable Game Notation) format for storing game history; FEN for board state snapshots
Complexity Analysis
- Move generation per piece: O(1) to O(n) where n = board dimension (sliding pieces)
- Legal move filtering: O(p × m) where p = pieces, m = moves per piece — requires board simulation
- Checkmate detection: O(p × m) — must verify no legal moves exist
- Full game tree (minimax depth d): O(b^d) where b ≈ 35 average branching factor
🏢 Asked at: Meta Interview Guide 2026: Facebook, Instagram, WhatsApp Engineering
🏢 Asked at: Apple Interview Guide 2026: iOS Systems, Hardware-Software Integration, and iCloud Architecture
🏢 Asked at: Atlassian Interview Guide
🏢 Asked at: Snap Interview Guide
🏢 Asked at: LinkedIn Interview Guide 2026: Social Graph Engineering, Feed Ranking, and Professional Network Scale
🏢 Asked at: Uber Interview Guide 2026: Dispatch Systems, Geospatial Algorithms, and Marketplace Engineering
🏢 Asked at: Shopify Interview Guide