Skip to main content
Chess Game Low Level Design
Difficulty: 🟡 Intermediate | Time: 45-60 minutes | Patterns: Template Method, Command, Memento

🎯 Problem Statement

Design a chess game that can:
  • Support two players (human or computer)
  • Validate moves according to chess rules
  • Detect check, checkmate, and stalemate
  • Track game history and support undo
  • Handle special moves (castling, en passant, pawn promotion)
Why This Problem? Chess is THE showcase for polymorphism - each piece has different movement rules but shares a common interface. Interviewers love to see how you handle complex inheritance hierarchies.

📋 Step 1: Clarify Requirements

Interview Tip: Chess has MANY rules. Always clarify which ones are in scope!

Questions to Ask the Interviewer

CategoryQuestionTypical Scope
PlayersHuman vs Human? Human vs AI?Usually both players human for LLD
RulesWhich special moves to include?Castling, en passant often in scope
FeaturesUndo/redo support?Often asked as extension
UIGUI or console-based?Console for LLD interviews
TimerChess clock functionality?Usually out of scope
VariantsFischer random? 3-check?Out of scope

Functional Requirements

  • Two players take turns (white moves first)
  • Each piece type has unique movement rules
  • Validate that moves don’t leave own king in check
  • Detect game-ending conditions (checkmate, stalemate)
  • Display current board state
  • Support undo/redo of moves

Non-Functional Requirements

  • Clean separation of concerns
  • Easy to add new piece types (for variants)
  • Testable move validation logic
  • Command pattern for move history

🧩 Step 2: Identify Core Objects

Key Insight: Chess is a perfect example of polymorphism - all pieces share get_valid_moves() but each implements it differently.

Game Elements

Game, Board, Cell/Square

Pieces

King, Queen, Rook, Bishop, Knight, Pawn

Players & Moves

Player, Move, GameStatus

Entity-Responsibility Mapping

EntityResponsibilitiesPattern Used
Piece (abstract)Define movement interfaceTemplate Method
King, Queen, etc.Implement specific movementsPolymorphism
BoardTrack piece positions, validate moves-
GameManage turns, check game statusState Pattern (optional)
MoveEncapsulate move action for undo/redoCommand Pattern
MoveHistoryStore moves for undoMemento Pattern

📐 Step 3: Class Diagram

┌─────────────────────────────────────────────────────────────┐
│                          Game                               │
├─────────────────────────────────────────────────────────────┤
│ - board: Board                                              │
│ - players: List[Player]                                     │
│ - currentTurn: Player                                       │
│ - status: GameStatus                                        │
│ - moveHistory: List[Move]                                   │
├─────────────────────────────────────────────────────────────┤
│ + start(): void                                             │
│ + makeMove(from, to): bool                                  │
│ + undo(): bool                                              │
│ + getStatus(): GameStatus                                   │
└─────────────────────────────────────────────────────────────┘

                              │ 1

┌─────────────────────────────────────────────────────────────┐
│                          Board                              │
├─────────────────────────────────────────────────────────────┤
│ - cells: Cell[8][8]                                         │
│ - capturedPieces: List[Piece]                              │
├─────────────────────────────────────────────────────────────┤
│ + getCell(row, col): Cell                                  │
│ + movePiece(from, to): bool                                │
│ + isValidMove(piece, from, to): bool                       │
│ + isKingInCheck(color): bool                               │
└─────────────────────────────────────────────────────────────┘

                              │ 64

┌─────────────────────────────────────────────────────────────┐
│                          Cell                               │
├─────────────────────────────────────────────────────────────┤
│ - row: int                                                  │
│ - col: int                                                  │
│ - piece: Piece                                              │
├─────────────────────────────────────────────────────────────┤
│ + isEmpty(): bool                                           │
│ + getPiece(): Piece                                         │
│ + setPiece(piece): void                                     │
└─────────────────────────────────────────────────────────────┘

                     ┌─────────────────┐
                     │    <<abstract>> │
                     │      Piece      │
                     ├─────────────────┤
                     │ - color: Color  │
                     │ - hasMoved: bool│
                     ├─────────────────┤
                     │ + canMove()     │
                     │ + getValidMoves()│
                     └─────────────────┘

          ┌────────┬────────┼────────┬────────┬────────┐
          │        │        │        │        │        │
       ┌──┴──┐ ┌───┴──┐ ┌───┴──┐ ┌───┴──┐ ┌───┴──┐ ┌───┴──┐
       │King │ │Queen │ │ Rook │ │Bishop│ │Knight│ │ Pawn │
       └─────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘

Step 4: Implementation

Enums and Constants

from enum import Enum
from typing import Optional, List, Tuple
from abc import ABC, abstractmethod
from dataclasses import dataclass
import copy

class Color(Enum):
    WHITE = "white"
    BLACK = "black"
    
    def opposite(self):
        return Color.BLACK if self == Color.WHITE else Color.WHITE

class GameStatus(Enum):
    ACTIVE = "active"
    WHITE_WIN = "white_wins"
    BLACK_WIN = "black_wins"
    STALEMATE = "stalemate"
    RESIGNED = "resigned"

@dataclass
class Position:
    row: int
    col: int
    
    def __hash__(self):
        return hash((self.row, self.col))
    
    def is_valid(self) -> bool:
        return 0 <= self.row < 8 and 0 <= self.col < 8
    
    def __add__(self, other):
        return Position(self.row + other.row, self.col + other.col)

Move Class

@dataclass
class Move:
    piece: 'Piece'
    from_pos: Position
    to_pos: Position
    captured_piece: Optional['Piece'] = None
    is_castling: bool = False
    is_en_passant: bool = False
    is_promotion: bool = False
    promotion_piece: Optional['Piece'] = None
    
    def __str__(self):
        piece_symbol = self.piece.__class__.__name__[0]
        if isinstance(self.piece, Knight):
            piece_symbol = 'N'
        if isinstance(self.piece, Pawn):
            piece_symbol = ''
        
        capture = 'x' if self.captured_piece else ''
        to_square = chr(ord('a') + self.to_pos.col) + str(self.to_pos.row + 1)
        
        return f"{piece_symbol}{capture}{to_square}"

Piece Base Class

class Piece(ABC):
    def __init__(self, color: Color):
        self.color = color
        self.has_moved = False
    
    @abstractmethod
    def get_possible_moves(self, position: Position, board: 'Board') -> List[Position]:
        """Get all possible moves (may include moves that leave king in check)"""
        pass
    
    @abstractmethod
    def symbol(self) -> str:
        """Return piece symbol for display"""
        pass
    
    def _get_moves_in_direction(
        self, 
        position: Position, 
        board: 'Board',
        direction: Tuple[int, int],
        max_steps: int = 8
    ) -> List[Position]:
        """Helper for sliding pieces (Rook, Bishop, Queen)"""
        moves = []
        current = position
        
        for _ in range(max_steps):
            current = Position(current.row + direction[0], current.col + direction[1])
            
            if not current.is_valid():
                break
            
            cell = board.get_cell(current)
            if cell.piece is None:
                moves.append(current)
            elif cell.piece.color != self.color:
                moves.append(current)  # Can capture
                break
            else:
                break  # Own piece blocks
        
        return moves

Concrete Piece Classes

