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
{“@context”:”https://schema.org”,”@type”:”FAQPage”,”mainEntity”:[{“@type”:”Question”,”name”:”How do you design a Chess game in an OOP interview?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Start with an abstract Piece class that each piece type extends, implementing get_valid_moves(). Use a Board class to manage the 8×8 grid and provide helper methods like is_in_check(). A ChessGame controller handles turn management, legal move filtering (which requires simulating each move to verify the king is not left in check), and special moves like castling, en passant, and pawn promotion.”}},{“@type”:”Question”,”name”:”How do you detect check and checkmate in a Chess game?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Check is detected by finding the current player’s king position, then scanning all enemy pieces to see if any can attack the king. Checkmate is detected when the player is in check AND has no legal moves that would escape check. For each candidate move, simulate it on the board and verify the king is no longer in check before adding it to the legal moves list.”}},{“@type”:”Question”,”name”:”How do you implement castling in a Chess game?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”Castling requires: neither the king nor the chosen rook has moved (tracked via has_moved flag), no pieces between them, the king is not in check, and the king does not pass through or land on an attacked square. When castling kingside, the king moves from e1 to g1 and the rook from h1 to f1. Queenside: king to c1, rook from a1 to d1.”}},{“@type”:”Question”,”name”:”What design pattern is used for chess piece move generation?”,”acceptedAnswer”:{“@type”:”Answer”,”text”:”The Template Method / Strategy pattern via abstract base class. Each piece type (King, Queen, Rook, Bishop, Knight, Pawn) implements its own get_valid_moves() method. Sliding pieces (Queen, Rook, Bishop) share a helper on the Board class that generates moves along directions until blocked. The Game class then filters pseudo-legal moves to remove those that leave the king in check.”}}]}
🏢 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