Low-Level Design: Chess Game (OOP Interview)

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: GameStatus enum 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

Scroll to Top