Facebook Pixel

Design a Chess Move Validator

Chess is a game problem and a textbook example of polymorphism: the state lives in a board, and each piece type encodes its own movement rules. The problem is harder than Tic-Tac-Toe because the piece hierarchy is non-trivial and the movement rules vary widely across six types.

This case study covers movement legality and capture detection only. Check, checkmate, castling, en passant, and promotion are deliberately out of scope — calling them out explicitly is part of the interview answer. They are the harder half of a full chess engine, and the Extensions section shows how each one slots back onto this design — check detection, for instance, reuses the very can_move hierarchy built here — so cutting them from the core is a scoping choice, not a dead end. A clean bounded design is worth more than an incomplete attempt at everything.

See It in Action

The demo below runs the finished move validator. Click any white piece to highlight its legal destinations: green dots are quiet moves, red rings are captures. Notice the sliding pieces (queen, rook, bishop) stop at the first blocker, knights jump, and no piece may land on a friendly square. Each piece type computes its own moves — the polymorphism this article builds.

Chess Move Validator

Clarifying Requirements

"Design a chess move validator" splits cleanly into what the validator must handle and what it can safely defer.

In scope: place a piece on any square of an 8×8 board using algebraic notation (files a–h, ranks 1–8); validate a move for the piece at the source square according to that piece type's movement pattern; detect a blocked path for sliding pieces (Queen, Rook, Bishop); reject a move to a square occupied by a same-color piece; execute a capture when the destination holds an opponent's piece.

Out of scope: check and checkmate detection, castling, en passant, pawn promotion, turn enforcement, and full game lifecycle. These features exist in chess — the point is to name them so the interviewer knows you know, and so the design stays bounded. A validator that handles movement legality is a complete, testable system. Adding check detection before the core is solid produces a buggy incomplete system.

The decision flow for a single move command captures all the rules at once:

Core Entities

Scanning the requirements for nouns that carry state or enforce rules turns up three candidates: a square, a piece, and a board. The square is the one worth pausing on. "e4" reads like an object, but before promoting it, decide whether a Square carries any state or behavior that a plain coordinate does not.

Decision checkpoint

1)

Should Square (an algebraic address like "e4") be its own class?

A Square is an algebraic address — "e4" — that maps to a 0-indexed row and column on the board. It is a pure coordinate, not an object with fields, so it does not become a class.

A Piece has a color (W or B) and a piece type (K Q R B N P). Crucially, each piece type knows its own movement pattern. This is the decisive design observation: rather than writing a switch-on-type function in Board, each piece type encodes its rule in a can_move method. The result is a clean hierarchy where adding a new piece type requires only a new subclass.

A Board owns the 8×8 grid and exposes three operations: place a piece on a square, execute a validated move, and query what piece sits at a square.

Designing the Classes

Six piece types each move differently, and board.move() has to validate any of them. There are two ways to structure that. One is a single validator in Board that branches on piece type — if piece_type == "Q": ... elif piece_type == "R": .... The other is to give each piece type its own can_move method and let the runtime dispatch. Before reading on, decide which keeps Board stable as piece types are added or changed.

Decision checkpoint

1)

Six piece types move differently. Should Board.move branch on piece type with an if/elif chain, or should each piece type own a can_move method?

Piece-movement logic belongs to the piece, not to the board. board.move() calls piece.can_move(board, fr, fc, tr, tc), and the dispatch is handled by the language runtime, not by a chain of if piece_type == "Q" conditionals. Adding a new piece type is a new class, not a change to Board.

Piece (abstract base)

Piece carries color and piece_type and exposes token() which returns the two-character string like "WN". The abstract can_move takes the board (for path inspection) and integer coordinates for both source and destination. Sliding pieces (Queen, Rook, Bishop) share a _path_clear helper: it walks the line from source to destination one step at a time and returns False if any intermediate cell is occupied. The helper is on Piece because all three sliding types need it.

The coordinate system used throughout is (rank, file) with rank 0 = rank 1, rank 7 = rank 8, file 0 = a, file 7 = h. The conversion sq_to_rc("e4") gives (3, 4). All movement math uses these integer coordinates.

King

The King moves at most one square in any direction. The check is max(|dr|, |dc|) == 1, which is true for all eight adjacent squares and false for everything else.

Queen

The Queen combines Rook and Bishop movement. A destination is reachable if the move is on the same row, same column, or a diagonal (|dr| == |dc|). Any valid pattern still requires the path to be clear.

Rook

The Rook requires the move to stay on the same row or same column (fr == tr or fc == tc) and then confirms the path is clear.

Bishop

The Bishop requires a diagonal move (|dr| == |dc|) and a clear path.

Knight

The Knight's L-shape does not require a clear path — it jumps. The pattern is (|dr|, |dc|) in {(1,2), (2,1)}. No path check needed.

Pawn

The Pawn is the most context-sensitive piece. White moves toward higher ranks (direction = +1), Black toward lower ranks (direction = −1). From the starting rank (rank 1 for White, rank 6 for Black) a pawn may advance two squares to an empty square, with the intermediate square also empty. From any other rank, a forward move of one square to an empty square is the only non- capture option. A pawn captures exactly one square diagonally forward, and only when the destination is occupied by an opponent — a forward diagonal to an empty square is not legal.

Board

Board owns a 8×8 grid initialized to None. place(token, sq) creates the right Piece subclass from the two-character token and sets the grid cell. move(from_sq, to_sq) runs a three-gate check — piece present, destination not same-color, movement pattern + path clear — then executes the move. The three gates have to run in some order; one ordering hides a bug. Before reading on, decide whether the same-color gate can come after can_move, given that a sliding piece's can_move only checks the path, not the destination occupant.

Decision checkpoint

1)

Must the same-color destination check run before or after piece.can_move?

at(sq) is a direct lookup.

Board does not know piece rules. It calls piece.can_move(...) and trusts the answer. This is the Tell-Don't-Ask principle: Board tells each piece to validate its own move instead of inspecting the piece type and applying the rule itself.

Try It Yourself

Build the move validator. Implement run_chess(instructions) that processes a sequence of commands and returns one output line per command.

Each instruction is a list of strings. Three commands are supported:

["place", piece, square] — place the piece (e.g. "WN") on the given square (e.g. "g1"). Returns "Placed WN at g1".

["move", from, to] — attempt to move the piece at from to to. Returns the move string on success, "Illegal move" on failure (wrong pattern, blocked path, own piece at destination, or no piece at source).

["at", square] — returns the piece token at the square, or "empty".

Piece tokens are two characters: color (W or B) + type (K Q R B N P). Squares are algebraic like "e4". White pawns move toward higher rank numbers; black pawns toward lower.

On a legal move to an empty square: "WN g1-f3". On a capture: "WN takes BP g1xf3".

Invest in Yourself
Your new job is waiting. 83% of people that complete the program get a job offer. Unlock unlimited access to all content and features.
Go Pro