SwadhyAI Notes

All Low-Level Design notes

Domain rules, then Command

Chess

Chess LLD is about assigning movement rules and game control correctly: each answers its legal moves, the answers occupancy and check, and a supports execute/undo. Example: a knight on b1 moves in an L-shape and jumps. A 200-case switch inside Board.move() will not leave time for undo, let alone castling.

Who knows how a piece moves, and how do I undo without rewriting history by hand? You will not finish a full rules engine in forty-five minutes. Say the scope: movement, capture, check, undo. Castle if time. That sentence is part of the design.

Interview path

1

Scope out loud

Movement, capture, check, undo. Not a chess.com clone; castle if the clock allows.

2

Piece answers legalMoves

Each type implements Piece. Board answers occupancy and isAttacked (under attack).

3

Move is a Command

execute/undo on a history stack. Take-back and replay fall out without Board rewrites.

What actually happens after the first legal knight move

After you place Nb1-c3, the interesting work is not printing the board. Check (king attacked), checkmate (no legal escape), and stalemate (no legal move, king safe) are queries after a tentative move. Castling and en passant (capture a pawn that just double-stepped) are special moves with extra preconditions. Put them on King and Pawn as extra types, not on . Game is the controller: whose turn, push command, switch side, detect end.

Color is a value. Position is a value. Piece identity on the board is occupancy, not a database id. Do not subclass Player into WhitePlayer with different physics. White and Black share the same movement rules with a direction for pawns. See Command and domain modeling.

Tentative execute, then undo if the king is in check, is simpler than generating only "safe" moves in one pass, as long as undo is correct. That is why the is not decoration here. Mutating the board with no history makes take-back "remember the last square in a variable."

Simple rule of thumb

Before drawing classes, decide who owns geometry versus who owns the turn. Movement belongs on the ; occupancy and check belong on the ; history belongs on a stack inside .

  • Say scope first: movement, capture, check, undo; castling only if time remains.
  • Never put a piece-type switch inside Board.move; every new rule would edit Board.
  • Reject leaving your own king in check via execute, query, undo, then throw.
  • Color is a value on Piece; WhitePlayer subclasses are not different physics.

Who knows the L-shape?

With ownership settled, each piece type still needs a clear rule. The grid below is what should encode; only answers occupancy and whether a square is attacked.

Knight

L-shape jumps; Board checks destination occupancy, never a blocked path.

Bishop / Rook / Queen

Rays ask Board for the first occupied square along each direction.

Pawn

Color sets direction; double-step, diagonal capture, en passant as special commands.

King

One step plus castling, with not-moved and not-through-check preconditions.

Play, then maybe undo

The sequence makes the tentative-move loop concrete: asks whose turn it is, a changes occupancy on the , then king safety decides push or undo.

Diagram text: the exact Mermaid definition below preserves the nodes, labels, and connections. Enable JavaScript to view the rendered diagram.
Read the diagram definition
sequenceDiagram
    participant U as Caller
    participant G as Game
    participant M as MoveCommand
    participant B as Board
    participant K as King safety

    U->>G: play(move)
    G->>G: whose turn?
    G->>M: execute(board)
    M->>B: occupancy change
    G->>K: inCheck(side to move)?
    alt leaves own king in check
        G->>M: undo(board)
        G-->>U: illegal
    else
        G->>G: push history, flip side
        G->>G: mate / stalemate query
    end

Java API sketch

The sketch below is the same ownership story in Java: , , , and with a history deque.

java
interface Piece {
  Color color();
  List<Square> legalMoves(Square from, Board board);
}

final class Square {
  final int file;
  final int rank;
}

interface MoveCommand {
  Square from();
  void execute(Board board);
  void undo(Board board);
}

class NormalMove implements MoveCommand {
  private final Square from;
  private final Square to;
  private Piece captured;
  public Square from() { return from; }
  public void execute(Board board) {
    captured = board.at(to).orElse(null);
    board.move(from, to);
  }
  public void undo(Board board) {
    board.move(to, from);
    if (captured != null) board.place(to, captured);
  }
}

class Board {
  Optional<Piece> at(Square s);
  boolean isAttacked(Square s, Color by);
  void move(Square from, Square to);
}

class Game {
  private final Board board;
  private final Deque<MoveCommand> history = new ArrayDeque<>();
  private Color side = Color.WHITE;

  void play(MoveCommand move) {
    Piece moving = board.at(move.from()).orElseThrow(IllegalMove::new);
    if (moving.color() != side) throw new IllegalMove();
    move.execute(board);
    if (board.kingInCheck(side)) {
      move.undo(board);
      throw new IllegalMove();
    }
    history.push(move);
    side = side.opposite();
  }

  void undo() {
    MoveCommand last = history.pop();
    last.undo(board);
    side = side.opposite();
  }
}

Invalid cases, concurrency, and what still breaks

The API above assumes every play is legal. These cases are how the design says no, how two threads collide, and which follow-ups stay in scope without rewriting Board.

Invalid cases

  • Moving the opponent's piece: Game compares side to move against Piece.color().
  • Sliding a bishop through a pawn: ray generation stops at occupancy; capture enemy only.
  • Leaving your king in check: execute, query, undo, reject; public API never shows it.
  • Castling in check or through attack: King's command checks isAttacked on the path.
  • Promoting a pawn to a king: promotion is a constrained set of piece types on the command.
  • Undo with an empty history: Game.undo rejects; the stack is the source of truth.

Concurrency

A chess game is one mutable . Two threads calling play() corrupt the position. Confine the game to one thread, or synchronize the facade. Do not lock each Square independently; you would deadlock on captures.

What the follow-up changes

  • Castling, en passant, promotion: new types; play still execute, check, push.
  • export: observer or logger wrapping Command, not a second Board write path.
  • AI: a MoveStrategy that generates legal commands, same idea as tic-tac-toe bots, heavier generator.

Still fails when…

  • Undo is only a saved int; take-back of captures and castling dies.
  • Public API exposes the tentative execute that left the king in check.
  • A 200-case switch on Board leaves no time for a correct history stack.

Trade-offs

  • Tentative execute needs correct undo; generating only safe moves is harder.
  • One Game lock is honest for machine coding; per-square locks invite deadlock.
  • You will not finish FIDE (full official rules); you will finish a history stack.

Wrap-up

You have ownership, the play loop, and the rejections. Use the forty-five-minute plan, the seam test, and the related notes to lock it in.

Forty-five minutes

  1. 0–8Scope sentence. Implement Knight.legalMoves for the L-shape.
  2. 8–20Play Nb1-c3. Undo restores the knight to b1.
  3. 20–32Move that leaves king in check: execute, undo, throw.
  4. 32–40History stack is the Command; name execute and undo out loud.
  5. 40–45Castle only if time. Do not start with PGN logging.

Tests that prove the seams

java
@Test
void undoRestoresKnight() {
  Game g = Game.standard();
  MoveCommand m = new NormalMove(Square.parse("b1"), Square.parse("c3"));
  g.play(m);
  assertTrue(g.board().at(Square.parse("c3")).isPresent());
  g.undo();
  assertTrue(g.board().at(Square.parse("b1")).isPresent());
}

Key takeaway

Pieces validate their own moves, the board owns occupancy, and check queries use the same attack rules. Adding a fairy piece means a new Piece class, not a new switch branch in Game. You're done when undo restores captures and a move that leaves the king in check never sticks on the public board.