Skip to content
intermediatePhase 50 · LLD Practice

Chess Game

Design a chess game with rules validation, turns, and win detection.

2h
0 problems
Topic Progress0%

Requirements and Scope

Functional Requirements

  • Two players take turns moving pieces on an 8x8 board
  • Each player starts with 16 pieces: 1 King, 1 Queen, 2 Rooks, 2 Bishops, 2 Knights, 8 Pawns
  • Pieces move according to specific rules (e.g., Rook moves in straight lines, Knight in L-shape)
  • Capturing an opponent's piece removes it from the board
  • The game ends when a player is checkmated (king under attack with no escape)
  • Support special moves: castling, en passant, pawn promotion
  • Players can resign or agree to a draw

Non-Functional Requirements

  • Modularity: Easy to add new piece types or rule variations
  • Testability: Each component independently testable
  • Extensibility: Support for timed games, AI opponents later
  • Performance: Move validation must be O(1) or O(board-size)

Core Entities

Entity Description
Player Two players, each controlling one side
Board 8x8 grid holding piece positions
Piece Abstract base for all chess pieces
Move Represents a single move (source, destination, special flags)
Game Orchestrates turns, validation, and game state

Game States

WAITING_FOR_PLAYERS -> IN_PROGRESS -> CHECK -> CHECKMATE -> GAME_OVER
                                          |-> STALEMATE -> GAME_OVER
                                          |-> RESIGNATION -> GAME_OVER
                                          |-> DRAW -> GAME_OVER

Board and Piece Design

Coordinate System

We use a simple (row, col) system where (0,0) is top-left (A8 in chess notation) and (7,7) is bottom-right (H1).

  A  B  C  D  E  F  G  H
8 [r][n][b][q][k][b][n][r]  Row 0 - Black pieces
7 [p][p][p][p][p][p][p][p]  Row 1 - Black pawns
6 [ ][ ][ ][ ][ ][ ][ ][ ]  Row 2
5 [ ][ ][ ][ ][ ][ ][ ][ ]  Row 3
4 [ ][ ][ ][ ][ ][ ][ ][ ]  Row 4
3 [ ][ ][ ][ ][ ][ ][ ][ ]  Row 5
2 [P][P][P][P][P][P][P][P]  Row 6 - White pawns
1 [R][N][B][Q][K][B][N][R]  Row 7 - White pieces

Piece Hierarchy

// Color enum
public enum Color {
    WHITE, BLACK;
    
    public Color opposite() {
        return this == WHITE ? BLACK : WHITE;
    }
}

// Position record for immutable coordinates
public record Position(int row, int col) {
    public boolean isValid() {
        return row >= 0 && row < 8 && col >= 0 && col < 8;
    }
}

// Abstract Piece base class
public abstract class Piece {
    protected final Color color;
    protected boolean hasMoved;
    
    public Piece(Color color) {
        this.color = color;
        this.hasMoved = false;
    }
    
    public Color getColor() { return color; }
    public boolean hasMoved() { return hasMoved; }
    public void markMoved() { this.hasMoved = true; }
    
    // Each piece type implements its own movement rules
    public abstract List<Position> getValidMoves(Position from, Board board);
    
    // Check if a move to 'to' is valid (doesn't leave own king in check)
    public boolean canMoveTo(Position from, Position to, Board board) {
        return getValidMoves(from, board).contains(to)
            && !board.wouldLeaveKingInCheck(from, to, this.color);
    }
    
    public abstract char getSymbol();
}

Concrete Piece Implementations

public class King extends Piece {
    public King(Color color) { super(color); }
    
    @Override
    public List<Position> getValidMoves(Position from, Board board) {
        List<Position> moves = new ArrayList<>();
        int[][] directions = {{-1,-1},{-1,0},{-1,1},{0,-1},{0,1},{1,-1},{1,0},{1,1}};
        
        for (int[] d : directions) {
            Position to = new Position(from.row() + d[0], from.col() + d[1]);
            if (to.isValid() && board.isEmptyOrEnemy(to, color)) {
                moves.add(to);
            }
        }
        
        // Castling moves
        if (!hasMoved) {
            addCastlingMoves(from, board, moves);
        }
        return moves;
    }
    
