Back to Home
Live Project · Chess Pieces

Chess Pieces —
Each One Knows
Its Own Moves.

A rook, a bishop and a knight are all Piece objects. The board loop asks each one for its legal_moves() — and every piece answers in its own way. That is polymorphism, played out on a chessboard.

This live project is a companion to the Polymorphism in Python guide. You'll see six subclasses override the same two methods, run the real Python code, and play with a live board where each piece moves according to its own rules.

📅 October 20, 2026 ⏱ 20 min read 🎓 Intermediate 🐍 Python 3.11+
Back to Home Play the Board See the Code Polymorphism Guide

Progress Tracker

Tick off each milestone as you complete it. Progress is saved in this browser.

🎯 Your Journey Through Chess Pieces

0%

    Why Chess Is a Perfect Polymorphism Example

    ♟️ One Question, Many Answers

    A chess engine needs to know: "Where can this piece legally move?" There are six kinds of pieces, and each has a completely different rule. A naive engine writes one giant if / elif block on piece type, repeated everywhere moves are needed.

    A polymorphic engine writes the question once — piece.legal_moves(board) — and gives each piece class its own answer. Adding a new piece (say, a fairy-chess "Princess") means writing one class, not hunting down every if ladder.

    This is the same idea behind the Polymorphism guide, but grounded in a game you already know.

    6 piece classes, one interface Runnable Python implementation Live interactive board Playable mini-game Quiz + progress tracker

    The Two Methods Every Piece Shares

    Every class in our engine inherits from a base Piece that declares the interface. The board code only ever calls these:

    MethodWhat it returnsWho uses it
    legal_moves(board)List of (row, col) destinations the piece can reachMove validation, highlighting, AI search
    symbol()A Unicode character used to render the pieceThe board renderer

    The payoff: the board code never asks "what kind of piece is this?" It just calls piece.legal_moves(board). Every piece answers for itself.

    Meet the Pieces

    Each of these is a subclass of Piece. Each overrides legal_moves() with its own rule and symbol() with its own glyph.

    ♔
    King

    One square in any direction.

    ♕
    Queen

    Any distance, straight or diagonal.

    ♖
    Rook

    Any distance, straight lines only.

    ♗
    Bishop

    Any distance, diagonals only.

    ♘
    Knight

    L-shape: two one way, one the other. Jumps.

    ♙
    Pawn

    Forward one (or two from start), diagonally to capture.

    How Each Override Differs

    PieceDirection setDistanceSpecial
    KingAll 8 directionsExactly 1—
    QueenAll 8 directions1…7 (slide)Stops at blockers
    Rook4 straight directions1…7 (slide)Stops at blockers
    Bishop4 diagonal directions1…7 (slide)Stops at blockers
    Knight8 L-shaped offsetsExactly 1 hopIgnores blockers
    PawnForward (and diagonals to capture)1 or 2Direction depends on color

    Notice: Rook, Bishop and Queen all "slide" in some direction set. They share the same _slide() helper — but each calls it with a different list of directions. That's overriding plus code reuse: different what, shared how.

    The Python Implementation

    This is the complete engine behind the board below. Read it once, then press Run to execute it right here in your browser.

    The Chess Piece Engine

    One base class, six subclasses, one shared _slide helper. The main loop asks every piece on the board for its moves — and nothing ever checks a piece's type.

    class Piece:
        def __init__(self, color, row, col):
            self.color = color
            self.row = row
            self.col = col
    
        def legal_moves(self, board):
            raise NotImplementedError("Subclasses must implement legal_moves()")
    
        def symbol(self):
            raise NotImplementedError("Subclasses must implement symbol()")
    
        def _inside(self, r, c):
            return 0 <= r < 8 and 0 <= c < 8
    
        def _can_land(self, board, r, c):
            target = board.get((r, c))
            return target is None or target.color != self.color
    
        def _slide(self, board, directions):
            moves = []
            for dr, dc in directions:
                r, c = self.row + dr, self.col + dc
                while self._inside(r, c):
                    target = board.get((r, c))
                    if target is None:
                        moves.append((r, c))
                    else:
                        if target.color != self.color:
                            moves.append((r, c))
                        break
                    r += dr
                    c += dc
            return moves
    
    
    DIRS_8 = [(dr, dc) for dr in (-1, 0, 1) for dc in (-1, 0, 1) if (dr, dc) != (0, 0)]
    DIRS_STRAIGHT = [(-1, 0), (1, 0), (0, -1), (0, 1)]
    DIRS_DIAGONAL = [(-1, -1), (-1, 1), (1, -1), (1, 1)]
    
    
    class King(Piece):
        def legal_moves(self, board):
            return [
                (self.row + dr, self.col + dc)
                for dr, dc in DIRS_8
                if self._inside(self.row + dr, self.col + dc)
                and self._can_land(board, self.row + dr, self.col + dc)
            ]
    
        def symbol(self):
            return "♔" if self.color == "white" else "♚"
    
    
    class Queen(Piece):
        def legal_moves(self, board):
            return self._slide(board, DIRS_8)
    
        def symbol(self):
            return "♕" if self.color == "white" else "♛"
    
    
    class Rook(Piece):
        def legal_moves(self, board):
            return self._slide(board, DIRS_STRAIGHT)
    
        def symbol(self):
            return "♖" if self.color == "white" else "♜"
    
    
    class Bishop(Piece):
        def legal_moves(self, board):
            return self._slide(board, DIRS_DIAGONAL)
    
        def symbol(self):
            return "♗" if self.color == "white" else "♝"
    
    
    class Knight(Piece):
        OFFSETS = [(-2, -1), (-2, 1), (-1, -2), (-1, 2),
                   (1, -2), (1, 2), (2, -1), (2, 1)]
    
        def legal_moves(self, board):
            return [
                (self.row + dr, self.col + dc)
                for dr, dc in self.OFFSETS
                if self._inside(self.row + dr, self.col + dc)
                and self._can_land(board, self.row + dr, self.col + dc)
            ]
    
        def symbol(self):
            return "♘" if self.color == "white" else "♞"
    
    
    class Pawn(Piece):
        def legal_moves(self, board):
            moves = []
            step = -1 if self.color == "white" else 1
            start_row = 6 if self.color == "white" else 1
    
            r = self.row + step
            if self._inside(r, self.col) and board.get((r, self.col)) is None:
                moves.append((r, self.col))
                if self.row == start_row and board.get((r + step, self.col)) is None:
                    moves.append((r + step, self.col))
    
            for dc in (-1, 1):
                r, c = self.row + step, self.col + dc
                if self._inside(r, c):
                    target = board.get((r, c))
                    if target is not None and target.color != self.color:
                        moves.append((r, c))
    
            return moves
    
        def symbol(self):
            return "♙" if self.color == "white" else "♟"
    
    
    board = {
        (7, 4): King("white", 7, 4),
        (7, 3): Queen("white", 7, 3),
        (7, 0): Rook("white", 7, 0),
        (7, 7): Rook("white", 7, 7),
        (7, 2): Bishop("white", 7, 2),
        (7, 5): Bishop("white", 7, 5),
        (7, 1): Knight("white", 7, 1),
        (6, 0): Pawn("white", 6, 0),
        (0, 4): King("black", 0, 4),
        (0, 3): Queen("black", 0, 3),
        (0, 1): Knight("black", 0, 1),
        (1, 1): Pawn("black", 1, 1),
    }
    
    for pos, piece in board.items():
        moves = piece.legal_moves(board)
        print(f"{piece.symbol()} {type(piece).__name__:7s} at {pos} "
              f"-> {len(moves)} moves")
    Expected output
    ♔ King at (7, 4) -> 3 moves ♕ Queen at (7, 3) -> 13 moves ♖ Rook at (7, 0) -> 11 moves ♖ Rook at (7, 7) -> 12 moves ♗ Bishop at (7, 2) -> 7 moves ♗ Bishop at (7, 5) -> 7 moves ♘ Knight at (7, 1) -> 2 moves ♙ Pawn at (6, 0) -> 2 moves ♔ King at (0, 4) -> 5 moves ♕ Queen at (0, 3) -> 13 moves ♘ Knight at (0, 1) -> 3 moves ♟ Pawn at (1, 1) -> 3 moves

    🔍 Deep dive — how one loop calls six different methods

    The final loop calls piece.legal_moves(board) for each of the 12 pieces. Watch how Python resolves each call:

    #What Python doesResult
    1Looks up legal_moves on KingFound — runs King.legal_moves
    2Looks up legal_moves on QueenFound — runs Queen.legal_moves
    3Looks up legal_moves on RookFound — runs Rook.legal_moves
    4… and so on for each pieceEach uses its own rule

    Key insight: the loop never mentions King, Queen or Knight. It only knows that every piece can answer legal_moves(). That's the polymorphic contract working.

    📖 Line-by-line walkthrough

    1. Piece.__init__(self, color, row, col) — stores the shared state: color and board position. Every subclass inherits it.
    2. legal_moves() and symbol() raise NotImplementedError — placeholders. Any subclass that forgets to override them fails loudly when called.
    3. _inside(r, c) — bounds check. Returns True if a square is on the 8×8 board.
    4. _can_land(board, r, c) — a piece can land on an empty square or an enemy's square, but not a friendly one.
    5. _slide(board, directions) — the shared helper for sliding pieces. For each direction, it walks step by step, collecting empty squares until it hits something. If that something is an enemy, it can be captured; then the walk in that direction stops.
    6. DIRS_8, DIRS_STRAIGHT, DIRS_DIAGONAL — direction sets. Rook uses straight, Bishop uses diagonal, Queen uses both.
    7. King.legal_moves — a one-step walk in all 8 directions. It can't use _slide directly (which slides multiple squares), so it builds the list with a comprehension.
    8. Queen.legal_moves — literally self._slide(board, DIRS_8). Maximum reuse.
    9. Rook.legal_moves — self._slide(board, DIRS_STRAIGHT).
    10. Bishop.legal_moves — self._slide(board, DIRS_DIAGONAL).
    11. Knight.OFFSETS — the eight L-shaped jumps, as a class-level constant.
    12. Knight.legal_moves — checks each offset, no sliding. Knights jump over anything, so it never looks at intermediate squares.
    13. Pawn.legal_moves — the only piece whose direction depends on color. Forward one; if still on the starting rank, forward two (if both squares are empty); diagonal captures on enemy pieces only.
    14. The board dictionary — maps (row, col) to piece. Empty squares simply aren't in the dict.
    15. The final loop — calls legal_moves() and symbol() on every piece, plus type(piece).__name__ to label the output. One loop, six behaviors.

    ⚠️ Common mistakes

    Using isinstance inside legal_moves. If Queen.legal_moves checks "am I also a rook?" it's collapsing the polymorphism back into the very pattern we escaped.

    Letting pawns capture forward. Real chess: pawns capture only diagonally. Our implementation gets that right — a common bug is to allow forward captures.

    Sliding through a blocker. In _slide, the walk must break after a capture. Otherwise a bishop can leap over a pawn.

    Forgetting the knight jumps. The knight is the only piece that doesn't use _slide, precisely because it jumps over occupied squares.

    🧪 Try it yourself

    Experiments in the playground below:

    • Add a "Princess" (bishop + knight) that merges the two move sets. How many existing classes did you edit?
    • Give Pawn en passant rules. Which other pieces need to know about it?
    • Add a Piece.attacks(board) method that returns the squares a piece threatens (which differs from legal_moves for pawns). Use it to check if a king is in check.
    • Write an is_checkmate(color, board) function that loops over every enemy piece and asks its legal_moves(). Notice you never mention a piece type.

    Experiment With the Engine

    Modify the code above — add a piece, change a rule, test a hypothesis. Press Run to execute it in your browser.

    🐍 chess_engine.py
    Press Run to execute the chess engine.

    Play With the Pieces

    Click a piece to see its legal moves highlighted. Green squares are valid moves; red squares are captures. Hover over a piece to see how many moves it has.

    Click any piece to see its legal moves.
    Light square
    Dark square
    Selected
    Legal move
    Capture

    What's happening under the hood: when you click a piece, the board calls piece.legal_moves(board) — the same method from the code above. The board doesn't know whether it's holding a rook or a queen. It just asks.

    Quick Knowledge Check

    Five questions on the ideas from this project.

    0/5

    Great effort — review any missed questions above.

    Share This Project

    Subscribe to Poetic Bytes

    Level up your Python knowledge.

    In-depth Python tutorials, AI insights, and software engineering guides — delivered straight to your inbox. No spam, ever.

    Weekly Python tutorials OOP series updates Free, cancel anytime

    🔒 Your email is never shared or sold.