Week 41 · Phase 5 — The Builds
Six piece types, an 8×8 board, and an OOP design where polymorphism actually earns its keep.
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.
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.
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.
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.
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.
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.
Board::starting_position(). Print the board with the standard letter notation (uppercase = white, lowercase = black).Board::all_moves(Color) that gathers pseudo-legal moves for every piece of that colour. From the starting position, white should have 20 moves (16 pawn moves + 4 knight moves).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.
Answer: B. Same surface — different behaviour per type. That's the textbook polymorphism use case.
Answer: B. No manual delete, no leaks. The smart pointer is doing the bookkeeping you'd otherwise do by hand.
Answer: B. Pieces only know geometry. The 'no leaving your king in check' rule is checked separately at the board level.