Knowledge Guide
HomeHands-On BuildsLLD Katas

Design Chess

Build a Chess Engine (Design Chess)

Chess is the LLD interview that separates "I can draw a class diagram" from "I can reason about correctness under adversarial pressure." Any candidate can sketch Board, Piece, Move and a per-piece movement rule. What actually gets probed is the sentence right after: "okay, now prove your engine never lets a player leave their own king in check." That single requirement — a move must be filtered by "does this leave MY king attacked?" before it's offered to the player — is where naive designs quietly ship an illegal-move bug, and where this kata lives. You already know the class breakdown from the Design Chess walkthrough; this project makes you build the piece hierarchy and the legal-move filter from an empty file in Java and Go, break the naive version on a concrete pinned-piece position, and walk out able to defend checkmate detection, the pseudo-legal-then-filter architecture, and how a real engine (bitboards) differs.

1. The Trap

You model the obvious classes: Board, an abstract Piece with a subclass per piece type, and each piece knows how it moves — a knight hops in L-shapes, a rook slides until blocked. You wire up generateMoves(square), dispatch through the hierarchy, and it works: pieces move exactly the way chess pieces should move. You demo it. It looks done.

Then the interviewer sets up one position and asks you to play a move. White King on e1. White Knight on e4. Black Rook on e8, with a clear file in between. Your engine offers the knight every one of its normal L-shaped hops — say, Ne4-f6 — because from the knight's own point of view, hopping to f6 is a completely ordinary, unblocked move. It has no idea a king exists three squares below it on the same file. Play that move for real and the board now has White's own king sitting in the open, one rook-move from capture. Your engine just let a player make an illegal move that loses the game to a rule the engine never checked.

That's the trap: every piece's movement rule is a strictly local question ("can I physically reach that square, given what's occupying the board?"). Whether the move is safe — whether it leaves your own king exposed — is a global question about the whole board's attack pattern, and a per-piece movement method structurally cannot answer it. A design that stops at "can this piece reach this square" has silently answered a different, easier question than "is this move legal," and shipped the gap as a bug.

Vertical e-file view: White King on e1, White Knight on e4, Black Rook on e8. The knight is the only piece blocking the file. A green highlighted line marks the e-file; text explains that the knight's own pseudo-legal move generator cannot see the king three squares below it, and that moving the knight opens the file so the rook attacks e1. Bottom line: pseudoLegal(White) = 13 moves, legalMoves(White) = 5 moves, after 8 knight hops are filtered out.
Vertical e-file view: White King on e1, White Knight on e4, Black Rook on e8. The knight is the only piece blocking the file. A green highlighted line marks the e-file; text explains that the knight's own pseudo-legal move generator cannot see the king three squares below it, and that moving the knight opens the file so the rook attacks e1. Bottom line: pseudoLegal(White) = 13 moves, legalMoves(White) = 5 moves, after 8 knight hops are filtered out.

2. Scope it like a senior

Before writing a line, pin down what "build a chess engine" actually means here — a candidate who starts typing class Pawn immediately usually mis-scopes the hard part:

For this kata: King, Rook, Knight — one piece that steps, one that slides, one that jumps, which is exactly enough structural variety to prove the polymorphic design and the check-filter both work. Bishop, Queen, and Pawn are noted but not re-implemented: Bishop is mechanically identical to Rook (diagonal slide instead of orthogonal), Queen is the union of both, and Pawn just adds direction-dependent capture/push rules plus promotion — the movement-generation pattern doesn't change, only the offset tables do.

3. Reason to the design

Simplest thing that could work: one giant generateMoves(pieceType, row, col) function with a switch on piece type, each case hand-coding that piece's rule. It compiles, it's easy to trace for a single piece, and for a toy demo it looks complete.

Why it fails: two independent problems pile up in one function. First, extensibility — adding a new piece type, or a chess variant with a custom piece, means editing one already-large function instead of adding a unit. Second, and far worse, is §1's trap: the switch only ever answers "how does this piece move," and there is no natural place inside a per-piece movement rule to ask "does making this move leave my OWN king attacked?" — that question needs the whole board's state, not just one square. Bolting it on inside the switch means duplicating "is X attacked" logic per piece type, and it is exactly the kind of duplication where someone forgets a case (a pinned knight, a discovered check from a piece nowhere near the king) and ships a bug that only shows up on a specific board position, not in casual testing.

