LLD: Chess Game
1. Introduction
Designing a full-featured, object-oriented Chess engine is one of the most classic and rigorous Low-Level Design (LLD) architectural problems asked in senior technical interviews. On the surface, chess appears simple: an grid with pieces operating under well-defined mechanics. However, an enterprise-grade design reveals profound architectural complexities:
- Polymorphism and Open/Closed Principle: How do you structure piece movement without creating a monolithic, unmaintainable chain of conditional
if/elsestatements? - State Restoration & Move History: How do you execute moves, compute tentative game states (to evaluate if a proposed move exposes the King to check), and implement time execution undo functionality?
- Complex Edge Case Rules: How do you model transient gameplay states such as En Passant, Castling (requiring tracking whether pieces have moved previously), and Pawn Promotion without contaminating clean abstractions?
- Scalability and Extensibility: How easily can your class hierarchy evolve to accommodate variations like 3D Chess, Fischer Random (Chess960), or different board geometries?
2. System Requirements & Design Principles
Core Functional Requirements
- Board Structure: Standard grid of cells (
0-7rows and0-7columns, equivalent to Algebraic notationa1throughh8). - Pieces and Rules: Support all standard pieces (King, Queen, Rook, Bishop, Knight, Pawn), each carrying distinct movement vectors and capturing rules.
- Turn Management: Two participating actors (White and Black) alternating consecutive execution turns with strict movement validation.
- Special Move Rules: Support intricate chess laws including Castling (Kingside & Queenside), En Passant, and Pawn Promotion.
- GameState & Condition Verification: Real-time computation of active board threats to identify Check, Checkmate, Stalemate, Resignation, and draw conditions (Threefold Repetition, 50-Move rule).
- Move History & Undo: Support complete move logging with undo and replay capabilities.
Non-Functional Requirements & SOLID Observance
- Single Responsibility Principle (SRP): The
Boardshould only represent the topological grid and item placement. It should not calculate whether a checkmate occurred or govern user authentication. - Open-Closed Principle (OCP): Introducing a new fairy chess piece (e.g., an "Archbishop" that moves like a Bishop + Knight) should only require subclassing
Piecewithout altering existing validation loops inGameController. - Liskov Substitution Principle (LSP): Any concrete subclass of
Piecemust seamlessly substitute into any board slot without throwing runtime type casting exceptions or unexpected behavioral violations.
3. Object-Oriented Architectural Topology
UML Class Diagram
The architecture organizes around decoupled abstractions: structural representation (Board, Spot, Piece), movement encapsulation (Move, CastlingMove), and match lifecycle management (ChessGame, Player, GameStatus).
classDiagram
class ChessGame {
-Board board
-Player[] players
-int currentPlayerIndex
-List~Move~ moveHistory
-GameStatus status
+makeMove(Move move) boolean
+undoLastMove() boolean
+isCheckmate(Color color) boolean
+isStalemate(Color color) boolean
-switchTurn() void
}
class Board {
-Spot[][] boxes
+getSpot(int x, int y) Spot
+resetBoard() void
+setupStandardPieces() void
+cloneBoardState() Board
}
class Spot {
-int x
-int y
-Piece piece
+isOccupied() boolean
+getPiece() Piece
+setPiece(Piece p) void
+removePiece() Piece
}
class Piece {
<<abstract>>
-boolean killed
-boolean hasMoved
-Color color
+isWhite() boolean
+isKilled() boolean
+setKilled(boolean killed) void
+canMove(Board board, Spot start, Spot end)* boolean
+generateLegalMoves(Board board, Spot current)* List~Spot~
}
class King {
+canMove(Board board, Spot start, Spot end) boolean
+isCastlingValid(Board board, Spot start, Spot end) boolean
}
class Queen {
+canMove(Board board, Spot start, Spot end) boolean
}
class Rook {
+canMove(Board board, Spot start, Spot end) boolean
}
class Bishop {
+canMove(Board board, Spot start, Spot end) boolean
}
class Knight {
+canMove(Board board, Spot start, Spot end) boolean
}
class Pawn {
-boolean eligibleForEnPassant
+canMove(Board board, Spot start, Spot end) boolean
+isEnPassant(Board board, Spot start, Spot end) boolean
}
class Move {
-Spot start
-Spot end
-Piece pieceMoved
-Piece pieceKilled
-boolean isCastlingMove
-boolean isPromotion
+execute(Board board) void
+undo(Board board) void
}
class Player {
-String accountId
-Color color
-boolean isHuman
}
class GameStatus {
<<enumeration>>
ACTIVE
WHITE_VICTORY_CHECKMATE
BLACK_VICTORY_CHECKMATE
STALEMATE
FORFEIT
}
ChessGame "1" *-- "1" Board
ChessGame "1" *-- "2" Player
ChessGame "1" *-- "*" Move
ChessGame "1" --> "1" GameStatus
Board "1" *-- "64" Spot
Spot "0..1" o-- "1" Piece
Piece <|-- King
Piece <|-- Queen
Piece <|-- Rook
Piece <|-- Bishop
Piece <|-- Knight
Piece <|-- Pawn
Move "1" --> "2" Spot
Move "1" --> "1..2" Piece
4. Design Patterns Utilized
1. Strategy & Polymorphism (Piece Validation)
Instead of centralizing move checking inside a massive switch-case evaluation block, every concrete unit extends the abstract Piece class and implements its own canMove(board, start, end) methodology.
- Rook: Evaluates linear path occupancy (horizontal/vertical vectors).
- Bishop: Evaluates diagonal trajectory vectors ().
- Knight: Evaluates step magnitude ( or ), completely ignoring intermittent board obstructions.
2. Command Pattern (Move Execution & Undo)
Encapsulating every attempted move inside an explicit Move class transforms actions into first-class transactional objects.
- By retaining internal references to
pieceMoved,pieceKilled, and previoushasMovedboolean states, we achieve clean undo and replay capabilities. - Crucial Architectural Utility: To evaluate if a proposed move puts the player's own King in check, the engine issues
move.execute(board), evaluates check conditions, and immediately reverts viamove.undo(board). This eliminates the memory and allocation overhead of deep-cloning the entire 64-spot array for every evaluated search tree branch.
3. Factory Pattern (Piece Initializations & Promotions)
When a Pawn reaches the opponent's rear rank (row 0 for Black, row 7 for White), it triggers promotion. A PieceFactory.createPiece(Type.QUEEN, Color.WHITE) encapsulates object creation, allowing runtime promotion instantiation without entanglements in core loop execution.
5. Production-Grade Reference Implementation
Below is an extensible, type-safe Python implementation demonstrating core board representation, polymorphic movement validation, Command-driven transactional executions, and self-check exposure detection.
from abc import ABC, abstractmethod
from enum import Enum, auto
from typing import List, Optional, Tuple
class Color(Enum):
WHITE = auto()
BLACK = auto()
class GameStatus(Enum):
ACTIVE = auto()
WHITE_WIN = auto()
BLACK_WIN = auto()
STALEMATE = auto()
class Spot:
"""Represents a discrete grid cell coordinate on the chessboard."""
def __init__(self, x: int, y: int, piece: Optional['Piece'] = None):
if not (0 <= x < 8 and 0 <= y < 8):
raise ValueError(f"Invalid coordinate dimensions: ({x}, {y})")
self.x = x
self.y = y
self.piece = piece
def is_occupied(self) -> bool:
return self.piece is not None
def __str__(self) -> str:
coords = f"{chr(ord('a') + self.y)}{self.x + 1}"
p_str = f"[{self.piece}]" if self.piece else "[ ]"
return f"{coords}:{p_str}"
class Piece(ABC):
"""Abstract Base Class enforcing modular Open-Closed contract for chess entities."""
def __init__(self, color: Color):
self.color = color
self.has_moved = False
self.killed = False
@abstractmethod
def can_move(self, board: 'Board', start: Spot, end: Spot) -> bool:
"""Determines mathematical geometrical capability to transition from start to end."""
pass
@abstractmethod
def get_symbol(self) -> str:
pass
def __str__(self) -> str:
return f"{'W' if self.color == Color.WHITE else 'B'}-{self.get_symbol()}"
class King(Piece):
def get_symbol(self) -> str:
return "K"
def can_move(self, board: 'Board', start: Spot, end: Spot) -> bool:
if end.is_occupied() and end.piece.color == self.color:
return False
dx = abs(start.x - end.x)
dy = abs(start.y - end.y)
# Standard adjacent 1-square reach
return (dx + dy == 1) or (dx == 1 and dy == 1)
class Knight(Piece):
def get_symbol(self) -> str:
return "N"
def can_move(self, board: 'Board', start: Spot, end: Spot) -> bool:
if end.is_occupied() and end.piece.color == self.color:
return False
dx = abs(start.x - end.x)
dy = abs(start.y - end.y)
# L-shaped leap trajectory
return (dx == 2 and dy == 1) or (dx == 1 and dy == 2)
class Rook(Piece):
def get_symbol(self) -> str:
return "R"
def can_move(self, board: 'Board', start: Spot, end: Spot) -> bool:
if end.is_occupied() and end.piece.color == self.color:
return False
if start.x != end.x and start.y != end.y:
return False
# Evaluate intervening trajectory blockages
return board.is_path_clear(start, end)
class Board:
"""Manages the 64-cell spatial topology and trajectory validation services."""
def __init__(self):
self.grid: List[List[Spot]] = [[Spot(x, y) for y in range(8)] for x in range(8)]
def get_spot(self, x: int, y: int) -> Spot:
return self.grid[x][y]
def is_path_clear(self, start: Spot, end: Spot) -> bool:
"""Verifies zero physical intermediate obstructions across lines of sight."""
dx = end.x - start.x
dy = end.y - start.y
step_x = 0 if dx == 0 else (1 if dx > 0 else -1)
step_y = 0 if dy == 0 else (1 if dy > 0 else -1)
curr_x, curr_y = start.x + step_x, start.y + step_y
while (curr_x, curr_y) != (end.x, end.y):
if self.grid[curr_x][curr_y].is_occupied():
return False
curr_x += step_x
curr_y += step_y
return True
def find_king(self, color: Color) -> Spot:
for row in self.grid:
for spot in row:
if spot.is_occupied() and isinstance(spot.piece, King) and spot.piece.color == color:
return spot
raise RuntimeError(f"Critical inconsistency: {color} King missing from topological board!")
def setup_custom_piece(self, piece: Piece, x: int, y: int):
self.grid[x][y].piece = piece
class Move:
"""Command design abstraction allowing O(1) state mutation, execution, and rollback."""
def __init__(self, start: Spot, end: Spot):
self.start = start
self.end = end
self.piece_moved: Piece = start.piece
self.piece_killed: Optional[Piece] = end.piece
self.prior_has_moved_state: bool = self.piece_moved.has_moved if self.piece_moved else False
def execute(self, board: Board):
self.end.piece = self.piece_moved
self.start.piece = None
self.piece_moved.has_moved = True
if self.piece_killed:
self.piece_killed.killed = True
def undo(self, board: Board):
self.start.piece = self.piece_moved
self.end.piece = self.piece_killed
self.piece_moved.has_moved = self.prior_has_moved_state
if self.piece_killed:
self.piece_killed.killed = False
class ChessEngine:
"""Orchestrates match progress, enforces safety rules, and evaluates checkmate conditions."""
def __init__(self, board: Board):
self.board = board
self.turn = Color.WHITE
self.history: List[Move] = []
self.status = GameStatus.ACTIVE
def is_square_attacked(self, target_spot: Spot, attacking_color: Color) -> bool:
"""Determines if any opponent unit can legally strike the indicated square."""
for row in self.board.grid:
for spot in row:
if spot.is_occupied() and spot.piece.color == attacking_color:
if spot.piece.can_move(self.board, spot, target_spot):
return True
return False
def is_in_check(self, king_color: Color) -> bool:
king_spot = self.board.find_king(king_color)
opponent_color = Color.BLACK if king_color == Color.WHITE else Color.WHITE
return self.is_square_attacked(king_spot, opponent_color)
def process_move(self, start_x: int, start_y: int, end_x: int, end_y: int) -> bool:
"""Validates geometry, performs self-check exposure protection, and logs history."""
if self.status != GameStatus.ACTIVE:
print("[Engine] Game is terminated.")
return False
start_spot = self.board.get_spot(start_x, start_y)
end_spot = self.board.get_spot(end_x, end_y)
if not start_spot.is_occupied() or start_spot.piece.color != self.turn:
print(f"[Engine] Invalid selection at ({start_x}, {start_y}). Not {self.turn.name}'s piece.")
return False
piece = start_spot.piece
if not piece.can_move(self.board, start_spot, end_spot):
print(f"[Engine] Illegal trajectory for {piece} to ({end_x}, {end_y}).")
return False
# Transactional trial execution via Command pattern
move = Move(start_spot, end_spot)
move.execute(self.board)
# Self-Check validation rule: You cannot conclude your turn with your own King in check
if self.is_in_check(self.turn):
print(f"[Engine] Move rejected! Leaves {self.turn.name} King in check.")
move.undo(self.board)
return False
# Commit verified action
self.history.append(move)
print(f"[Engine] Executed: {piece} from ({start_x},{start_y}) to ({end_x},{end_y})")
# Advance operational turn
self.turn = Color.BLACK if self.turn == Color.WHITE else Color.WHITE
if self.is_in_check(self.turn):
print(f"[Engine] WARNING: {self.turn.name} King is now IN CHECK!")
return True
# --- Production Verification Script ---
if __name__ == "__main__":
b = Board()
# Deploy test pieces
white_king = King(Color.WHITE)
white_rook = Rook(Color.WHITE)
black_king = King(Color.BLACK)
black_knight = Knight(Color.BLACK)
b.setup_custom_piece(white_king, 0, 4) # e1
b.setup_custom_piece(white_rook, 0, 0) # a1
b.setup_custom_piece(black_king, 7, 4) # e8
b.setup_custom_piece(black_knight, 5, 3) # d6
engine = ChessEngine(b)
print("\n--- Initial Game State ---")
print(f"White King at e1, White Rook at a1. Black King at e8, Black Knight at d6.")
# 1. White moves Rook horizontally to open file e (a1 -> e1 occupied by King, let's try a1 -> e1 fail, a1 -> d1 success)
engine.process_move(0, 0, 0, 3) # Rook to d1 (a1 -> d1)
# 2. Black Knight attempts illegal linear move
engine.process_move(5, 3, 4, 3) # Knight d6 -> d5 (Illegal for knight)
# 3. Black Knight performs legal jump targeting check on White King
engine.process_move(5, 3, 6, 5) # Knight d6 -> f7
6. Mathematical Modeling & Complexity Analysis
Game Tree Complexity & Shannon Number
When designing artificial intelligence search evaluation models (such as Minimax with Alpha-Beta pruning) on top of this OOP architecture, understanding computational scaling is fundamental.
- Branching Factor (): In a typical middlegame position, a player has approximately 35 to 40 legal moves available.
- Game Depth (): A full contest lasts an average of 80 ply (40 individual turns per player).
- The Shannon Number: The lower bound of total complexity across standard chess game trees evaluates to:
This vastly exceeds the total number of physical atoms in the observable universe (). Consequently, software engines cannot evaluate exhaustive game histories; they rely on deterministic search truncation combined with static evaluation heuristics () representing positional board strength:
Space and Time Complexity of OOP Architecture
- Move Validation (
canMove): for directional unit leaps (King, Knight). where for sliding vectors (Rook, Bishop, Queen) to iterate across line-of-sight grid checks. - Check Evaluation (
is_in_check): Requires iterating across all active opponent pieces and running trajectory checks against the King's coordinate: where active adversary units and path tiles effectively constant processing overhead due to tight physical boundaries ( squares total). - Move Undo / State Reversal: execution via Command Pattern pointer swaps without allocating heap memory.
Bitboard Architecture (Industrial Performance Optimization)
While object-oriented modeling (Spot[][], polymorphic class resolution) is paramount for standard LLD interviews and business modeling, tournament-grade engines (Stockfish, Komodo) completely abandon OOP hierarchies in favor of Bitboards.
- A Bitboard models the entire 64-square grid as a single 64-bit unsigned integer (
uint64_t), where each scalar bit maps directly to a coordinate (). - Rather than iterating loop arrays, checking if a King is attacked by enemy Rooks executes in a single CPU clock tick using hardware-level Bitwise Logical Operations:
// C++ Bitboard rapid intersection test
uint64_t white_rooks = 0x0000000000000081ULL; // Rooks on a1 and h1
uint64_t rank_1_mask = 0x00000000000000FFULL;
uint64_t occupied_squares = white_rooks | black_pieces | ... ;
// Instantaneously calculate legal horizontal attacks using bit-shifts and CPU instruction sets (BMI2)
uint64_t valid_attacks = get_rook_attacks(king_square, occupied_squares);
bool is_checked = (valid_attacks & white_rooks) != 0;
7. Edge Cases & Corner Cases
A truly complete LLD architecture accounts for intricate edge conditions without breaking clean encapsulation:
1. Disguised Pinned Piece Moves (Self-Check Prevention)
- Problem: A White Knight sits on
d2directly between the White King one1and a Black Bishop ona5. The Knight attempts a legal L-shaped leap toc4. The move appears valid according to the Knight's geometry, but moving it opens an unblocked diagonal line of sight, placing the White King in check. - Resolution: The
ChessEngine.process_moveworkflow must never trust individual piece geometry alone. It must execute a speculative, temporary commit (move.execute()), invokeis_in_check(current_turn), and instantly revert (move.undo()) if the test fails.
2. Castling Verification Checklist
Castling is the only standard maneuver where two distinct pieces (King and Rook) displace concurrently within a single turn. The validation engine must assert five rigid invariants before enabling this transition:
- Neither the King nor the designated Rook has moved previously in the contest (
!king.hasMoved && !rook.hasMoved). - All intermediate tiles between the King and Rook are completely void of piece occupation.
- The King is not currently in Check.
- The King does not transit across any square directly attacked by an enemy piece during its lateral slide.
- The King does not terminate its jump on a square attacked by an enemy unit.
3. En Passant Expiration Tracking
When a Pawn executes a double-step leap from its start rank (e2 to e4), an opponent Pawn stationed on an adjacent file (d4) can capture it as if it had advanced only a single step to e3.
- The Catch: This option is only valid immediately on the subsequent turn. If the opponent responds with any other board action, the right to perform en passant on that specific Pawn evaporates permanently.
- Implementation Design: Attach a transient tracking reference to
ChessGame:Spot enPassantVulnerableSpot. This variable is populated only on a double-step Pawn jump and is completely cleared (None) at the opening of every subsequent turn cycle.
8. Senior Interview Q&A
Q1: Why should you avoid storing historical board states by taking complete snapshots of the 2D Spot[][] array after every single move?
Answer:
Snapshotting the full board array at every step incurs profound runtime memory overhead and Garbage Collection churn. In an average 80-ply match, deep-cloning an array of 64 object instances—each holding references to individual Piece structures—generates massive redundant allocations ( objects) to represent what is usually just two altered memory references per turn.
By implementing the Command Pattern via explicit Move instances, we record only the essential delta transformation: the initial starting spot, the ending destination spot, the moved piece reference, and any captured piece reference. Reversing or replaying states requires only flipping two internal memory pointers ( time and space complexity per turn), completely avoiding cloning allocations.
Q2: How would you extend your architecture to support artificial intelligence evaluation algorithms (such as Minimax with Alpha-Beta pruning) without locking up the user interface?
Answer: To support deep game-tree exploration without blocking regular interactive execution, we decouple the AI evaluator using an asynchronous interface:
- Abstract Actor Hierarchy: Create an abstract
Playerinterface containing the contractasync Future<Move> makeMove(Board readOnlyBoard). Subclass this intoHumanPlayer(which resolves futures upon UI click listener invocation) andAIPlayer. - Immediacy via Thread-Pool Task Isolation: When an
AIPlayerevaluates its turn, it clones the board once at the inception of its execution loop to establish an isolated domain sandbox. It launches computational search evaluation on a background worker CPU thread pool (e.g., viaCompletableFuturein Java orasyncioTask worker in Python/Rust). - Alpha-Beta Search Engine: The background worker iterates deep into candidate branches using simple
execute()andundo()operations on its isolated board copy, applying standard static board scoring functions to prune non-viable subtrees before returning the mathematically optimalMoveback to the UI thread dispatcher.
Q3: If you were asked to support "Fischer Random Chess" (Chess960), where back-rank piece arrangements are randomized before starting, what classes in your architecture would change?
Answer: A properly architected LLD utilizing SOLID mechanics absorbs this requirement cleanly with virtually zero modifications to core game mechanics:
- What remains unchanged:
Piece, all concrete subclass movement logics (King, Queen, Rook, etc.),Move,Commandlogic,Spot, andis_in_checkcalculations remain completely untouched. - What alters via Extensibility: We implement a Strategy / Factory abstraction for Board Configuration. Instead of hardcoding piece coordinates within
Board.setupStandardPieces(), we define an interfaceBoardSetupStrategywith methodvoid deployPieces(Board board). We implement two concrete strategies:StandardSetupStrategy(classic setup) andFischerRandomSetupStrategy(which employs a randomized permutation algorithm to arrange back-rank units while ensuring bishops reside on opposite colors and the King falls between the two Rooks). We inject the chosen strategy into theChessGameconstructor.
Q4: Describe how you would handle persistent draws such as the "Threefold Repetition" rule or the "50-Move without capture or pawn advance" rule efficiently.
Answer:
- 50-Move Rule: Implement an integer counter on the
ChessGameclass:int halfMoveClock. Whenever any move executes viaprocess_move(), check if the moving unit is an instance ofPawnor ifmove.pieceKilled != null. If either holds true, resethalfMoveClock = 0; otherwise, incrementhalfMoveClock++. If the counter hits100(representing 50 full turns per player), transitionstatus = GameStatus.STALEMATE_50_MOVE_RULE. - Threefold Repetition: Tracking repetition requires establishing identity parity across spatial configurations, turn assignments, and castling/en-passant availabilities. To avoid expensive matrix equality checks across history arrays, implement Zobrist Hashing. Assign an array of random 64-bit integers (
uint64_t) to every possible piece-color-square combination. As moves occur, compute the board's new hash identically in via XOR bitwise folding (current_hash ^= ZobristTable[start_piece][start_sq] ^ ZobristTable[moved_piece][end_sq]). Store these hash representations in aHashMap<Uint64, Integer> frequencies. When any hash count reaches3, trigger a draw.
Q5: In a multi-threaded high-concurrency online server hosting tens of thousands of concurrent chess matches (e.g., Chess.com architecture), how would you structure thread safety and persistence for match state transitions?
Answer: In a globally scalable backend system hosting thousands of concurrent matches, running one dedicated OS thread per game is an antipattern that quickly exhausts memory and leads to excessive context switching.
- Stateless Asynchronous Event Loops: Deploy match servers as stateless, asynchronous event processors utilizing non-blocking I/O (e.g., Netty, Node.js, or Go Epoll loops). Incoming Websocket network packets bearing move commands contain a match token (
game_id,source_square,dest_square). - Concurrency Lock Granularity: Because individual chess matches represent strictly sequential turn executions, global system locks are completely unnecessary. We implement fine-grained Distributed In-Memory Sharding (via Redis Cluster). When a move event arrives, the server acquires a brief optimistic atomic lock solely on that specific
game_idstring key, extracts the minimal serialized notation state (e.g., FEN — Forsyth–Edwards Notation string), reconstructs the lightweightBoardtopology in sub-millisecond RAM, executes validation, updates the new FEN back to Redis, releases the key lock, and broadcasts the websocket event to spectators and opponents.
9. Summary & Architectural Takeaways
- Decentralize Unit Capabilities: Use Polymorphic inheritances (
PieceKnight,Rook) to ensure clean isolation of movement calculation logic, respecting the Open/Closed Principle. - Adopt the Command Pattern for State History: Modeling transitions as reversible
Moveobjects enables instantaneous state rollbacks during checkmate simulations and UI undo loops without cloning heavy board matrices. - Distinguish OOP Interview Design from Engine Bitboards: While object hierarchies represent gold-standard system architecture for commercial maintainability, performance-critical computational engines operate exclusively on scalar unsigned 64-bit integer bit-shift arrays.
- Speculate to Validate: Always perform speculative execution rollbacks (
execute()verify!is_in_checkundo()) rather than writing redundant geometrical algorithms to identify discovered checks or illegal pinned piece transitions.