class King(Piece):
    def symbol(self) -> str:
        return '♔' if self.color == Color.WHITE else '♚'
    
    def get_possible_moves(self, position: Position, board: 'Board') -> List[Position]:
        moves = []
        directions = [
            (-1, -1), (-1, 0), (-1, 1),
            (0, -1),           (0, 1),
            (1, -1),  (1, 0),  (1, 1)
        ]
        
        for d in directions:
            new_pos = Position(position.row + d[0], position.col + d[1])
            if new_pos.is_valid():
                cell = board.get_cell(new_pos)
                if cell.piece is None or cell.piece.color != self.color:
                    moves.append(new_pos)
        
        # Castling
        if not self.has_moved:
            moves.extend(self._get_castling_moves(position, board))
        
        return moves
    
    def _get_castling_moves(self, position: Position, board: 'Board') -> List[Position]:
        moves = []
        row = position.row
        
        # King-side castling
        if self._can_castle_kingside(row, board):
            moves.append(Position(row, 6))
        
        # Queen-side castling
        if self._can_castle_queenside(row, board):
            moves.append(Position(row, 2))
        
        return moves
    
    def _can_castle_kingside(self, row: int, board: 'Board') -> bool:
        rook_cell = board.get_cell(Position(row, 7))
        if not rook_cell.piece or not isinstance(rook_cell.piece, Rook):
            return False
        if rook_cell.piece.has_moved:
            return False
        
        # Check path is clear
        for col in [5, 6]:
            if board.get_cell(Position(row, col)).piece is not None:
                return False
        
        # Check king doesn't pass through check
        for col in [4, 5, 6]:
            if board.is_square_attacked(Position(row, col), self.color.opposite()):
                return False
        
        return True
    
    def _can_castle_queenside(self, row: int, board: 'Board') -> bool:
        rook_cell = board.get_cell(Position(row, 0))
        if not rook_cell.piece or not isinstance(rook_cell.piece, Rook):
            return False
        if rook_cell.piece.has_moved:
            return False
        
        # Check path is clear
        for col in [1, 2, 3]:
            if board.get_cell(Position(row, col)).piece is not None:
                return False
        
        # Check king doesn't pass through check
        for col in [2, 3, 4]:
            if board.is_square_attacked(Position(row, col), self.color.opposite()):
                return False
        
        return True


class Queen(Piece):
    def symbol(self) -> str:
        return '♕' if self.color == Color.WHITE else '♛'
    
    def get_possible_moves(self, position: Position, board: 'Board') -> List[Position]:
        moves = []
        # Queen combines Rook and Bishop movements
        directions = [
            (-1, 0), (1, 0), (0, -1), (0, 1),  # Rook directions
            (-1, -1), (-1, 1), (1, -1), (1, 1)  # Bishop directions
        ]
        
        for direction in directions:
            moves.extend(self._get_moves_in_direction(position, board, direction))
        
        return moves


class Rook(Piece):
    def symbol(self) -> str:
        return '♖' if self.color == Color.WHITE else '♜'
    
    def get_possible_moves(self, position: Position, board: 'Board') -> List[Position]:
        moves = []
        directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
        
        for direction in directions:
            moves.extend(self._get_moves_in_direction(position, board, direction))
        
        return moves


class Bishop(Piece):
    def symbol(self) -> str:
        return '♗' if self.color == Color.WHITE else '♝'
    
    def get_possible_moves(self, position: Position, board: 'Board') -> List[Position]:
        moves = []
        directions = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
        
        for direction in directions:
            moves.extend(self._get_moves_in_direction(position, board, direction))
        
        return moves


class Knight(Piece):
    def symbol(self) -> str:
        return '♘' if self.color == Color.WHITE else '♞'
    
    def get_possible_moves(self, position: Position, board: 'Board') -> List[Position]:
        moves = []
        # L-shaped moves
        offsets = [
            (-2, -1), (-2, 1), (-1, -2), (-1, 2),
            (1, -2), (1, 2), (2, -1), (2, 1)
        ]
        
        for offset in offsets:
            new_pos = Position(position.row + offset[0], position.col + offset[1])
            if new_pos.is_valid():
                cell = board.get_cell(new_pos)
                if cell.piece is None or cell.piece.color != self.color:
                    moves.append(new_pos)
        
        return moves