The key move — split "can this piece reach that square" from "is this move safe": put move generation ON the piece (polymorphism: an abstract Piece with a pseudoLegalMoves(board, row, col) method, one subclass per piece type). This gives you pseudo-legal moves: obeys the piece's own rule and board occupancy, knows nothing about check. Then, as a completely separate, piece-agnostic, board-level operation, filter: for every pseudo-legal move, actually make it, ask "is my own king attacked now?", then undo it. This is the make → is-king-attacked → unmake pattern, and it is the entire fix — it doesn't matter whether the exposure is a simple pin, a discovered check from a completely different piece, or moving the king itself into an attacked square: they all reduce to the same one question asked after simulating the move.

The elegant reuse inside the fix: "is square S attacked by side X?" doesn't need its own hand-written logic — it's just "does any pseudo-legal move of side X's pieces land on S?" You already have that generator. One mechanism (pseudoLegalMoves) answers both "how can my pieces move" and, reused, "what does the enemy attack" — which is exactly how isKingAttacked is built below, and it's why the design doesn't need a second, separately-maintained "attack map."

4. Build it — milestones

Attempt each milestone yourself before reading the reference implementation below. Contract: an abstract Piece with pseudoLegalMoves(Board, row, col) → List<Move>; a Board with legalMoves(side) → List<Move>, isKingAttacked(side) → boolean, isCheckmate(side) → boolean, isStalemate(side) → boolean. Board coordinates: row 0 = rank 1, col 0 = file a (so e1 = (0,4), e8 = (7,4)).

Reference implementation — Java

The whole trick is the split between Piece.pseudoLegalMoves (movement physics, polymorphic per piece) and Board.legalMoves (the make→check→unmake safety filter, piece-agnostic). isSquareAttacked reuses pseudoLegalMoves instead of duplicating attack logic — that reuse is the design's whole leverage.

import java.util.*;

public class Chess {
    enum Color { WHITE, BLACK }

    static final class Move {
        final int fromRow, fromCol, toRow, toCol;
        Move(int fromRow, int fromCol, int toRow, int toCol) {
            this.fromRow = fromRow; this.fromCol = fromCol;
            this.toRow = toRow; this.toCol = toCol;
        }
        boolean sameAs(int fr, int fc, int tr, int tc) {
            return fromRow == fr && fromCol == fc && toRow == tr && toCol == tc;
        }
        public String toString() {
            return "" + (char) ('a' + fromCol) + (fromRow + 1) + (char) ('a' + toCol) + (toRow + 1);
        }
    }

    // Every piece generates its OWN pseudo-legal moves -- polymorphism replaces
    // a giant switch(pieceType) in the move generator. "Pseudo-legal" means the
    // move obeys this piece's movement rule and the board's occupancy, but has
    // NO idea whether it leaves the mover's own king in check. That filter is a
    // separate, board-level concern -- see Board.legalMoves().
    static abstract class Piece {
        final Color color;
        Piece(Color color) { this.color = color; }
        abstract List<Move> pseudoLegalMoves(Board board, int row, int col);
        abstract char symbol();
    }

    static final class King extends Piece {
        King(Color c) { super(c); }
        List<Move> pseudoLegalMoves(Board board, int row, int col) {
            List<Move> moves = new ArrayList<>();
            for (int dr = -1; dr <= 1; dr++) {
                for (int dc = -1; dc <= 1; dc++) {
                    if (dr == 0 && dc == 0) continue;
                    int r = row + dr, c = col + dc;
                    if (board.inBounds(r, c) && board.canLandOn(r, c, color)) {
                        moves.add(new Move(row, col, r, c));
                    }
                }
            }
            return moves; // castling omitted here -- see Optimise / Defend
        }
        char symbol() { return color == Color.WHITE ? 'K' : 'k'; }
    }

    static final class Rook extends Piece {
        Rook(Color c) { super(c); }
        List<Move> pseudoLegalMoves(Board board, int row, int col) {
            List<Move> moves = new ArrayList<>();
            int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
            for (int[] d : dirs) {
                int r = row + d[0], c = col + d[1];
                while (board.inBounds(r, c)) {
                    Piece occupant = board.get(r, c);
                    if (occupant == null) {
                        moves.add(new Move(row, col, r, c));
                    } else {
                        if (occupant.color != color) moves.add(new Move(row, col, r, c));
                        break; // a piece (either color) stops the slide
                    }
                    r += d[0]; c += d[1];
                }
            }
            return moves;
        }
        char symbol() { return color == Color.WHITE ? 'R' : 'r'; }
    }

