Chess Game
Pieces with movement rules, Board, Move, Game state. The cleanest polymorphism problem in the OOD canon.
Context#
A two-player chess game tracks an 8×8 board, two sets of six piece types (king, queen, rook, bishop, knight, pawn), a turn counter, a move history, and a game status that resolves to one of Checkmate | Stalemate | Draw | Resigned or InProgress. Players alternate moves; each move must be legal (the piece can move there, the destination isn’t blocked by a friendly piece, and the player’s own king isn’t left in check). The system must enforce castling, en passant, pawn promotion, and the various draw conditions (fifty-move rule, threefold repetition, insufficient material).
The problem is the cleanest polymorphism prompt in the OOD canon. The Game class does not switch on piece type — piece.getValidMoves(board, position) is the only call it makes. Every other case study has at least one place where you reluctantly admit an instanceof; chess does not. That is what makes it the favourite prompt for senior rounds.
The interviewer’s hidden objectives, in roughly the order they will be tested:
- Can you clarify scope — full FIDE rules vs. a tutorial-grade subset — without spinning out?
- Can you draw the class hierarchy with
Pieceabstract and six concrete subclasses? - Can you express movement rules polymorphically, with no
switch (piece.type)inGame? - Can you model
Moveas a Command so undo and history fall out naturally? - Can you defend trade-offs when the interviewer pushes (AI opponent, network play, three-fold repetition, PGN export)?
Requirements (functional and non-functional)#
Clarifying in the room is the most points-bearing part. The scope below is the one most interviewers expect; anything outside it should be flagged out-of-scope so you can finish.
Functional — in scope.
- Standard 8×8 board with the conventional starting position.
- Two players, alternating moves. White moves first.
- All six piece types with their full movement rules, including:
- Castling (king-side and queen-side) under FIDE preconditions.
- En passant capture.
- Pawn promotion to queen / rook / bishop / knight (player’s choice).
- A move is rejected if it leaves the moving player’s king in check.
- The game ends on checkmate, stalemate, resignation, or draw (fifty-move rule, threefold repetition, insufficient material, agreed draw).
- Undo the last move (one level deep for the interview; deeper as a follow-up).
Functional — out of scope (called out explicitly). Chess engine / AI opponent, network multiplayer, time controls and clocks, opening-book databases, PGN parsing, chess variants (Chess960, atomic). Acknowledge them so the interviewer knows you saw them.
Non-functional.
- A two-player human game in process; no concurrency requirements beyond “moves alternate”.
- Latency target: move validation under 10 ms (matters when extending to AI move generation).
- The design must be testable without a UI: positions construct programmatically, moves apply by API.
- The board representation must be cheap to copy for hypothetical-move checks (the check-detection step plays the move on a clone).
Use case diagram#
┌────────────────┐ ┌────────────────┐ │ Player W │ │ Player B │ └────────┬───────┘ └────────┬───────┘ │ │ ┌──────────────────┼─────┬────────────────────┤ ▼ ▼ │ ▼ [make move] [offer draw]│ [resign] │ │ │ │ └──────────────────┼─────┴────────────────────┘ ▼ ┌─────────────────────────────────────┐ │ Chess Game │ └────────┬────────────────────────────┘ ▼ ┌──────────────────────────────────────────┐ │ [validate move] [detect check/mate/draw]│ │ [undo move] [promote pawn] │ └──────────────────────────────────────────┘Two symmetric primary actors; the system’s interesting use cases all live downstream of make move.
Class diagram#
┌──────────────────────────┐ │ Game │ ├──────────────────────────┤ │ board : Board │ │ history : List<Move> │ ◇── Command log │ turn : Color │ │ status : GameStatus │ ◇── State (enum-driven) ├──────────────────────────┤ │ play(move) : Result │ │ undo() │ │ legalMoves(color) : List │ └─────────────┬────────────┘ │ owns ▼ ┌──────────────────────────┐ │ Board │ ├──────────────────────────┤ │ squares : Square[8][8] │ ├──────────────────────────┤ │ at(position) : Piece? │ │ place(piece, position) │ │ move(from, to) │ │ snapshot() : Board │ │ kingPosition(color) │ └──────────────────────────┘
┌──────────────────────────────────────────┐ │ Piece (abstract) │ ├──────────────────────────────────────────┤ │ color : Color │ │ hasMoved : boolean │ ├──────────────────────────────────────────┤ │ getValidMoves(Board, Position) : List<Position> │ ◀── the only polymorphic call Game makes │ symbol() : char │ └──────────────────────────────────────────┘ ▲ ┌──────┬──────┬─────┴────┬──────┬──────┬──────┐ ▼ ▼ ▼ ▼ ▼ ▼ ▼ King Queen Bishop Knight Rook Pawn
┌──────────────────────────┐ ┌──────────────────────────┐ │ Move │ │ Position │ ├──────────────────────────┤ ├──────────────────────────┤ │ from, to : Position │ │ row, col : int │ │ piece : Piece │ │ algebraic() : String │ │ captured : Piece? │ └──────────────────────────┘ │ kind : MoveKind │ │ (Normal/Castle/EnPassant/Promotion) │ │ promotedTo : PieceType? │ ├──────────────────────────┤ │ apply(board) │ ◀── Command pattern │ undo(board) │ └──────────────────────────┘Three patterns are doing the load-bearing work:
- Polymorphism on
Piece.getValidMoves— the single most important decision in the design. Six concrete subclasses each implement their own movement rule;Gamecalls one method and gets a list back. - Command pattern on
Move— every move carries enough info toapply(board)andundo(board). The move history is a command log; undo and audit fall out for free. - State on
Game.status—InProgress → Check → (Checkmate | InProgress) → ... → (Stalemate | Checkmate | Draw | Resigned). The transitions happen after every successful move.
What is not in the diagram and that is deliberate:
- No
Playerclass doing logic. A player is just aColorfor the interview — humans drive the UI. If AI arrives,Playergains achooseMove(game)method; the game does not change. - Pawn promotion is a
MoveKind, not a subclass ofPawn. The pawn promotes to a different piece type on the board; the piece on the next move is aQueen, not a “promoted pawn”. No subclass needed. - No
MoveValidatorgod class. Validation lives in two places: the piece (getValidMoves) decides geometric legality; the game (play) decides that the move doesn’t leave the king in check. Each layer has its job.
Sequence diagram (key flows)#
The play(move) flow:
Player Game Board Piece Move │ play(m) │ │ │ │ │────────►│ │ │ │ │ │ board.at(m.from) │ │ │ │──────────────►│ │ │ │ │ piece │ │ │ │ │◄──────────────│ │ │ │ │ piece.getValidMoves(board, m.from) │ │ │──────────────────────────────►│ │ │ │ legal targets │ │ │ │◄──────────────────────────────│ │ │ │ check m.to ∈ legal │ │ │ │ m.apply(board) │ │ │──────────────────────────────────────────────►│ │ │ inCheck(turn) on clone? │ │ │ if yes → m.undo(board) ; reject │ │ │──────────────────────────────────────────────►│ │ │ update history, status, turn │ │ result │ │ │◄────────│ │Castling, in particular, looks like this — KingsideCastleMove.apply moves both king and rook, and undo reverses both. The game doesn’t know it was castling; it asked the king for legal moves and (e1 → g1) was among them with MoveKind.CASTLE.
The undo flow:
Player Game Move(latest) Board │ undo() │ │ │ │───────►│ │ │ │ │ history.removeLast() │ │ │──────────────►│ │ │ │ m.undo(board) │ │ │─────────────────────────────────►│ │ │ recompute status; flip turn │Move.undo carries the captured piece (if any), so the reverse is exact. The board does not need a history of its own.
Activity diagram (for non-trivial state)#
The game’s status state machine:
┌─────────────┐ │ InProgress │◄────────────────────────────┐ └──────┬──────┘ │ │ play(move) accepted │ ▼ │ ┌─────────────────────┐ │ │ opponent in check? │ │ └─────┬───────────┬───┘ │ yes │ no │ │ ▼ ▼ │ ┌──────────┐ ┌─────────────────────┐ │ │ Check │ │ opponent has any │ │ └────┬─────┘ │ legal move? │ │ │ └─────────┬───────────┘ │ ▼ no │ yes │ ┌────────────────────┐ ▼ │ │ opponent has any │ (back to InProgress)───────┘ │ legal move? │ └─────┬──────────┬───┘ no │ yes│ ▼ ▼ ┌──────────┐ (back to InProgress, opponent in check) │Checkmate │ └──────────┘
Also reachable from InProgress: Stalemate (no legal move, not in check) Draw (fifty-move / threefold / insufficient material / agreement) Resigned (player resigns)The fifty-move rule and threefold repetition are computed against the move history; insufficient material is computed against the board. None of these need a separate state — they each transition InProgress → Draw directly.
Java implementation#
A representative slice; the rest is mechanical.
public enum Color { WHITE, BLACK; public Color other() { return this == WHITE ? BLACK : WHITE; }}
public final class Position { public final int row, col; public Position(int row, int col) { this.row = row; this.col = col; } public boolean inBounds() { return row >= 0 && row < 8 && col >= 0 && col < 8; } public Position offset(int dr, int dc) { return new Position(row + dr, col + dc); }}
public abstract class Piece { protected final Color color; protected boolean hasMoved; protected Piece(Color color) { this.color = color; } public Color color() { return color; } public boolean hasMoved() { return hasMoved; } public void markMoved() { this.hasMoved = true; }
/** Geometrically legal targets; does NOT consider self-check. */ public abstract List<Position> getValidMoves(Board board, Position from); public abstract char symbol();}
public final class Knight extends Piece { private static final int[][] DELTAS = {{-2,-1},{-2,1},{-1,-2},{-1,2},{1,-2},{1,2},{2,-1},{2,1}}; public Knight(Color c) { super(c); } public List<Position> getValidMoves(Board board, Position from) { List<Position> out = new ArrayList<>(8); for (int[] d : DELTAS) { Position to = from.offset(d[0], d[1]); if (!to.inBounds()) continue; Piece occupant = board.at(to); if (occupant == null || occupant.color() != color) out.add(to); } return out; } public char symbol() { return color == Color.WHITE ? 'N' : 'n'; }}
public final class Rook extends Piece { private static final int[][] DIRS = {{-1,0},{1,0},{0,-1},{0,1}}; public Rook(Color c) { super(c); } public List<Position> getValidMoves(Board board, Position from) { return slidingMoves(board, from, color, DIRS); } public char symbol() { return color == Color.WHITE ? 'R' : 'r'; }}
/** Helper shared by Rook, Bishop, Queen — each just supplies its direction set. */static List<Position> slidingMoves(Board board, Position from, Color mine, int[][] dirs) { List<Position> out = new ArrayList<>(); for (int[] d : dirs) { Position p = from.offset(d[0], d[1]); while (p.inBounds()) { Piece occ = board.at(p); if (occ == null) { out.add(p); } else { if (occ.color() != mine) out.add(p); break; } p = p.offset(d[0], d[1]); } } return out;}
public final class Move { public enum Kind { NORMAL, CASTLE_KINGSIDE, CASTLE_QUEENSIDE, EN_PASSANT, PROMOTION } private final Position from, to; private final Piece piece; private final Piece captured; // nullable; en-passant captures the pawn behind 'to' private final Kind kind; private final Class<? extends Piece> promoteTo;
public Move(Position from, Position to, Piece piece, Piece captured, Kind kind, Class<? extends Piece> promoteTo) { this.from = from; this.to = to; this.piece = piece; this.captured = captured; this.kind = kind; this.promoteTo = promoteTo; }
public void apply(Board board) { switch (kind) { case NORMAL, EN_PASSANT -> { if (kind == Kind.EN_PASSANT) board.remove(epCapturedSquare()); board.move(from, to); piece.markMoved(); } case CASTLE_KINGSIDE -> { board.move(from, to); // king e1→g1 board.move(new Position(from.row, 7), new Position(from.row, 5)); // rook h1→f1 piece.markMoved(); } case CASTLE_QUEENSIDE -> { board.move(from, to); board.move(new Position(from.row, 0), new Position(from.row, 3)); piece.markMoved(); } case PROMOTION -> { board.move(from, to); board.place(instantiate(promoteTo, piece.color()), to); } } }
public void undo(Board board) { // Symmetric: move piece back; restore captured; un-promote if needed. // (Omitted for brevity; one branch per Kind.) }}
public final class Game { private final Board board = Board.starting(); private final List<Move> history = new ArrayList<>(); private Color turn = Color.WHITE; private GameStatus status = GameStatus.IN_PROGRESS;
public Result play(Move m) { if (status != GameStatus.IN_PROGRESS) return Result.GAME_OVER; Piece p = board.at(m.from()); if (p == null || p.color() != turn) return Result.ILLEGAL; List<Position> targets = p.getValidMoves(board, m.from()); if (!targets.contains(m.to())) return Result.ILLEGAL;
m.apply(board); if (board.isInCheck(turn)) { // self-check check m.undo(board); return Result.LEAVES_KING_IN_CHECK; } history.add(m); turn = turn.other(); status = computeStatus(); // Check / Checkmate / Stalemate / Draw / InProgress return Result.OK; }
public void undo() { if (history.isEmpty()) return; Move last = history.remove(history.size() - 1); last.undo(board); turn = turn.other(); status = computeStatus(); }}Notes the interviewer will look for:
getValidMovesis the only polymorphic call. Every piece-specific decision happens behind that one method. If you seeif (piece instanceof Pawn)inGame, the design has failed even if the rest is correct.- Self-check check uses a real apply/undo, not a clone. The Command pattern earns its keep here — applying and undoing is cheaper than copying the board every time, and the move object already has all the info.
slidingMovesis shared across Rook / Bishop / Queen by passing the direction set. That’s reuse without inheritance — the queen does not extend rook.- Castling preconditions live on the King’s
getValidMoves, not on the move. The king knows: it hasn’t moved, the rook hasn’t moved, squares between are empty, the king isn’t in check, doesn’t pass through check, doesn’t land in check.
Trade-offs and extensions#
Decisions explicitly made and what they cost:
| Decision | Why | Cost if requirements change |
|---|---|---|
Piece abstract base + six subclasses | Movement is genuinely per-type; polymorphism is the right answer. | None — this is the textbook choice. |
Command pattern on Move | Undo, history, and self-check check all reuse the same abstraction. | None — necessary for any non-toy chess implementation. |
| Apply/undo on a real board instead of cloning | Boards are cheap to mutate; clones are expensive at any non-trivial depth. | If concurrent search (parallel alpha-beta) arrives, the board must be either thread-local or copy-on-write. |
Position as a value object | Equality, hashing, and printing work the way you’d expect. | None. |
MoveKind enum on Move instead of subclasses | Move is data, not behaviour — the behaviour lives on apply. A CastleMove subclass would be over-modelled. | A move-grading AI may want subclass-like extensibility; the enum is still fine, with grading logic in a separate visitor. |
Promotion as a MoveKind, not a Pawn subclass | The pawn becomes a queen on the next move — there is no “promoted pawn” entity. | None. |
isInCheck walks the opponent’s pieces | O(piece_count × move_count) per check — small constant in chess. | If you add an AI that calls this per ply, switch to attack maps (bitboards). |
Likely follow-up extensions and the shape of the answer:
- AI opponent.
Playergains achooseMove(Game)method;HumanPlayerreads from the UI,MinimaxPlayerruns search. The game does not change. - Network play.
Moveis already serialisable (positions, kind, promotion target). Add aMoveTransportinterface; the local game just receives moves over the wire instead of from the keyboard. - Threefold repetition. Hash the position after every move; store the count keyed by Zobrist hash; transition to
Drawwhen any hash hits 3. - PGN export. A formatter that walks
historyand prints algebraic notation. The move object already carries enough — disambiguation needs to know which other pieces could have reached the same square (agetValidMovesquery suffices). - Time controls. A
Clockper side; on every turn switch, pause one and start the other. Time-out is another transition intoResigned(or its ownTimeForfeitstatus). - Chess960 / variants. The starting position is no longer the FIDE one; the king’s castling preconditions depend on starting rook files. Subclass
Boardor factor aStartingPositionstrategy.
Mock interview follow-ups#
Questions interviewers reach for and the briefest correct answer:
- “Where does the
instanceof piece typeswitch live?” — Nowhere. That’s the design’s whole point. Each piece’sgetValidMovesis its own implementation;Gamecalls one method. - “How do you check that a move doesn’t leave the king in check?” — Apply the move, ask
isInCheck(turn), undo if true. The Command pattern makes this two lines. - “How do you implement castling?” — Three things have to be true (king and rook haven’t moved, squares between are empty, king isn’t in or passing through or landing in check). The king’s
getValidMovesreturnsg1/c1withMoveKind.CASTLE_*when they hold; the move’sapplyshuffles both pieces. - “What about en passant?” — A pawn that just moved two squares is capturable on the next move only. Stash the last move on the game (already there in
history); the pawn’sgetValidMovesconsults it. The move’s kind isEN_PASSANTandapplyremoves the captured pawn from the square behind the destination. - “How do you detect checkmate vs. stalemate?” — After the move, ask: is the side-to-move in check, and do they have any legal move? Both → checkmate. Only the second is false → stalemate. Iterating “any legal move” means walking every piece’s
getValidMovesand filtering by self-check. - “How do you handle pawn promotion if the player wants a knight, not a queen?” — The move carries
promoteTo. The UI prompts; the move is constructed with the chosen target. The piece swap happens inapply. - “How do you support undo more than one move deep?” —
historyis already a list;undo()already operates on the last entry. Loop. The cost is replaying status detection, which is cheap.
Related#
- Parking Lot — the sibling case study; State + Strategy in the same proportions.
- Command Pattern —
Moveis the textbook command, complete with undo. - State Pattern —
Game.statusis a clean state machine. - Strategy Pattern —
Piece.getValidMovesis per-class strategy; the sibling reuse pattern. - Approaching the OOD Interview — the meta-script that produced this writeup’s structure.