    private void addCastlingMoves(Position from, Board board, List<Position> moves) {
        // Kingside castling (O-O)
        if (canCastle(from, board, true)) {
            moves.add(new Position(from.row(), from.col() + 2));
        }
        // Queenside castling (O-O-O)
        if (canCastle(from, board, false)) {
            moves.add(new Position(from.row(), from.col() - 2));
        }
    }
    
    private boolean canCastle(Position from, Board board, boolean kingside) {
        int col = kingside ? 7 : 0;
        Piece rook = board.getPiece(new Position(from.row(), col));
        if (rook == null || rook.hasMoved() || !(rook instanceof Rook)) return false;
        
        // Check squares between king and rook are empty
        int step = kingside ? 1 : -1;
        for (int c = from.col() + step; c != col; c += step) {
            if (!board.isEmpty(new Position(from.row(), c))) return false;
        }
        
        // King must not be in check, and must not pass through check
        if (board.isSquareAttacked(from, color.opposite())) return false;
        for (int c = from.col() + step; c != from.col() + 2 * step; c += step) {
            if (board.isSquareAttacked(new Position(from.row(), c), color.opposite())) return false;
        }
        return true;
    }
    
    @Override
    public char getSymbol() { return color == Color.WHITE ? 'K' : 'k'; }
}

public class Queen extends Piece {
    public Queen(Color color) { super(color); }
    
    @Override
    public List<Position> getValidMoves(Position from, Board board) {
        List<Position> moves = new ArrayList<>();
        // Queen combines Rook and Bishop movement
        addLineMoves(from, board, moves, new int[][]{{-1,0},{1,0},{0,-1},{0,1}});
        addLineMoves(from, board, moves, new int[][]{{-1,-1},{-1,1},{1,-1},{1,1}});
        return moves;
    }
    
    private void addLineMoves(Position from, Board board, List<Position> moves, int[][] directions) {
        for (int[] d : directions) {
            int r = from.row() + d[0], c = from.col() + d[1];
            while (new Position(r, c).isValid()) {
                if (board.isEmpty(new Position(r, c))) {
                    moves.add(new Position(r, c));
                } else if (board.isEnemy(new Position(r, c), color)) {
                    moves.add(new Position(r, c));
                    break;
                } else {
                    break;
                }
                r += d[0];
                c += d[1];
            }
        }
    }
    
    @Override
    public char getSymbol() { return color == Color.WHITE ? 'Q' : 'q'; }
}

public class Rook extends Piece {
    public Rook(Color color) { super(color); }
    
    @Override
    public List<Position> getValidMoves(Position from, Board board) {
        List<Position> moves = new ArrayList<>();
        addLineMoves(from, board, moves, new int[][]{{-1,0},{1,0},{0,-1},{0,1}});
        return moves;
    }
    
    @Override
    public char getSymbol() { return color == Color.WHITE ? 'R' : 'r'; }
}

public class Bishop extends Piece {
    public Bishop(Color color) { super(color); }
    
    @Override
    public List<Position> getValidMoves(Position from, Board board) {
        List<Position> moves = new ArrayList<>();
        addLineMoves(from, board, moves, new int[][]{{-1,-1},{-1,1},{1,-1},{1,1}});
        return moves;
    }
    
    @Override
    public char getSymbol() { return color == Color.WHITE ? 'B' : 'b'; }
}

public class Knight extends Piece {
    public Knight(Color color) { super(color); }
    
    @Override
    public List<Position> getValidMoves(Position from, Board board) {
        List<Position> moves = new ArrayList<>();
        int[][] jumps = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}};
        
        for (int[] j : jumps) {
            Position to = new Position(from.row() + j[0], from.col() + j[1]);
            if (to.isValid() && board.isEmptyOrEnemy(to, color)) {
                moves.add(to);
            }
        }
        return moves;
    }
    
    @Override
    public char getSymbol() { return color == Color.WHITE ? 'N' : 'n'; }
}

public class Pawn extends Piece {
    public Pawn(Color color) { super(color); }
    