    static final class Knight extends Piece {
        Knight(Color c) { super(c); }
        List<Move> pseudoLegalMoves(Board board, int row, int col) {
            List<Move> moves = new ArrayList<>();
            int[][] offsets = {{1, 2}, {2, 1}, {-1, 2}, {-2, 1}, {1, -2}, {2, -1}, {-1, -2}, {-2, -1}};
            for (int[] o : offsets) {
                int r = row + o[0], c = col + o[1];
                if (board.inBounds(r, c) && board.canLandOn(r, c, color)) {
                    moves.add(new Move(row, col, r, c));
                }
            }
            return moves;
        }
        char symbol() { return color == Color.WHITE ? 'N' : 'n'; }
    }

    static final class Board {
        final Piece[][] grid = new Piece[8][8]; // grid[row][col]; row 0 = rank 1, col 0 = file a

        boolean inBounds(int r, int c) { return r >= 0 && r < 8 && c >= 0 && c < 8; }
        Piece get(int r, int c) { return grid[r][c]; }
        void set(int r, int c, Piece p) { grid[r][c] = p; }
        boolean canLandOn(int r, int c, Color mover) {
            Piece occupant = grid[r][c];
            return occupant == null || occupant.color != mover;
        }

        // Pseudo-legal moves for a whole side: dispatch to each piece's own rule.
        List<Move> pseudoLegalMoves(Color side) {
            List<Move> moves = new ArrayList<>();
            for (int r = 0; r < 8; r++) {
                for (int c = 0; c < 8; c++) {
                    Piece p = grid[r][c];
                    if (p != null && p.color == side) moves.addAll(p.pseudoLegalMoves(this, r, c));
                }
            }
            return moves;
        }

        // A square is attacked by `bySide` if any of that side's pseudo-legal
        // moves targets it. Reusing pseudoLegalMoves here (instead of a second
        // hand-rolled "attack map") is what keeps this correct -- one rule per
        // piece, used for both move generation and check detection.
        boolean isSquareAttacked(int row, int col, Color bySide) {
            for (int r = 0; r < 8; r++) {
                for (int c = 0; c < 8; c++) {
                    Piece p = grid[r][c];
                    if (p == null || p.color != bySide) continue;
                    for (Move m : p.pseudoLegalMoves(this, r, c)) {
                        if (m.toRow == row && m.toCol == col) return true;
                    }
                }
            }
            return false;
        }

        int[] findKing(Color side) {
            for (int r = 0; r < 8; r++) {
                for (int c = 0; c < 8; c++) {
                    Piece p = grid[r][c];
                    if (p instanceof King && p.color == side) return new int[]{r, c};
                }
            }
            throw new IllegalStateException("no king on board for " + side);
        }

        boolean isKingAttacked(Color side) {
            int[] k = findKing(side);
            Color enemy = (side == Color.WHITE) ? Color.BLACK : Color.WHITE;
            return isSquareAttacked(k[0], k[1], enemy);
        }

        // make -> check -> unmake: the whole mechanism that turns a pseudo-legal
        // move into a LEGAL one. Play it for real, ask "is my own king now
        // attacked?", then undo it -- the board is never left in the tried state.
        Piece makeMove(Move m) {
            Piece captured = grid[m.toRow][m.toCol];
            grid[m.toRow][m.toCol] = grid[m.fromRow][m.fromCol];
            grid[m.fromRow][m.fromCol] = null;
            return captured;
        }
        void unmakeMove(Move m, Piece captured) {
            grid[m.fromRow][m.fromCol] = grid[m.toRow][m.toCol];
            grid[m.toRow][m.toCol] = captured;
        }

        List<Move> legalMoves(Color side) {
            List<Move> legal = new ArrayList<>();
            for (Move m : pseudoLegalMoves(side)) {
                Piece captured = makeMove(m);
                if (!isKingAttacked(side)) legal.add(m);
                unmakeMove(m, captured);
            }
            return legal;
        }

        boolean isCheckmate(Color side) { return isKingAttacked(side) && legalMoves(side).isEmpty(); }
        boolean isStalemate(Color side) { return !isKingAttacked(side) && legalMoves(side).isEmpty(); }