class Pawn(Piece):
    def symbol(self) -> str:
        return '♙' if self.color == Color.WHITE else '♟'
    
    def get_possible_moves(self, position: Position, board: 'Board') -> List[Position]:
        moves = []
        direction = 1 if self.color == Color.WHITE else -1
        start_row = 1 if self.color == Color.WHITE else 6
        
        # Forward move
        forward = Position(position.row + direction, position.col)
        if forward.is_valid() and board.get_cell(forward).piece is None:
            moves.append(forward)
            
            # Double move from starting position
            if position.row == start_row:
                double = Position(position.row + 2 * direction, position.col)
                if board.get_cell(double).piece is None:
                    moves.append(double)
        
        # Diagonal captures
        for col_offset in [-1, 1]:
            capture_pos = Position(position.row + direction, position.col + col_offset)
            if capture_pos.is_valid():
                cell = board.get_cell(capture_pos)
                if cell.piece and cell.piece.color != self.color:
                    moves.append(capture_pos)
                
                # En passant
                if self._can_en_passant(position, capture_pos, board):
                    moves.append(capture_pos)
        
        return moves
    
    def _can_en_passant(self, from_pos: Position, to_pos: Position, board: 'Board') -> bool:
        if not board.last_move:
            return False
        
        last_move = board.last_move
        if not isinstance(last_move.piece, Pawn):
            return False
        
        # Was it a double pawn move?
        if abs(last_move.from_pos.row - last_move.to_pos.row) != 2:
            return False
        
        # Is it adjacent to us?
        if last_move.to_pos.row != from_pos.row:
            return False
        if abs(last_move.to_pos.col - from_pos.col) != 1:
            return False
        
        # Target square matches en passant capture
        return last_move.to_pos.col == to_pos.col

Cell Class

class Cell:
    def __init__(self, row: int, col: int):
        self.row = row
        self.col = col
        self.piece: Optional[Piece] = None
    
    @property
    def position(self) -> Position:
        return Position(self.row, self.col)
    
    def is_empty(self) -> bool:
        return self.piece is None
    
    def __str__(self):
        if self.piece:
            return self.piece.symbol()
        return '·'

Board Class

