LLD: Chess
Board, pieces, moves, and turns — polymorphic pieces that know how they move, a game that knows the rules around them, and moves as objects you can undo.
Big idea
Chess is a showcase for polymorphism: every piece answers "where can you move from here?" in its own way, so the game never switches on piece type. Around the pieces sits a Game that enforces turn order and the rules no single piece can know (you can't leave your own king in check), and moves are objects (Command) so history, undo, and notation come for free.
Requirements
Functional
- 8×8 board, two players, standard starting position.
- Each piece type has its movement rules; sliding pieces are blocked by others; captures remove the opponent's piece.
- Players alternate turns; illegal moves are rejected with a reason.
- A move that leaves your own king in check is illegal; detect check.
- Move history with undo.
Out of scope (name them): castling, en passant, promotion choice, checkmate/stalemate search, clocks. Each slots in later without restructuring.
Entities
Game (turns, rules, history), Board (8×8 grid, lookups), Piece (abstract: colour, moves(board, square)), King, Queen, Rook, Bishop, Knight, Pawn, Move (from, to, captured piece), Player, Color.
Think of it like
A referee and a team of specialists. Each specialist (piece) knows exactly how they're allowed to move and says "from here I could go to these squares". The referee (game) checks whose turn it is and that nobody's move leaves their own king exposed, and writes every move in the scorebook (history).
Class diagram
Key flow: validating a move
Code skeleton
FILES = "abcdefgh"
def sq(name): return FILES.index(name[0]), int(name[1]) - 1
def name(c, r): return f"{FILES[c]}{r + 1}"
class Piece:
symbol = "?"
def __init__(self, color): self.color = color
def moves(self, board, pos): raise NotImplementedError
def __str__(self): return self.symbol.upper() if self.color == "white" else self.symbol
class SlidingPiece(Piece):
directions = ()
def moves(self, board, pos):
out = []
for dc, dr in self.directions:
c, r = pos[0] + dc, pos[1] + dr
while 0 <= c < 8 and 0 <= r < 8:
other = board.get((c, r))
if other is None:
out.append((c, r))
else:
if other.color != self.color: out.append((c, r))
break # blocked
c, r = c + dc, r + dr
return out
class Rook(SlidingPiece): symbol, directions = "r", ((1, 0), (-1, 0), (0, 1), (0, -1))
class Bishop(SlidingPiece): symbol, directions = "b", ((1, 1), (1, -1), (-1, 1), (-1, -1))
class Queen(SlidingPiece): symbol, directions = "q", Rook.directions + Bishop.directions
class Knight(Piece):
symbol = "n"
def moves(self, board, pos):
jumps = [(1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2)]
targets = [(pos[0] + a, pos[1] + b) for a, b in jumps]
return [t for t in targets if 0 <= t[0] < 8 and 0 <= t[1] < 8
and (board.get(t) is None or board[t].color != self.color)]
class King(Piece):
symbol = "k"
def moves(self, board, pos):
return [(pos[0] + a, pos[1] + b) for a in (-1, 0, 1) for b in (-1, 0, 1) if (a or b)
and 0 <= pos[0] + a < 8 and 0 <= pos[1] + b < 8
and (board.get((pos[0] + a, pos[1] + b)) is None or board[(pos[0] + a, pos[1] + b)].color != self.color)]
class Pawn(Piece):
symbol = "p"
def moves(self, board, pos):
step = 1 if self.color == "white" else -1
c, r = pos
out = []
if board.get((c, r + step)) is None:
out.append((c, r + step))
start = 1 if self.color == "white" else 6
if r == start and board.get((c, r + 2 * step)) is None:
out.append((c, r + 2 * step))
for dc in (-1, 1):
t = board.get((c + dc, r + step))
if t is not None and t.color != self.color:
out.append((c + dc, r + step))
return out
class Game:
def __init__(self, setup):
self.board = {sq(s): p for s, p in setup.items()}
self.turn, self.history = "white", []
def in_check(self, color):
king = next(p for p, piece in self.board.items() if isinstance(piece, King) and piece.color == color)
return any(king in piece.moves(self.board, p) for p, piece in list(self.board.items()) if piece.color != color)
def move(self, src, dst):
s, d = sq(src), sq(dst)
piece = self.board.get(s)
if piece is None or piece.color != self.turn:
return f"{src}-{dst}: not your piece"
if d not in piece.moves(self.board, s):
return f"{src}-{dst}: illegal for {type(piece).__name__}"
captured = self.board.pop(d, None) # apply the move (a Move object, in essence)
self.board[d] = self.board.pop(s)
if self.in_check(self.turn): # rule no single piece knows
self.board[s] = self.board.pop(d)
if captured: self.board[d] = captured
return f"{src}-{dst}: leaves your king in check"
self.history.append((s, d, captured))
self.turn = "black" if self.turn == "white" else "white"
note = " (check)" if self.in_check(self.turn) else ""
return f"{src}-{dst}{' x' + str(captured) if captured else ''}{note}"
def undo(self):
s, d, captured = self.history.pop()
self.board[s] = self.board.pop(d)
if captured: self.board[d] = captured
self.turn = "black" if self.turn == "white" else "white"
game = Game({"e1": King("white"), "d1": Queen("white"), "e2": Pawn("white"), "g1": Knight("white"),
"e8": King("black"), "e7": Rook("black"), "d7": Pawn("black")})
for src, dst in [("e2", "e4"), ("e7", "e4"), ("g1", "g3"), ("d1", "d2"), ("g1", "e2"),
("d7", "d6"), ("d1", "d6"), ("e8", "e7"), ("e8", "f7")]:
print(game.move(src, dst))
game.undo()
print("after undo, to move:", game.turn)output
e2-e4
e7-e4 xP (check)
g1-g3: illegal for Knight
d1-d2: leaves your king in check
g1-e2
d7-d6
d1-d6 xp
e8-e7: leaves your king in check
e8-f7
after undo, to move: blackRun and edit it in the app.
Every moves() method is small and testable alone, and adding a fairy-chess piece is one new class. The "can't expose your king" rule lives in Game, because it needs the whole board.
Watch it
Where does the "a bishop moves diagonally until blocked" rule belong?
Why is "a move may not leave your own king in check" enforced by Game rather than by each piece?
Extensions and trade-offs
- Special moves — castling, en passant, and promotion need history or state (has the king moved?), so they're added in
Gamewith help from pieces. - Checkmate/stalemate — "no legal move exists" = try every piece's moves through the same validation.
- Moves as objects — a
Moveclass withexecute/undo(Command) supports replay, notation export, and networked play. - Performance — engines use bitboards; an LLD interview cares about clarity, so say that and move on.
- Players — human, AI, remote: a
Playerinterface withchoose_move(game)(Strategy).
Key terms
- Sliding piece — A piece (rook, bishop, queen) that moves any distance along a line until blocked.
- Pseudo-legal move — A move allowed by the piece's pattern, before checking the king's safety.
- Check — A position where a king is attacked by an opponent's piece.