        // THE BUG, kept on the page deliberately: a "naive" generator that treats
        // pseudo-legal moves as if they were already legal -- no make/check/unmake
        // filter at all. Never call this from real game logic; it exists only to
        // reproduce the break-it scenario below.
        List<Move> naiveLegalMoves(Color side) {
            return pseudoLegalMoves(side);
        }
    }

    static void assertTrue(boolean cond, String label) {
        if (!cond) throw new AssertionError("FAILED: " + label);
        System.out.println("PASS: " + label);
    }

    static boolean contains(List<Move> moves, int fr, int fc, int tr, int tc) {
        for (Move m : moves) if (m.sameAs(fr, fc, tr, tc)) return true;
        return false;
    }

    public static void main(String[] args) {
        // ---- Scenario 1: the pin. White King e1, White Knight e4, Black Rook e8. ----
        // Board coords: row 0 = rank 1, col 0 = file a. e1=(0,4) e4=(3,4) e8=(7,4).
        Board b = new Board();
        b.set(0, 4, new King(Color.WHITE));
        b.set(3, 4, new Knight(Color.WHITE));
        b.set(7, 4, new Rook(Color.BLACK));
        b.set(7, 0, new King(Color.BLACK)); // a8 -- present, irrelevant to the pin

        assertTrue(!b.isKingAttacked(Color.WHITE),
            "White king not currently in check -- the knight still blocks the e-file");

        // The naive generator (the bug): does it think Ne4-f6 is a fine move?
        List<Move> naive = b.naiveLegalMoves(Color.WHITE);
        assertTrue(contains(naive, 3, 4, 5, 5),
            "BUG: naive generator OKs Ne4-f6 -- it never looks at the king at all");

        // Prove it's actually illegal: play it for real and re-check the king.
        Move ne4f6 = new Move(3, 4, 5, 5);
        Piece captured = b.makeMove(ne4f6);
        assertTrue(b.isKingAttacked(Color.WHITE),
            "reproduced: moving the pinned knight exposes White's own king to the rook on e8");
        b.unmakeMove(ne4f6, captured);

        // The real (filtered) legal-move list must reject exactly this move.
        List<Move> legal = b.legalMoves(Color.WHITE);
        assertTrue(!contains(legal, 3, 4, 5, 5),
            "fix confirmed: legalMoves() filters out Ne4-f6 -- the pin is enforced");
        assertTrue(naive.size() == 13 && legal.size() == 5,
            "pin removes exactly the knight's 8 pseudo-legal hops (13 pseudo-legal -> 5 legal)");

        // ---- Scenario 2: checkmate detection -- the two-rook "ladder" mate. ----
        Board mate = new Board();
        mate.set(7, 0, new King(Color.BLACK));  // a8
        mate.set(6, 7, new Rook(Color.WHITE));  // h7 -- seals off rank 7
        mate.set(7, 3, new Rook(Color.WHITE));  // d8 -- checks along rank 8
        mate.set(0, 7, new King(Color.WHITE));  // h1, elsewhere on the board

        assertTrue(mate.isKingAttacked(Color.BLACK),
            "Black king in check from the rook on d8 along rank 8");
        assertTrue(mate.legalMoves(Color.BLACK).isEmpty(),
            "Black has zero legal moves -- a7/b7/b8 are all covered by the h7 or d8 rook");
        assertTrue(mate.isCheckmate(Color.BLACK),
            "isCheckmate correctly reports checkmate for the ladder mate");
        assertTrue(!mate.isStalemate(Color.BLACK),
            "not a stalemate -- the king IS in check (stalemate requires NOT in check)");

        // ---- Scenario 3: stalemate is the OTHER empty-legal-moves case. ----
        // Black King a8, boxed in but NOT currently in check: Rook d7 covers rank 7
        // (so a7 and b7 are both attacked), Rook b6 covers file b (so b8 is attacked
        // too), and neither rook's line passes through a8 itself.
        Board stale = new Board();
        stale.set(7, 0, new King(Color.BLACK)); // a8
        stale.set(5, 1, new Rook(Color.WHITE)); // b6 -- covers file b (b7, b8) but NOT rank 8 or a7
        stale.set(6, 3, new Rook(Color.WHITE)); // d7 -- covers rank 7 (a7, b7, c7) but not a8/b8
        stale.set(0, 7, new King(Color.WHITE)); // h1

        assertTrue(!stale.isKingAttacked(Color.BLACK),
            "Black king NOT in check in the stalemate position");
        assertTrue(stale.legalMoves(Color.BLACK).isEmpty(),
            "Black has zero legal moves -- a7 and b7 covered by d7, b8 covered by b6, a8 has no other neighbor");
        assertTrue(stale.isStalemate(Color.BLACK), "isStalemate correctly reports stalemate, not checkmate");
        assertTrue(!stale.isCheckmate(Color.BLACK), "and confirms it is NOT checkmate -- no legal moves but no check either");

        System.out.println("ALL JAVA TESTS PASSED");
    }
}