class Board:
    def __init__(self):
        self.cells: List[List[Cell]] = [
            [Cell(row, col) for col in range(8)] for row in range(8)
        ]
        self.captured_pieces: List[Piece] = []
        self.last_move: Optional[Move] = None
        self._initialize_pieces()
    
    def _initialize_pieces(self):
        # Place pawns
        for col in range(8):
            self.cells[1][col].piece = Pawn(Color.WHITE)
            self.cells[6][col].piece = Pawn(Color.BLACK)
        
        # Place other pieces
        piece_order = [Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook]
        for col, piece_class in enumerate(piece_order):
            self.cells[0][col].piece = piece_class(Color.WHITE)
            self.cells[7][col].piece = piece_class(Color.BLACK)
    
    def get_cell(self, position: Position) -> Cell:
        return self.cells[position.row][position.col]
    
    def find_king(self, color: Color) -> Position:
        for row in range(8):
            for col in range(8):
                piece = self.cells[row][col].piece
                if isinstance(piece, King) and piece.color == color:
                    return Position(row, col)
        raise ValueError(f"King not found for {color}")
    
    def is_square_attacked(self, position: Position, by_color: Color) -> bool:
        """Check if a square is attacked by any piece of given color"""
        for row in range(8):
            for col in range(8):
                piece = self.cells[row][col].piece
                if piece and piece.color == by_color:
                    moves = piece.get_possible_moves(Position(row, col), self)
                    if position in moves:
                        return True
        return False
    
    def is_king_in_check(self, color: Color) -> bool:
        king_pos = self.find_king(color)
        return self.is_square_attacked(king_pos, color.opposite())
    
    def get_valid_moves(self, position: Position) -> List[Position]:
        """Get all legal moves for piece at position (filters out moves that leave king in check)"""
        piece = self.get_cell(position).piece
        if not piece:
            return []
        
        possible_moves = piece.get_possible_moves(position, self)
        valid_moves = []
        
        for move_pos in possible_moves:
            # Simulate move
            if not self._would_leave_king_in_check(position, move_pos, piece.color):
                valid_moves.append(move_pos)
        
        return valid_moves
    
    def _would_leave_king_in_check(self, from_pos: Position, to_pos: Position, color: Color) -> bool:
        """Simulate move and check if king would be in check"""
        # Save state
        from_cell = self.get_cell(from_pos)
        to_cell = self.get_cell(to_pos)
        moving_piece = from_cell.piece
        captured_piece = to_cell.piece
        
        # Make temporary move
        to_cell.piece = moving_piece
        from_cell.piece = None
        
        # Check if king is in check
        in_check = self.is_king_in_check(color)
        
        # Restore state
        from_cell.piece = moving_piece
        to_cell.piece = captured_piece
        
        return in_check
    
    def make_move(self, from_pos: Position, to_pos: Position) -> Optional[Move]:
        """Execute a move on the board"""
        from_cell = self.get_cell(from_pos)
        to_cell = self.get_cell(to_pos)
        piece = from_cell.piece
        
        if not piece:
            return None
        
        # Create move record
        move = Move(
            piece=piece,
            from_pos=from_pos,
            to_pos=to_pos,
            captured_piece=to_cell.piece
        )
        
        # Handle special moves
        if isinstance(piece, King) and abs(from_pos.col - to_pos.col) == 2:
            self._execute_castling(from_pos, to_pos)
            move.is_castling = True
        elif isinstance(piece, Pawn):
            if abs(from_pos.col - to_pos.col) == 1 and to_cell.piece is None:
                # En passant
                captured_pawn_pos = Position(from_pos.row, to_pos.col)
                move.captured_piece = self.get_cell(captured_pawn_pos).piece
                self.get_cell(captured_pawn_pos).piece = None
                move.is_en_passant = True
            elif to_pos.row in [0, 7]:
                # Pawn promotion (default to Queen)
                move.is_promotion = True
                move.promotion_piece = Queen(piece.color)
                piece = move.promotion_piece
        
        # Execute move
        if move.captured_piece:
            self.captured_pieces.append(move.captured_piece)
        
        to_cell.piece = piece
        from_cell.piece = None
        piece.has_moved = True
        self.last_move = move
        
        return move
    
    def _execute_castling(self, king_from: Position, king_to: Position):
        """Move rook for castling"""
        row = king_from.row
        if king_to.col == 6:  # King-side
            rook_from = Position(row, 7)
            rook_to = Position(row, 5)
        else:  # Queen-side
            rook_from = Position(row, 0)
            rook_to = Position(row, 3)
        
        rook = self.get_cell(rook_from).piece
        self.get_cell(rook_to).piece = rook
        self.get_cell(rook_from).piece = None
        rook.has_moved = True
    
    def display(self):
        print("\n  a b c d e f g h")
        for row in range(7, -1, -1):
            print(f"{row + 1} ", end="")
            for col in range(8):
                print(f"{self.cells[row][col]} ", end="")
            print(f"{row + 1}")
        print("  a b c d e f g h\n")

Player Class

class Player:
    def __init__(self, color: Color, name: str = None):
        self.color = color
        self.name = name or f"Player ({color.value})"
        self.captured_pieces: List[Piece] = []
    
    def __str__(self):
        return self.name

Game Class

class Game:
    def __init__(self, player1_name: str = None, player2_name: str = None):
        self.board = Board()
        self.players = [
            Player(Color.WHITE, player1_name),
            Player(Color.BLACK, player2_name)
        ]
        self.current_player_index = 0
        self.status = GameStatus.ACTIVE
        self.move_history: List[Move] = []
    
    @property
    def current_player(self) -> Player:
        return self.players[self.current_player_index]
    
    def make_move(self, from_notation: str, to_notation: str) -> bool:
        """Make a move using algebraic notation (e.g., 'e2', 'e4')"""
        from_pos = self._notation_to_position(from_notation)
        to_pos = self._notation_to_position(to_notation)
        return self.make_move_by_position(from_pos, to_pos)
    
    def make_move_by_position(self, from_pos: Position, to_pos: Position) -> bool:
        """Make a move using Position objects"""
        if self.status != GameStatus.ACTIVE:
            print("Game is not active!")
            return False
        
        piece = self.board.get_cell(from_pos).piece
        
        # Validate piece belongs to current player
        if not piece or piece.color != self.current_player.color:
            print("Invalid piece selection!")
            return False
        
        # Validate move is legal
        valid_moves = self.board.get_valid_moves(from_pos)
        if to_pos not in valid_moves:
            print("Invalid move!")
            return False
        
        # Execute move
        move = self.board.make_move(from_pos, to_pos)
        self.move_history.append(move)
        
        if move.captured_piece:
            self.current_player.captured_pieces.append(move.captured_piece)
            print(f"Captured {move.captured_piece.symbol()}!")
        
        # Check game status
        self._update_game_status()
        
        # Switch turns
        self.current_player_index = 1 - self.current_player_index
        
        return True
    
    def _update_game_status(self):
        """Check for checkmate, stalemate after a move"""
        opponent_color = self.current_player.color.opposite()
        
        # Check if opponent has any valid moves
        has_valid_moves = self._has_any_valid_moves(opponent_color)
        is_in_check = self.board.is_king_in_check(opponent_color)
        
        if not has_valid_moves:
            if is_in_check:
                # Checkmate!
                self.status = (GameStatus.WHITE_WIN 
                              if self.current_player.color == Color.WHITE 
                              else GameStatus.BLACK_WIN)
                print(f"Checkmate! {self.current_player} wins!")
            else:
                # Stalemate
                self.status = GameStatus.STALEMATE
                print("Stalemate! Game is a draw.")
        elif is_in_check:
            print("Check!")
    
    def _has_any_valid_moves(self, color: Color) -> bool:
        """Check if player has any valid moves"""
        for row in range(8):
            for col in range(8):
                piece = self.board.cells[row][col].piece
                if piece and piece.color == color:
                    if self.board.get_valid_moves(Position(row, col)):
                        return True
        return False
    
    def _notation_to_position(self, notation: str) -> Position:
        """Convert 'e4' to Position(3, 4)"""
        col = ord(notation[0].lower()) - ord('a')
        row = int(notation[1]) - 1
        return Position(row, col)
    
    def display(self):
        print(f"\n=== {self.current_player}'s turn ===")
        self.board.display()
        
        if self.board.is_king_in_check(self.current_player.color):
            print("⚠️ Your king is in CHECK!")
    
    def get_move_history(self) -> List[str]:
        """Return move history in algebraic notation"""
        history = []
        for i, move in enumerate(self.move_history):
            move_num = i // 2 + 1
            if i % 2 == 0:
                history.append(f"{move_num}. {move}")
            else:
                history[-1] += f" {move}"
        return history

Step 5: Usage Example

# Create a new game
game = Game("Alice", "Bob")

# Display initial board
game.display()

# Make some moves (Scholar's Mate)
game.make_move("e2", "e4")  # White pawn
game.display()

game.make_move("e7", "e5")  # Black pawn
game.display()

game.make_move("f1", "c4")  # White bishop
game.display()

game.make_move("b8", "c6")  # Black knight
game.display()

game.make_move("d1", "h5")  # White queen
game.display()

game.make_move("g8", "f6")  # Black knight (blunder!)
game.display()

game.make_move("h5", "f7")  # Checkmate!
game.display()

# Print move history
print("Move History:")
for move in game.get_move_history():
    print(move)

Key Design Decisions

All pieces share common attributes (color, has_moved) and behaviors (can be captured, belong to a player). Each piece type overrides get_possible_moves() with its unique movement pattern. This is classic polymorphism.
The Piece class only knows about its own movement rules. The Board class handles higher-level concerns like check detection and move legality. This follows Single Responsibility Principle.
Moves need metadata (captured piece, special moves, notation). The Move class also enables undo functionality and move history - following Command pattern principles.
Store complete board state in Move object or implement inverse operations. For each move type, you’d need to restore: piece positions, has_moved flags, captured pieces, and special move states.

Extension Points

Interview Extensions - Be ready to discuss:
  • AI Player: Minimax algorithm with alpha-beta pruning
  • Time Control: Add clock with increment
  • PGN Export: Standard chess notation format
  • Draw Conditions: 50-move rule, threefold repetition
  • Variants: Chess960, King of the Hill