This is the project where the language earns it. Tic-tac-toe was a single class. Scrabble was four or five. Chess will be a dozen, related to each other in ways that are awkward in C and natural in C++. The piece hierarchy alone is the cleanest possible motivation for inheritance and virtual functions: every piece has a colour, occupies one square, and has a notion of "the squares I can move to" — but the rule for which squares varies wildly between Pawn and Queen. That's the textbook definition of polymorphism.

This week we build the static engine: the piece hierarchy, the board, the move list. Next week we glue it together with a full move validator (with castling, en passant, and check detection). Week 43 we make it think.

The piece hierarchy

Six concrete piece types, all sharing the same surface: position, colour, and a way to ask "where can I move from here?". The base class is abstract; the concrete subclasses fill in the moves.

enum class Color { White, Black };
struct Square { int file, rank; };      // 0..7 each

class Board;     // forward declare

class Piece {
public:
    Piece(Color c) : color_(c) {}
    virtual ~Piece() = default;

    Color color() const { return color_; }
    virtual char  letter() const = 0;     // 'P','N','B','R','Q','K'
    virtual std::vector<Square>
        pseudo_legal_moves(const Board&, Square from) const = 0;

private:
    Color color_;
};

Two pure virtual methods — letter() for printing and pseudo_legal_moves() for movement — make Piece abstract. Concrete subclasses implement them. "Pseudo-legal" because we'll filter out moves that leave the king in check separately; pieces only know geometry, not the global state of the king.

A piece example: the rook

class Rook : public Piece {
public:
    using Piece::Piece;
    char letter() const override { return 'R'; }

    std::vector<Square>
    pseudo_legal_moves(const Board& b, Square from) const override {
        std::vector<Square> moves;
        static const int dirs[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};
        for (auto& d : dirs) {
            int f = from.file + d[0];
            int r = from.rank + d[1];
            while (f >= 0 && f < 8 && r >= 0 && r < 8) {
                if (auto* p = b.at({f,r})) {
                    if (p->color() != color())
                        moves.push_back({f,r});  // capture
                    break;                          // own piece or after capture: stop
                }
                moves.push_back({f,r});
                f += d[0]; r += d[1];
            }
        }
        return moves;
    }
};

Four directions (rank + file), slide outwards from the source square. Stop when you walk off the board. Stop when you hit any piece — but if it's an opponent, that final square is a capture and is included. Bishop is the same idea with the four diagonal directions; Queen is rook + bishop. Knight is the eight L-shapes. King is one step in any of eight directions. Pawn is the irregular one (forward, two on first move, diagonal-only captures, en passant, promotion) — the one that takes the most lines.

The Board

class Board {
public:
    static Board starting_position();      // the standard layout

    Piece*       at(Square s);
    const Piece* at(Square s) const;

    Color side_to_move() const { return stm_; }
    // state for special rules — covered next week
    bool can_castle_kingside(Color) const;
    bool can_castle_queenside(Color) const;
    std::optional<Square> en_passant_target() const;

private:
    std::unique_ptr<Piece> grid_[8][8];      // owns its pieces
    Color stm_ = Color::White;
    // castling rights, en passant, halfmove clock, fullmove number…
};

std::unique_ptr from Week 25 lets the grid own its pieces. When a piece is captured, replacing the destination with a new unique_ptr automatically deletes the old one. No manual delete, no leaks, no double-frees. This is exactly the kind of code where smart pointers stop being theoretical and become indispensable.

The exposed at() returns a raw pointer because callers are observing, not taking ownership. The board keeps the only owning handle.

Why polymorphism, here, is real

You could write chess in C with a giant switch (piece_type) in every move-generation function. People do. It works. But every time you add a feature — generate moves, check legality, evaluate material, render Unicode — you write the same six-case switch again. After three or four of those, you have the same bug fixed in three places out of four, and the engine starts misbehaving in subtle ways.

With virtual functions, every operation that depends on piece type lives next to the piece. King::pseudo_legal_moves is in the King class. King::letter is in the King class. If you add a method like King::value(), you only touch the King file. The compiler reminds you whenever you've added an interface method but forgotten to implement it on a derived class. This is the use case Stroustrup designed C++ for.

When you have N types and M operations, classes group code by type, switches group it by operation. Pick the dimension that changes more rarely.

The numbers

A reasonable chess move generator emits all pseudo-legal moves for a side in maybe 50 microseconds. From there, alpha-beta search (Week 43) explores millions of positions per second. Modern engines like Stockfish reach ~100 million positions/sec on commodity hardware. Our hobby engine will do maybe 1 million — enough to play a respectable game when paired with a depth-5 search.

We won't compete with Stockfish. We will, by Week 47, ship a chess engine that beats casual humans, runs as a native macOS and Windows app, and which you wrote, line by line, with full understanding.

Try it yourself

What's next

The pieces know how they move in isolation. They don't know about castling rights, the 50-move rule, threefold repetition, en passant, promotion choice, or the absolute prohibition on moving into check. Those are game-state rules, and they live on the Board, not the pieces. Next week we wire them in and produce a move validator that accepts a move iff it's strictly legal in current chess rules.

Week 42 is Chess Logic.

Quick check

1. Why is polymorphism the right design for chess piece types?
  1. All pieces share identical movement rules
  2. Each piece type has unique movement rules but the same interface (colour, position, list-of-moves)
  3. Polymorphism is required by C++
Reveal Answer

Answer: B. Same surface — different behaviour per type. That's the textbook polymorphism use case.

2. Why use std::unique_ptr<Piece> for the squares of the chess board?
  1. It's faster than raw pointers
  2. The board owns its pieces; capturing means assigning a new unique_ptr, which auto-deletes the captured piece
  3. The compiler refuses otherwise
Reveal Answer

Answer: B. No manual delete, no leaks. The smart pointer is doing the bookkeeping you'd otherwise do by hand.

3. What does 'pseudo-legal' mean for piece moves?
  1. Legal under FIDE rules
  2. Geometrically valid for the piece, but not yet checked for whether the king would be left in check
  3. Suggested by the AI
Reveal Answer

Answer: B. Pieces only know geometry. The 'no leaving your king in check' rule is checked separately at the board level.