Reference implementation — Go

Go has no class inheritance, so the "piece hierarchy" becomes a PieceKind enum dispatched inside Piece.PseudoLegalMoves — the idiomatic Go shape for "one type, several behaviors" when you don't want a full interface-per-piece-type. The board-level logic (IsSquareAttacked, LegalMoves, IsCheckmate/IsStalemate) is identical in structure to the Java version.

package main

import "fmt"

type Color int

const (
	White Color = iota
	Black
)

func opponent(c Color) Color {
	if c == White {
		return Black
	}
	return White
}

type Move struct{ fromRow, fromCol, toRow, toCol int }

func (m Move) sameAs(fr, fc, tr, tc int) bool {
	return m.fromRow == fr && m.fromCol == fc && m.toRow == tr && m.toCol == tc
}

// PieceKind + a per-kind pseudo-legal move method stand in for the polymorphic
// Piece hierarchy (Go has no classes/inheritance -- an interface + concrete
// types is the idiomatic equivalent of "each piece generates its own moves").
type PieceKind int

const (
	KingKind PieceKind = iota
	RookKind
	KnightKind
)

type Piece struct {
	kind  PieceKind
	color Color
}

// Piece.PseudoLegalMoves obeys ONLY this piece's movement rule and the board's
// occupancy. It has no idea whether the move leaves the mover's own king in
// check -- that filter is a separate, board-level concern (Board.LegalMoves).
func (p Piece) PseudoLegalMoves(b *Board, row, col int) []Move {
	switch p.kind {
	case KingKind:
		return kingMoves(b, row, col, p.color)
	case RookKind:
		return rookMoves(b, row, col, p.color)
	case KnightKind:
		return knightMoves(b, row, col, p.color)
	}
	return nil
}

func kingMoves(b *Board, row, col int, color Color) []Move {
	var moves []Move
	for dr := -1; dr <= 1; dr++ {
		for dc := -1; dc <= 1; dc++ {
			if dr == 0 && dc == 0 {
				continue
			}
			r, c := row+dr, col+dc
			if b.inBounds(r, c) && b.canLandOn(r, c, color) {
				moves = append(moves, Move{row, col, r, c})
			}
		}
	}
	return moves // castling omitted here -- see Optimise / Defend
}

func rookMoves(b *Board, row, col int, color Color) []Move {
	var moves []Move
	dirs := [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}}
	for _, d := range dirs {
		r, c := row+d[0], col+d[1]
		for b.inBounds(r, c) {
			occupant, occupied := b.get(r, c)
			if !occupied {
				moves = append(moves, Move{row, col, r, c})
			} else {
				if occupant.color != color {
					moves = append(moves, Move{row, col, r, c})
				}
				break // a piece (either color) stops the slide
			}
			r += d[0]
			c += d[1]
		}
	}
	return moves
}

func knightMoves(b *Board, row, col int, color Color) []Move {
	var moves []Move
	offsets := [][2]int{{1, 2}, {2, 1}, {-1, 2}, {-2, 1}, {1, -2}, {2, -1}, {-1, -2}, {-2, -1}}
	for _, o := range offsets {
		r, c := row+o[0], col+o[1]
		if b.inBounds(r, c) && b.canLandOn(r, c, color) {
			moves = append(moves, Move{row, col, r, c})
		}
	}
	return moves
}

// Board.grid[row][col]; row 0 = rank 1, col 0 = file a. A nil *Piece means empty.
type Board struct {
	grid [8][8]*Piece
}

func NewBoard() *Board { return &Board{} }

func (b *Board) inBounds(r, c int) bool { return r >= 0 && r < 8 && c >= 0 && c < 8 }
func (b *Board) get(r, c int) (Piece, bool) {
	p := b.grid[r][c]
	if p == nil {
		return Piece{}, false
	}
	return *p, true
}
func (b *Board) set(r, c int, p Piece) { pp := p; b.grid[r][c] = &pp }
func (b *Board) canLandOn(r, c int, mover Color) bool {
	occupant, occupied := b.get(r, c)
	return !occupied || occupant.color != mover
}