    @Override
    public List<Position> getValidMoves(Position from, Board board) {
        List<Position> moves = new ArrayList<>();
        int direction = color == Color.WHITE ? -1 : 1;
        int startRow = color == Color.WHITE ? 6 : 1;
        
        // Forward one
        Position oneAhead = new Position(from.row() + direction, from.col());
        if (oneAhead.isValid() && board.isEmpty(oneAhead)) {
            moves.add(oneAhead);
            // Forward two from starting position
            Position twoAhead = new Position(from.row() + 2 * direction, from.col());
            if (from.row() == startRow && board.isEmpty(twoAhead)) {
                moves.add(twoAhead);
            }
        }
        
        // Diagonal captures
        for (int dc : new int[]{-1, 1}) {
            Position diag = new Position(from.row() + direction, from.col() + dc);
            if (diag.isValid() && board.isEnemy(diag, color)) {
                moves.add(diag);
            }
        }
        
        // En passant
        Position enPassant = board.getEnPassantTarget();
        if (enPassant != null && enPassant.row() == from.row() + direction
                && Math.abs(enPassant.col() - from.col()) == 1) {
            moves.add(enPassant);
        }
        
        return moves;
    }
    
    @Override
    public char getSymbol() { return color == Color.WHITE ? 'P' : 'p'; }
}

Move Validation and Game Flow

Board Class

public class Board {
    private final Piece[][] grid;
    private Position enPassantTarget;
    
    public Board() {
        grid = new Piece[8][8];
        initialize();
    }
    
    private void initialize() {
        // Black pieces (row 0)
        grid[0][0] = new Rook(Color.BLACK);
        grid[0][1] = new Knight(Color.BLACK);
        grid[0][2] = new Bishop(Color.BLACK);
        grid[0][3] = new Queen(Color.BLACK);
        grid[0][4] = new King(Color.BLACK);
        grid[0][5] = new Bishop(Color.BLACK);
        grid[0][6] = new Knight(Color.BLACK);
        grid[0][7] = new Rook(Color.BLACK);
        for (int c = 0; c < 8; c++) grid[1][c] = new Pawn(Color.BLACK);
        
        // White pieces (row 7)
        grid[7][0] = new Rook(Color.WHITE);
        grid[7][1] = new Knight(Color.WHITE);
        grid[7][2] = new Bishop(Color.WHITE);
        grid[7][3] = new Queen(Color.WHITE);
        grid[7][4] = new King(Color.WHITE);
        grid[7][5] = new Bishop(Color.WHITE);
        grid[7][6] = new Knight(Color.WHITE);
        grid[7][7] = new Rook(Color.WHITE);
        for (int c = 0; c < 8; c++) grid[6][c] = new Pawn(Color.WHITE);
    }
    
    public Piece getPiece(Position pos) {
        return grid[pos.row()][pos.col()];
    }
    
    public void setPiece(Position pos, Piece piece) {
        grid[pos.row()][pos.col()] = piece;
    }
    
    public boolean isEmpty(Position pos) {
        return getPiece(pos) == null;
    }
    
    public boolean isEnemy(Position pos, Color myColor) {
        Piece p = getPiece(pos);
        return p != null && p.getColor() != myColor;
    }
    
    public boolean isEmptyOrEnemy(Position pos, Color myColor) {
        return isEmpty(pos) || isEnemy(pos, myColor);
    }
    
    public Position getEnPassantTarget() { return enPassantTarget; }
    public void setEnPassantTarget(Position pos) { this.enPassantTarget = pos; }
    
