Design a Chess Game
You'll learn to
- -Model the board, pieces, and moves so each piece type owns its own legal-move logic via polymorphism
- -Layer in check/checkmate detection, move history (Command, for undo), and turn state (State) cleanly
Chess is the most demanding classic LLD prompt on this list, precisely because it has real domain complexity (six piece types with genuinely different movement rules, check/checkmate, special moves) that a shallow design cannot hand-wave away. It rewards someone who scopes deliberately rather than trying to model every chess rule in 45 minutes.
Step 1: Scope the Problem Out Loud
Before designing, name what is in and out of scope: standard piece movement and capture, check and checkmate detection - yes. Castling, en passant, and pawn promotion - mention them explicitly as known special cases you would add given more time, rather than silently ignoring them or trying to cram all of them in. This scoping move itself is a strong interview signal, echoing the very first chapter of this course.
Step 2: Piece Hierarchy - Polymorphic Move Legality
class Color(Enum):
WHITE = "white"
BLACK = "black"
class Piece(ABC):
def __init__(self, color: Color):
self.color = color
@abstractmethod
def is_legal_move(self, board: "Board", start: "Position", end: "Position") -> bool: ...
class Bishop(Piece):
def is_legal_move(self, board, start, end) -> bool:
row_diff = abs(end.row - start.row)
col_diff = abs(end.col - start.col)
if row_diff != col_diff: # must move diagonally
return False
return board.is_path_clear(start, end)
class Knight(Piece):
def is_legal_move(self, board, start, end) -> bool:
row_diff, col_diff = abs(end.row - start.row), abs(end.col - start.col)
return (row_diff, col_diff) in [(1, 2), (2, 1)] # knights jump, no path check neededThis is the polymorphism payoff from the OOP Foundations module made concrete: Board.move_piece() calls piece.is_legal_move() once, with zero knowledge of or branching on which of the six piece types it is calling - each piece class owns exactly the movement logic that applies to it, and adding a piece variant (a chess variant with a custom piece) means one new class, touching nothing else.
Step 3: The Board and Move Execution
class Board:
def __init__(self):
self._grid: dict[Position, Piece] = self._initial_setup()
def is_path_clear(self, start: "Position", end: "Position") -> bool:
for pos in positions_between(start, end): # every square strictly between start and end
if pos in self._grid:
return False
return True
def move_piece(self, start: "Position", end: "Position") -> "Piece | None":
piece = self._grid[start]
target = self._grid.get(end)
if target is not None and target.color == piece.color:
raise IllegalMoveError() # can't capture your own piece - checked once here,
if not piece.is_legal_move(self, start, end): # not duplicated across all six piece classes
raise IllegalMoveError()
captured = target
self._grid[end] = piece
del self._grid[start]
return capturedNotice the same-color check lives in move_piece(), not in every individual is_legal_move(). Whether a square holds a friendly piece is a rule every piece type shares equally, so it belongs in the one place that applies to all of them - each is_legal_move() only needs to answer "does this piece move this way," not "is this specific destination allowed," which keeps the six piece classes from each duplicating the identical own-piece check.
Step 4: Turn Order as State, Move History as Command
Two patterns from earlier modules combine cleanly here. Whose turn it is, and whether the game is ongoing, in check, or over, is naturally a State machine (WhiteTurnState, BlackTurnState, CheckmateState), each restricting which actions are valid. Each executed move is naturally a Command object (piece, start, end, captured piece) pushed onto a history stack, giving undo (a common chess-app feature) for free using exactly the CommandHistory shape from the Command pattern chapter.
class MoveCommand(Command):
def __init__(self, board: Board, start: "Position", end: "Position"):
self._board = board
self._start = start
self._end = end
self._captured: Piece | None = None
def execute(self) -> None:
self._captured = self._board.move_piece(self._start, self._end)
def undo(self) -> None:
self._board._grid[self._start] = self._board._grid.pop(self._end)
if self._captured:
self._board._grid[self._end] = self._capturedStep 5: Check Detection, Scoped Honestly
Checking whether a king is in check is: for every opposing piece, ask "is a move to the king's square legal for this piece" - reusing the exact is_legal_move() method already defined on every Piece, rather than writing separate check-detection logic per piece type. Checkmate then layers on top: the king is in check, and no legal move by the current player resolves it - a real but bounded amount of additional complexity worth naming rather than fully implementing live if time is short.
On a problem this large, explicitly saying "given the time, I'll implement standard movement and check detection fully, and describe rather than code castling/en passant/promotion" is a stronger signal than silently running out of time mid-way through an unscoped attempt.
Interview Signal is part of Pro
See a real weak answer next to a real strong one for this exact topic.
Quiz is part of Pro
Test what you just read with a short quiz, and bank the XP.