// Pseudo-legal moves for a whole side: dispatch to each piece's own rule.
func (b *Board) PseudoLegalMoves(side Color) []Move {
	var moves []Move
	for r := 0; r < 8; r++ {
		for c := 0; c < 8; c++ {
			p := b.grid[r][c]
			if p != nil && p.color == side {
				moves = append(moves, p.PseudoLegalMoves(b, r, c)...)
			}
		}
	}
	return moves
}

// A square is attacked by bySide if any of that side's pseudo-legal moves
// targets it. Reusing PseudoLegalMoves here (instead of a second hand-rolled
// "attack map") is what keeps this correct -- one rule per piece, used for
// both move generation and check detection.
func (b *Board) IsSquareAttacked(row, col int, bySide Color) bool {
	for r := 0; r < 8; r++ {
		for c := 0; c < 8; c++ {
			p := b.grid[r][c]
			if p == nil || p.color != bySide {
				continue
			}
			for _, m := range p.PseudoLegalMoves(b, r, c) {
				if m.toRow == row && m.toCol == col {
					return true
				}
			}
		}
	}
	return false
}

func (b *Board) findKing(side Color) (int, int) {
	for r := 0; r < 8; r++ {
		for c := 0; c < 8; c++ {
			p := b.grid[r][c]
			if p != nil && p.kind == KingKind && p.color == side {
				return r, c
			}
		}
	}
	panic(fmt.Sprintf("no king on board for %v", side))
}

func (b *Board) IsKingAttacked(side Color) bool {
	r, c := b.findKing(side)
	return b.IsSquareAttacked(r, c, opponent(side))
}

// make -> check -> unmake: the whole mechanism that turns a pseudo-legal move
// into a LEGAL one. Play it for real, ask "is my own king now attacked?",
// then undo it -- the board is never left in the tried state.
func (b *Board) makeMove(m Move) *Piece {
	captured := b.grid[m.toRow][m.toCol]
	b.grid[m.toRow][m.toCol] = b.grid[m.fromRow][m.fromCol]
	b.grid[m.fromRow][m.fromCol] = nil
	return captured
}
func (b *Board) unmakeMove(m Move, captured *Piece) {
	b.grid[m.fromRow][m.fromCol] = b.grid[m.toRow][m.toCol]
	b.grid[m.toRow][m.toCol] = captured
}

func (b *Board) LegalMoves(side Color) []Move {
	var legal []Move
	for _, m := range b.PseudoLegalMoves(side) {
		captured := b.makeMove(m)
		if !b.IsKingAttacked(side) {
			legal = append(legal, m)
		}
		b.unmakeMove(m, captured)
	}
	return legal
}

func (b *Board) IsCheckmate(side Color) bool {
	return b.IsKingAttacked(side) && len(b.LegalMoves(side)) == 0
}
func (b *Board) IsStalemate(side Color) bool {
	return !b.IsKingAttacked(side) && len(b.LegalMoves(side)) == 0
}

// THE BUG, kept on the page deliberately: a "naive" generator that treats
// pseudo-legal moves as if they were already legal -- no make/check/unmake
// filter at all. Never call this from real game logic; it exists only to
// reproduce the break-it scenario below.
func (b *Board) NaiveLegalMoves(side Color) []Move {
	return b.PseudoLegalMoves(side)
}

func containsMove(moves []Move, fr, fc, tr, tc int) bool {
	for _, m := range moves {
		if m.sameAs(fr, fc, tr, tc) {
			return true
		}
	}
	return false
}

func check(cond bool, label string) {
	if !cond {
		panic("FAILED: " + label)
	}
	fmt.Println("PASS:", label)
}