    // Find king position for a given color
    public Position findKing(Color color) {
        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                Piece p = grid[r][c];
                if (p instanceof King && p.getColor() == color) {
                    return new Position(r, c);
                }
            }
        }
        throw new IllegalStateException("King not found for " + color);
    }
    
    // Check if a square is attacked by any enemy piece
    public boolean isSquareAttacked(Position pos, Color attackerColor) {
        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                Piece p = grid[r][c];
                if (p != null && p.getColor() == attackerColor) {
                    if (p.getValidMoves(new Position(r, c), this).contains(pos)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }
    
    // Check if making a move would leave own king in check
    public boolean wouldLeaveKingInCheck(Position from, Position to, Color myColor) {
        // Simulate the move
        Piece movedPiece = getPiece(from);
        Piece captured = getPiece(to);
        
        setPiece(to, movedPiece);
        setPiece(from, null);
        
        Position kingPos = findKing(myColor);
        boolean inCheck = isSquareAttacked(kingPos, myColor.opposite());
        
        // Undo the move
        setPiece(from, movedPiece);
        setPiece(to, captured);
        
        return inCheck;
    }
    
    // Execute a move on the board
    public void executeMove(Move move) {
        Piece piece = getPiece(move.from());
        Piece captured = getPiece(move.to());
        
        // Handle castling rook movement
        if (piece instanceof King && Math.abs(move.to().col() - move.from().col()) == 2) {
            boolean kingside = move.to().col() > move.from().col();
            int rookFromCol = kingside ? 7 : 0;
            int rookToCol = kingside ? 5 : 3;
            Piece rook = getPiece(new Position(move.from().row(), rookFromCol));
            setPiece(new Position(move.from().row(), rookToCol), rook);
            setPiece(new Position(move.from().row(), rookFromCol), null);
            rook.markMoved();
        }
        
        // Handle en passant capture
        if (piece instanceof Pawn && move.to().equals(enPassantTarget)) {
            int capturedRow = move.from().row();
            setPiece(new Position(capturedRow, move.to().col()), null);
        }
        
        // Set en passant target for next move
        if (piece instanceof Pawn && Math.abs(move.to().row() - move.from().row()) == 2) {
            enPassantTarget = new Position(
                (move.from().row() + move.to().row()) / 2,
                move.from().col()
            );
        } else {
            enPassantTarget = null;
        }
        
        // Handle pawn promotion
        if (piece instanceof Pawn && (move.to().row() == 0 || move.to().row() == 7)) {
            piece = move.promotionPiece() != null ? move.promotionPiece() : new Queen(piece.getColor());
        }
        
        setPiece(move.to(), piece);
        setPiece(move.from(), null);
        piece.markMoved();
    }
    
    // Deep copy for simulation
    public Board copy() {
        Board copy = new Board();
        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                copy.grid[r][c] = this.grid[r][c];
            }
        }
        copy.enPassantTarget = this.enPassantTarget;
        return copy;
    }
}

Game Class

public record Move(Position from, Position to, Piece promotionPiece) {}

public enum GameStatus {
    IN_PROGRESS, CHECK, CHECKMATE, STALEMATE, RESIGNATION, DRAW
}

public class Game {
    private final Board board;
    private final Player player1; // White
    private final Player player2; // Black
    private Color currentTurn;
    private GameStatus status;
    private final List<Move> moveHistory;
    
    public Game(Player player1, Player player2) {
        this.board = new Board();
        this.player1 = player1;
        this.player2 = player2;
        this.currentTurn = Color.WHITE;
        this.status = GameStatus.IN_PROGRESS;
        this.moveHistory = new ArrayList<>();
    }
    
    public boolean makeMove(Position from, Position to, Piece promotionPiece) {
        if (status != GameStatus.IN_PROGRESS && status != GameStatus.CHECK) {
            return false; // Game is over
        }
        
        Piece piece = board.getPiece(from);
        if (piece == null || piece.getColor() != currentTurn) {
            return false; // Not the player's piece
        }
        
        // Validate move
        if (!piece.canMoveTo(from, to, board)) {
            return false;
        }
        
        // Execute move
        Move move = new Move(from, to, promotionPiece);
        board.executeMove(move);
        moveHistory.add(move);
        
        // Switch turns
        currentTurn = currentTurn.opposite();
        
        // Update game status
        updateStatus();
        return true;
    }
    
    private void updateStatus() {
        Position kingPos = board.findKing(currentTurn);
        boolean inCheck = board.isSquareAttacked(kingPos, currentTurn.opposite());
        boolean hasLegalMoves = hasAnyLegalMove(currentTurn);
        
        if (inCheck && !hasLegalMoves) {
            status = GameStatus.CHECKMATE;
        } else if (!inCheck && !hasLegalMoves) {
            status = GameStatus.STALEMATE;
        } else if (inCheck) {
            status = GameStatus.CHECK;
        } else {
            status = GameStatus.IN_PROGRESS;
        }
    }
    
    private boolean hasAnyLegalMove(Color color) {
        for (int r = 0; r < 8; r++) {
            for (int c = 0; c < 8; c++) {
                Piece p = board.getPiece(new Position(r, c));
                if (p != null && p.getColor() == color) {
                    Position from = new Position(r, c);
                    for (Position to : p.getValidMoves(from, board)) {
                        if (p.canMoveTo(from, to, board)) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
    
    public void resign(Color playerColor) {
        status = GameStatus.RESIGNATION;
    }
}

Follow-ups and Extensions

Pawn Promotion Handler

public class PromotionHandler {
    public static Piece promote(Pawn pawn, String choice) {
        return switch (choice.toLowerCase()) {
            case "queen" -> new Queen(pawn.getColor());
            case "rook" -> new Rook(pawn.getColor());
            case "bishop" -> new Bishop(pawn.getColor());
            case "knight" -> new Knight(pawn.getColor());
            default -> new Queen(pawn.getColor()); // Default to queen
        };
    }
}

Move History with Undo

public class MoveHistory {
    private final Deque<GameState> history = new ArrayDeque<>();
    
    public void save(GameState state) {
        history.push(state);
    }
    
    public GameState undo() {
        if (history.isEmpty()) return null;
        return history.pop();
    }
}

public record GameState(Board board, Color currentTurn, GameStatus status) {}

Timed Chess (Chess Clock)

public class ChessClock {
    private final long whiteTimeMs;
    private final long blackTimeMs;
    private final long incrementMs;
    private Color activeColor;
    private long lastTickMs;
    
    public ChessClock(long timeMs, long incrementMs) {
        this.whiteTimeMs = timeMs;
        this.blackTimeMs = timeMs;
        this.incrementMs = incrementMs;
        this.activeColor = Color.WHITE;
    }
    
    public void switchTurn() {
        long now = System.currentTimeMillis();
        long elapsed = now - lastTickMs;
        // Deduct time and add increment
        lastTickMs = now;
        activeColor = activeColor.opposite();
    }
    
    public boolean isTimeUp(Color color) {
        return getTimeRemaining(color) <= 0;
    }
}

Observer Pattern for UI Updates

public interface GameObserver {
    void onMoveMade(Move move, Piece captured);
    void onCheck(Color kingColor);
    void onCheckmate(Color loser);
    void onGameOver(GameStatus status);
}

public class Game {
    private final List<GameObserver> observers = new ArrayList<>();
    
    public void addObserver(GameObserver observer) {
        observers.add(observer);
    }
    
    private void notifyMove(Move move, Piece captured) {
        for (GameObserver o : observers) o.onMoveMade(move, captured);
    }
}

Design Patterns Used

Pattern Usage
Strategy Each piece encapsulates its own movement validation
Composite Board contains Pieces in a grid structure
Observer UI components subscribe to game state changes
Command Moves as objects enabling undo/redo
State Game status transitions (IN_PROGRESS -> CHECK -> CHECKMATE)

Complexity Analysis

Operation Time Complexity
Get valid moves for piece O(n) where n = board size
Check if king in attack O(n²) worst case
Execute move O(1)
Check for checkmate O(n² × n) = O(n³)
Find king position O(n²)

Interview Tips

  • Start by defining the core entities and their relationships
  • Discuss the Strategy pattern for piece movement early
  • Mention special moves (castling, en passant, promotion) as follow-ups
  • Be ready to discuss check/checkmate detection algorithms
  • Consider thread safety if discussing multiplayer/networked games
  • Mention how you would test each component independently

Practice Problems

0/3solved
Design Chess Game System

Design a scalable Chess Game system. Cover high-level architecture, data model, and API design.

Solution
// Complete system design:
// - Functional + Non-functional requirements
// - Capacity estimation
// - Data model (SQL/NoSQL choice)
// - API endpoints
// - Component architecture
// - Scaling strategy
// - Monitoring & reliability
Chess Game Scaling

How would you scale Chess Game to handle 10x the current load? Identify bottlenecks and solutions.

Solution
// Scaling approach:
// 1. Load balancing
// 2. Database sharding/replication
// 3. Cache layer (Redis)
// 4. CDN for static assets
// 5. Async processing (queues)
// 6. Microservices decomposition
Chess Game Failure Modes

Analyze potential failure modes for Chess Game and design mitigation strategies.

Solution
// Failure mitigation:
// 1. Redundancy (multi-AZ)
// 2. Circuit breakers
// 3. Retry with backoff
// 4. Dead letter queues
// 5. Health checks
// 6. Graceful degradation

Quiz

1. What design pattern is most appropriate for implementing different movement rules for each chess piece?

Question 1 options

2. How do you detect checkmate?

Question 2 options

3. What is en passant in chess?

Question 3 options

4. In the code, why do we simulate moves when checking for check?

Question 4 options

5. What is the time complexity of checking for checkmate?

Question 5 options

Flashcards

Question

What is the Strategy Pattern and how is it used in Chess?

Answer

Strategy Pattern defines a family of algorithms (piece movement rules) and makes them interchangeable. Each Piece subclass implements getValidMoves() differently, allowing the Board to validate moves polymorphically without knowing the specific piece type.

Question

How does castling work in code?

Answer

Castling involves moving the King two squares toward a Rook, and the Rook jumping to the other side. Conditions: Neither piece has moved, squares between them are empty, King is not in check, and King doesn't pass through check. Implemented as a special case in King's getValidMoves().

Question

Why use a Board.copy() method?

Answer

Board.copy() creates a deep copy for simulating moves without affecting the actual game state. This is essential for: 1) Checking if a move leaves own king in check, 2) AI move evaluation, 3) Undo functionality.

Question

What is the en passant target?

Answer

The en passant target is set when a pawn moves two squares forward from its starting position. It stores the position the capturing pawn can move to (the square the captured pawn 'passed through'). It's cleared after one move if not used.

Question

How to handle pawn promotion?

Answer

When a Pawn reaches the opposite end of the board (row 0 for White, row 7 for Black), it must be promoted to a Queen, Rook, Bishop, or Knight. In code, this is detected in executeMove() and the Pawn is replaced with the chosen piece type.

Revision Notes

Key Takeaways

  • 1.Use Strategy pattern to encapsulate piece-specific movement rules
  • 2.Always validate that moves don't leave your own king in check
  • 3.Special moves (castling, en passant, promotion) are edge cases that need careful handling
  • 4.Board simulation (copy + modify + check) is essential for move validation
  • 5.Game state machine: IN_PROGRESS -> CHECK -> CHECKMATE/STALEMATE
  • 6.Consider Observer pattern for UI updates and event handling

Interview Tips

  • Start with the core entities and their relationships before diving into logic
  • Explain the Strategy pattern for piece movement early in the discussion
  • Mention special moves as follow-ups to show depth of knowledge
  • Be prepared to discuss time complexity of check/checkmate detection
  • Consider thread safety if discussing multiplayer/networked extensions
  • Show how you would test each component independently

Cheat Sheet

Chess Game LLD Cheat Sheet

Core Entities: Player, Board, Piece (abstract), Move, Game

Piece Hierarchy:

Piece (abstract)
├── King    - moves 1 square any direction, castling
├── Queen   - combines Rook + Bishop movement
├── Rook    - moves in straight lines (rows/cols)
├── Bishop  - moves diagonally
├── Knight  - L-shaped jumps (2+1)
└── Pawn    - forward 1 (or 2 from start), diagonal capture

Key Design Patterns:

  • Strategy: Each piece validates its own moves
  • Observer: UI subscribes to game events
  • Command: Moves as objects for undo/redo
  • State: Game status transitions

Special Moves:

  • Castling: King 2 squares + Rook jumps (conditions apply)
  • En Passant: Pawn captures adjacent pawn that just moved 2 squares
  • Promotion: Pawn reaches last rank, becomes Queen/Rook/Bishop/Knight

Check Detection: Simulate move -> check if king is attacked -> undo move

Checkmate: King in check + no legal moves exist

Stalemate: Not in check + no legal moves (draw)