Course contents

LLD problem — Chess (piece movement)

Sign in to run this lab and save your progress

Learn

Before you start

Chess is the biggest problem in this module — and even here, we've scoped it down to piece movement + captures. The FULL game (check, checkmate, castling, en-passant, promotion, draws) is a semester of code, not a lesson. That's fine — the DESIGN skill is in the piece hierarchy, not in memorizing rules.

The two patterns you'll want

  1. Factory (or registry) for building pieces — six kinds, each with its own class. Board.place(kind, ...) looks up the class by kind and builds one.

  2. Polymorphism for move rules — each Piece subclass owns its can_move(board, from, to) method. Board doesn't contain the movement rules; it delegates.

If Board contained six if/elif branches for the six piece types, adding a new piece (say, Amazon = Queen + Knight) would mean editing Board. With polymorphism, it's a new class.

Say both in design.md.

The sliding-piece subtlety

Bishop, Rook, and Queen slide along lines. Two things must both hold for a valid slide:

  1. The move is IN one of the piece's directions (Bishop: pure diagonal; Rook: pure horizontal / vertical; Queen: either).
  2. Every square BETWEEN from and to is empty.

Common bug: checking that the destination is empty but not the in-between squares. Then a bishop can teleport past friendly pieces.

The pattern to use: compute step = (sign(dr), sign(dc)), then walk from (from + step) to (to - step) checking that each square is empty.

Pawns are weird

Pawns are the only piece where:

  • The direction of movement depends on color
  • Forward movement can't capture
  • Sideways movement (diagonal) requires a piece to capture

In a full chess implementation they get even weirder (en-passant, promotion, initial two-square move). We've cut those.

Convention: white moves "toward higher rows", black "toward lower rows". Standard chess UI shows white at the bottom (row 7-ish), but we don't take a stand on orientation — we just pin the direction rule and stick to it.

The rules discipline

  • Never two pieces on one square. Enforced by place() checking at() is None first; move() removes the destination piece (if any) before placing.
  • Captures remove the captured piece exactly once. Enforced by the single delete-in-move path.
  • Each piece's rules live in one place. Enforced by putting can_move on the piece subclass and having Board just call it.

What we chose NOT to model

Naming these in your design.md's Trade-offs section shows maturity:

  • Check / checkmate — needs "does any enemy attack the king?" — a full-board scan per move
  • Castling — needs "king hasn't moved, rook hasn't moved, no squares in between attacked"
  • En-passant — pawn state machine, per-color, tracked per-move
  • Promotion — pawn reaches back rank → replaced by the piece the player chose
  • Turn order — track whose turn it is, alternate

Each is a big addition. Saying "these are cut for scope, but the design supports adding them" is stronger than silently ignoring them.

Your task

Chess is enormous. Modeling every rule (check, checkmate, castling, en-passant, promotion, draw claims, threefold repetition, 50-move rule…) is a book. We scope down to piece movement + move validity on a board. Enough to demonstrate the design, small enough to test.

The scoped problem

A Board(size=8) holds pieces on a grid.

Pieces:

  • Each piece has a color ("white" / "black") and a kind ("pawn", "knight", "bishop", "rook", "queen", "king").

Operations:

  • Board(size=8) — starts empty.

  • place(kind, color, row, col) — put a piece. Raises if occupied or out of bounds. Duplicates allowed at different positions.

  • at(row, col) — returns the piece dict {"kind":..., "color":...} or None.

  • is_valid_move(from_pos, to_pos) — return bool.

    • From must have a piece.
    • Move must be geometrically valid for that piece's kind.
    • Destination must be empty OR contain an opponent's piece (capture).
    • Sliding pieces (bishop, rook, queen) can't jump over other pieces.
    • Knight jumps.
    • Pawn: white moves DOWN (rows increase); black moves UP (rows decrease). Simple 1-step forward. Diagonal capture only if opponent present. No double-move-on-first-turn. No en-passant. No promotion.
    • King: 1 square any direction. No castling.
  • move(from_pos, to_pos) — performs the move. Raises ValueError if is_valid_move returns False. On capture, removes the captured piece.

Coordinates: (row, col), 0-indexed, top-left is (0,0).

Requirement-gathering questions

  1. Check / checkmate? — Out of scope.
  2. Whose turn is it? — Not tracked. Any move is validated purely on piece rules.
  3. Castling, en passant, promotion? — Out of scope.
  4. Draw / stalemate? — Out of scope.
  5. Move notation (algebraic)? — Not for MVP. Use (row, col) tuples.

Design goals

  • Adding a new piece type should be a manageable change.
  • Each piece's move rules live in ONE place — not scattered across Board methods.
  • Invariants: never two pieces on one square; captures remove the captured piece exactly once.

Your task

Public API:

from main import Board

b = Board(size=8)
b.place("knight", "white", 4, 4)
b.at(4, 4)                          # {"kind": "knight", "color": "white"}
b.is_valid_move((4, 4), (6, 5))     # True (knight jump)
b.is_valid_move((4, 4), (5, 5))     # False (knight can't move like king)
b.place("pawn", "black", 6, 5)
b.move((4, 4), (6, 5))              # captures black pawn
b.at(4, 4)                          # None
b.at(6, 5)                          # white knight

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
11-chess-starter