func main() {
	// ---- Scenario 1: the pin. White King e1, White Knight e4, Black Rook e8. ----
	// Board coords: row 0 = rank 1, col 0 = file a. e1=(0,4) e4=(3,4) e8=(7,4).
	b := NewBoard()
	b.set(0, 4, Piece{KingKind, White})
	b.set(3, 4, Piece{KnightKind, White})
	b.set(7, 4, Piece{RookKind, Black})
	b.set(7, 0, Piece{KingKind, Black}) // a8 -- present, irrelevant to the pin

	check(!b.IsKingAttacked(White),
		"White king not currently in check -- the knight still blocks the e-file")

	// The naive generator (the bug): does it think Ne4-f6 is a fine move?
	naive := b.NaiveLegalMoves(White)
	check(containsMove(naive, 3, 4, 5, 5),
		"BUG: naive generator OKs Ne4-f6 -- it never looks at the king at all")

	// Prove it's actually illegal: play it for real and re-check the king.
	ne4f6 := Move{3, 4, 5, 5}
	captured := b.makeMove(ne4f6)
	check(b.IsKingAttacked(White),
		"reproduced: moving the pinned knight exposes White's own king to the rook on e8")
	b.unmakeMove(ne4f6, captured)

	// The real (filtered) legal-move list must reject exactly this move.
	legal := b.LegalMoves(White)
	check(!containsMove(legal, 3, 4, 5, 5),
		"fix confirmed: LegalMoves() filters out Ne4-f6 -- the pin is enforced")
	check(len(naive) == 13 && len(legal) == 5,
		"pin removes exactly the knight's 8 pseudo-legal hops (13 pseudo-legal -> 5 legal)")

	// ---- Scenario 2: checkmate detection -- the two-rook "ladder" mate. ----
	mate := NewBoard()
	mate.set(7, 0, Piece{KingKind, Black}) // a8
	mate.set(6, 7, Piece{RookKind, White}) // h7 -- seals off rank 7
	mate.set(7, 3, Piece{RookKind, White}) // d8 -- checks along rank 8
	mate.set(0, 7, Piece{KingKind, White}) // h1, elsewhere on the board

	check(mate.IsKingAttacked(Black),
		"Black king in check from the rook on d8 along rank 8")
	check(len(mate.LegalMoves(Black)) == 0,
		"Black has zero legal moves -- a7/b7/b8 are all covered by the h7 or d8 rook")
	check(mate.IsCheckmate(Black),
		"IsCheckmate correctly reports checkmate for the ladder mate")
	check(!mate.IsStalemate(Black),
		"not a stalemate -- the king IS in check (stalemate requires NOT in check)")

	// ---- Scenario 3: stalemate is the OTHER empty-legal-moves case. ----
	// Black King a8, boxed in but NOT currently in check: Rook d7 covers rank 7
	// (so a7 and b7 are both attacked), Rook b6 covers file b (so b8 is attacked
	// too), and neither rook's line passes through a8 itself.
	stale := NewBoard()
	stale.set(7, 0, Piece{KingKind, Black}) // a8
	stale.set(5, 1, Piece{RookKind, White}) // b6
	stale.set(6, 3, Piece{RookKind, White}) // d7
	stale.set(0, 7, Piece{KingKind, White}) // h1

	check(!stale.IsKingAttacked(Black),
		"Black king NOT in check in the stalemate position")
	check(len(stale.LegalMoves(Black)) == 0,
		"Black has zero legal moves -- a7 and b7 covered by d7, b8 covered by b6, a8 has no other neighbor")
	check(stale.IsStalemate(Black), "IsStalemate correctly reports stalemate, not checkmate")
	check(!stale.IsCheckmate(Black), "and confirms it is NOT checkmate -- no legal moves but no check either")

	fmt.Println("ALL GO TESTS PASSED")
}

Both files above are copy-paste compilable as-is: javac Chess.java && java Chess compiles clean and runs a self-contained main with 13 assertions; go vet chess.go && go build chess.go && ./chess vets clean, builds clean, and runs the same 13 assertions — both print PASS for every one and end in ALL JAVA/GO TESTS PASSED.

5. Break it — the failure lesson

The bug that actually ships: an engineer builds the piece hierarchy, gets every individual piece's movement rule correct, tests each one in isolation, and ships — without ever adding the make→check→unmake filter, because "the pieces all move correctly" feels like "the engine is correct." naiveLegalMoves / NaiveLegalMoves on this page reproduces exactly that: it returns pseudoLegalMoves verbatim, with zero awareness of check.

