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
| Category | Question | Typical Scope |
|---|---|---|
| Players | Human vs Human? Human vs AI? | Usually both players human for LLD |
| Rules | Which special moves to include? | Castling, en passant often in scope |
| Features | Undo/redo support? | Often asked as extension |
| UI | GUI or console-based? | Console for LLD interviews |
| Timer | Chess clock functionality? | Usually out of scope |
| Variants | Fischer 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
| Entity | Responsibilities | Pattern Used |
|---|---|---|
Piece (abstract) | Define movement interface | Template Method |
King, Queen, etc. | Implement specific movements | Polymorphism |
Board | Track piece positions, validate moves | - |
Game | Manage turns, check game status | State Pattern (optional) |
Move | Encapsulate move action for undo/redo | Command Pattern |
MoveHistory | Store moves for undo | Memento Pattern |
📐 Step 3: Class Diagram
Copy
┌─────────────────────────────────────────────────────────────┐
│ 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
Copy
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
Copy
@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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
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
Copy
# 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
Why Abstract Piece Class?
Why Abstract Piece Class?
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.Why Separate Board Validation?
Why Separate Board Validation?
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.
Why Move Object?
Why Move Object?
Moves need metadata (captured piece, special moves, notation). The Move class also enables undo functionality and move history - following Command pattern principles.
How Would You Add Undo?
How Would You Add Undo?
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