Reproduce it (the exact position from §1): White King e1, White Knight e4, White to move, Black Rook e8 with the file otherwise clear. First confirm the setup is sane — isKingAttacked(WHITE) is false: the knight is still blocking, so the king is not currently in check. Now ask the naive generator whether Ne4-f6 is fine: it says yes — the assertion "BUG: naive generator OKs Ne4-f6" passes, because the knight's own rule has no idea a king exists. Play that exact move for real with makeMove, then re-check: isKingAttacked(WHITE) is now true — the file is open and the rook attacks e1. That's the illegal move, reproduced and proven, not hypothetical. Finally, run the real legalMoves(WHITE) and confirm it does NOT contain Ne4-f6: the pin removes exactly the knight's 8 candidate hops, so pseudo-legal count 13 becomes legal count 5 — every other white move (the king's own 5 steps) survives untouched, because only the knight was the sole blocker on that specific line.

The lesson generalizes past this one pin: any piece that is the sole thing standing between its own king and an enemy slider (rook/bishop/queen) is pinned, and no per-piece movement rule can ever detect that on its own — a piece has no visibility into what's behind it on the board. The only fix that covers every such case (pins, discovered checks off a totally unrelated piece, or the king itself stepping into an attacked square) is the same one mechanism: simulate the move, ask the board-level "is my king attacked," undo it. Skipping that step is invisible in casual play (most moves aren't pinned) and is exactly the kind of bug that survives a demo and gets caught by an opponent — or an interviewer — on move six.

6. Optimise — with trade-offs

Every design choice below buys something at a named cost against a named alternative. State the cost, not just the win.

Piece representationMove-gen costMemoryBuysCostsUse when
OO piece hierarchy (this kata: object array + polymorphic pseudoLegalMoves)Object dispatch + list allocation per piece per call; fine for interactive play64 squares of piece references — trivialReadable, unit-testable per piece, natural fit for an LLD interview and for adding game variants (a new piece = a new class)Slow relative to bitboards: pointer-chasing, per-call allocations, no SIMD-friendly layoutLLD interviews, teaching, casual/turn-based play, correctness-first prototypes
Bitboards (Stockfish, Leela, real engines)Bit ops (AND/OR/popcount) on 64-bit ints; magic bitboards precompute sliding-piece attacks — orders of magnitude faster~12 uint64 (one per piece type per color) + large precomputed attack tablesMillions of positions/sec — the throughput a deep alpha-beta search actually needsMuch harder to write, debug, and explain; magic-bitboard generation is its own multi-day project; a poor fit for "walk me through your design" interviewsProduction engines doing deep tree search, where move-gen is the hot loop called millions of times per move decided
Legality strategyWhen correctness is enforcedCostBuysCostsUse when
Pseudo-legal + filter (this kata: make → isKingAttacked → unmake, per candidate move)After generation, once per candidate moveO(moves) simulations, each re-deriving attacked squares from scratchSimple, obviously correct — ONE filter catches every interaction (pins, discovered checks, walking into check) with no special-casingRedundant work: re-computes "is X attacked" fresh for every candidate, even though most of the board didn't changeThis kata, most hobby/teaching engines, anywhere correctness-per-line-of-code matters more than raw throughput
Fully-legal generation (precompute pins + checkers first, generate only safe moves directly)Once per position, up front — before any move is generatedA pin/checker-detection pass, then generation with zero redundant simulationMuch faster in deep search — no make/unmake per candidate move at allMeaningfully more complex: must special-case single-check (block/capture/king-move only) vs double-check (king must move), en-passant's rare discovered-check case, etc.Search-heavy engines where move generation is called millions of times per search — the complexity buys real throughput
Movement-rule mechanismBuysCostsUse when
Polymorphism (Piece subclass per type, this kata)Rule and identity are one object; simplest mental model; instanceof King works for findKingA piece can't swap its movement rule at runtime without becoming a different typeFixed rule set — standard chess, teaching, interview settings
Strategy pattern (inject a MovementRule object per piece instance)Swap a piece's movement rule at runtime without losing its identity — e.g. pawn promotion keeps the same piece "instance"/history but swaps in the queen's rule; also natural for chess variants (Chess960/homebrew rules) where a piece's behavior is configured, not hard-typedExtra indirection: every piece carries a rule reference plus a registry of rule objects to wire upPromotion-heavy or variant-rich designs, or when an interviewer explicitly asks "what if a piece's move rule could change at runtime?"

7. Defend under drilling

8. You can now defend


Re-authored/Deepened for this guide. Model answer: the Design Chess LLD walkthrough.

🤖 Don't fully get this? Learn it with Claude

Stuck on Design Chess? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Design Chess** (Hands-On Builds) and want to truly understand it. Explain Design Chess from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Design Chess** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Design Chess** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Design Chess** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes