Chess
Advanced · 18 min read · ▶ live playground · ✦ checkpoint
“Design a chess game.” Six piece types and sixty-four squares sound finite, but legal movement is only the first layer: a move can look correct and still expose its own king. The interview is testing whether you can turn that heavyweight rule set into a small ownership map without drawing every rule as a class.
Requirements
Start with standard chess and name the exclusions. Otherwise, “legal move” can expand from piece geometry into clocks, notation, online play, and tournament draw rules while you are still sketching the board.
| Kind | Requirement for this design |
|---|---|
| Functional | The game uses an 8×8 board addressed by unambiguous from/to coordinates. |
| Functional | White and Black each have King, Queen, Rook, Bishop, Knight, and Pawn pieces. |
| Functional | Every piece enforces its own movement shape; line-moving pieces also require a clear path. |
| Functional | Two human players alternate turns, with White moving first. |
| Functional | A move must start with the current player's piece and may not land on that player's piece. |
| Functional | A legal move must not leave the moving player's own king in check. |
| Functional | Moving onto an opponent piece captures and removes that piece. |
| Functional | The game detects whether the opponent's king is in check after a move. |
| Functional | The game detects checkmate when a checked player has no legal reply. |
| Functional | The game detects stalemate when an unchecked player has no legal move. |
| Functional | The game tracks active, check, checkmate, or stalemate status and retains accepted moves. |
| Non-functional | Piece rules remain independent of console, web, networking, or persistence code. |
Clocks and timers, notation import/export, computer AI, matchmaking, spectators, and networking are out of scope. Castling, en passant, and promotion are important standard rules; we will place them in the design below, while the compact executable skeleton focuses on the representative movement and validation core.
Clarifying questions to ask
Every answer either changes the validation pipeline or introduces history that the board alone cannot provide.
- Standard chess only, or variants too? Standard two-player chess on an 8×8 board. Chess960 placement, custom pieces, alternative boards, and team variants are separate rule sets rather than flags scattered through
Game.move(). - Are castling, en passant, and promotion included? They belong in the complete design. The core skeleton leaves them outside its representative three-piece implementation, and the extended concerns section identifies their state and commit rules explicitly.
- How is a move entered? As from/to coordinates such as
e2 → e4. Algebraic notation is a presentation and parsing concern; the domain receives validated board coordinates. - Are both players human? Yes. A controller supplies a player ID with each move. An AI may later choose a coordinate, but it must submit through the same
Game.move()boundary. - Who moves first? White. An illegal move does not change the turn, board, status, or history.
- Can a player capture the king? No. Chess ends at checkmate. Attack detection may ask whether a piece could reach the king's square, but an accepted move never removes a king.
- What does path clear mean? Every square strictly between source and destination must be empty for a rook, bishop, or queen. Knights jump, while kings and ordinary pawn steps travel only one square.
- How is check reported?
move()returns a structured result with capture, status, whether the opponent is in check, and the next player. The core does not print chess prose. - How do we decide checkmate versus stalemate? Generate every legal reply for the side to move. No reply plus a current attack on its king is checkmate; no reply without that attack is stalemate.
- Which draw rules are required? Stalemate is required here. Threefold repetition, the fifty-move rule, insufficient material, and draw by agreement need additional history and product rules, so they remain outside this critical flow.
- Do we need undo or replay? Not as a public command, but accepted moves are immutable records. That gives a future undo, replay, audit, or notation adapter a stable fact instead of reconstructing intent from the board.
- What happens on promotion? The moving player chooses Queen, Rook, Bishop, or Knight when a pawn reaches its final rank. The choice belongs to the move command or promotion rule, not to a UI-only mutation after the move.
- Can two clients submit a turn concurrently? The skeleton assumes serialized calls. A networked game needs a game version or transaction so two commands cannot both pass the same current-turn check.
- What performance target matters? Clarity first. A straightforward legal-move scan over sixty-four destinations is appropriate for machine coding; an AI search may later justify a compact board representation, cached attacks, and tighter allocation.
Entity extraction
Apply the noun-and-verb pass from requirements-to-classes before making sixty-four square objects the center of the design:
- A chess game has two players, a current turn, a status, and a move history.
- A board contains sixty-four cells or squares, each addressed by a position.
- A piece has a color and one of six piece types: king, queen, rook, bishop, knight, or pawn.
- A move joins a source, destination, moved piece, and optional captured piece.
- A move validator or the game coordinates piece legality, path checks, simulation, and king safety.
Sort those nouns by identity, value, actor, and varying behavior:
| Bucket | Nouns | Model choice |
|---|---|---|
| Aggregate / orchestrator | chess game | Game owns turn, status, accepted history, and the validation order |
| Board state | board, cell, position | Board owns an 8×8 grid of Cell; position is a small value |
| Polymorphic entity | piece | Abstract Piece with one concrete subtype per movement rule |
| Domain entity | player | Player joins identity to one Color |
| Immutable record | move, capture | Move stores what an accepted command changed |
| Enum | color, game status | Color and GameStatus are finite vocabularies |
| Rule collaborator | move validator, attack map | An attack map is the set of squares one side threatens; the rule may be extracted when Game grows |
| Actor / boundary | human, UI, notation parser, network client | Submits a move but does not own chess invariants |
The verb pass assigns each rule to the object that owns the needed facts:
| Verb phrase | Owner | Reason |
|---|---|---|
| find a piece or square | Board | The board owns coordinate bounds and occupancy |
| decide whether the movement shape is legal | concrete Piece | Rook, Knight, King, and the other types vary by movement |
| decide whether a path is clear | Board, queried by line pieces | Only the board knows intervening occupancy |
| enforce turn and reject a finished game | Game | Turn and lifecycle are game-level state |
| simulate and reject self-check | Game or MoveValidator | The rule needs the whole board and both sides' attacks |
| apply a move and report capture | Board | Source and destination occupancy change together |
| detect checkmate or stalemate | rules layer coordinated by Game | The answer combines king attack with all legal replies |
| record an accepted move | Game stores immutable Move | History changes only after every validation succeeds |
The hierarchy uses inheritance from inheritance only where the is-a statement is honest: a rook is a piece. It gains subtype polymorphism from polymorphism: Game asks piece.canMove(...), and runtime dispatch selects the concrete movement rule without a six-case switch in the game.
Core class diagram
The core shows three representative pieces. Queen, Bishop, and Pawn follow the same Piece shape; special-move collaborators stay in prose so the diagram remains a readable nine-node ownership map.
class diagram
Core chess board and polymorphic piece model
- 1Game owns turn, lifecycle, history, and the ordered move-validation use case.
- 2Game delegates coordinate, occupancy, simulation, and commit work to Board.
- 3Exactly two Players give external identities to the two turns.
- 4Each Player owns one Color from the finite White and Black vocabulary.
- 5Board contains sixty-four Cells, each protecting one position and occupant.
- 6A Cell is empty or associates its coordinate with one abstract Piece.
- 7Every Piece retains the Color used for turn and capture checks.
- 8Rook overrides movement with straight-line geometry and a clear-path query.
- 9Knight overrides movement with an L shape and does not inspect the path.
- 10King supplies one-square geometry; the rules layer separately protects it from check.
Piece.canMove() answers pseudo-legal movement: movement shape, destination ownership, and any path requirement. It does not claim that the whole chess move is legal. Game still has to simulate that move and prove the moving side's king remains safe.
The diagram omits Move because it is a small immutable record in the critical flow, not another behavior owner. It also omits a separate MoveValidator; extracting one is useful when full tournament rules make Game too large, but adding it before the pipeline is understood would only rename the coordinator.
Extended concerns
- The full six-piece hierarchy:
Queencombines rook-like straight movement with bishop-like diagonals.Bishopmoves diagonally and asks the board for a clear path.Pawndepends on color for forward direction, moves without capturing, captures diagonally, and has a first-move rule. All six answer the same movement contract, so callers stay polymorphic. - The move-validation pipeline: Check active game and current player; find the source piece; reject a friendly destination; ask the piece for pseudo-legal movement; simulate on a board copy; locate the moving side's king and test the opponent's attack map; commit once; then record the move. A rejection before commit leaves board, turn, and history unchanged.
- Check, checkmate, and stalemate: Check means the king's square is attacked. Generate candidate replies through each piece, filter them through the same simulate-and-test rule, and ask whether any legal reply remains. Checked plus none is checkmate; unchecked plus none is stalemate. Attack generation must use pseudo-legal movement rather than recursively asking whether every attacking move leaves its own king safe.
- Castling: The rule needs unmoved king and rook history, empty squares, a king that is not currently checked, and an attack-free path for the king. Represent the accepted result as one compound move that relocates both pieces atomically; do not make every caller remember a second rook move.
- En passant: A pawn alone cannot decide it because eligibility depends on the opponent's immediately previous two-square pawn move. A rules layer can recognize the move and produce a capture whose removed square differs from the destination.
- Promotion: Reaching the final rank creates the player's selected Queen, Rook, Bishop, or Knight and replaces the pawn during the same commit. A small piece factory or promotion rule owns that creation choice; the UI only supplies the selection.
Local movement belongs in overrides. Rules that depend on several pieces, attacked squares, or move history belong in the game rules layer. Forcing castling and en passant entirely into King.canMove() or Pawn.canMove() would make those objects reach through strangers and duplicate commit logic.
Critical-flow sequence diagram
The flow worth drawing is “make a validated move.” The path-query messages show a rook-like branch; a knight or king may answer canMove() without them.
sequence diagram
Make a move without exposing the moving side's king
- 1Game verifies lifecycle and turn, then asks Board for the source occupant.
- 2Dynamic dispatch checks piece geometry; a line piece also queries the path.
- 3Game tests king safety on a simulated board before any real state changes.
- 4Only a safe move reaches Board.apply(), which returns the captured occupant.
- 5Game evaluates the opponent, records one immutable move, and advances the turn.
If simulation finds the moving side's king attacked, messages 10 through 14 disappear. That rejection is why self-check cannot be an after-the-fact status update: applying first and attempting to undo later risks leaking capture, history, or event side effects.
Checkmate and stalemate reuse the same flow in search mode. Candidate moves stop before durable recording, while each simulated result asks whether the moving king is safe.
TypeScript skeleton
The skeleton implements Rook, Knight, and King movement, including real clear-path checks, capture, turn enforcement, simulate-then-test self-check, and legal-reply scanning. Queen, Bishop, Pawn, and the three special moves plug into the extended design above.
export {};
const BOARD_SIZE = 8;
enum Color {
White = "white",
Black = "black",
}
enum GameStatus {
Active = "active",
Check = "check",
Checkmate = "checkmate",
Stalemate = "stalemate",
}
interface Position {
readonly row: number;
readonly col: number;
}
function opposite(color: Color): Color {
return color === Color.White ? Color.Black : Color.White;
}
function samePosition(a: Position, b: Position): boolean {
return a.row === b.row && a.col === b.col;
}
function parseSquare(value: string): Position {
if (!/^[a-h][1-8]$/.test(value)) {
throw new Error("square must look like a1 through h8");
}
return {
col: value.charCodeAt(0) - "a".charCodeAt(0),
row: Number(value[1]) - 1,
};
}
function squareName(position: Position): string {
const file = String.fromCharCode(
"a".charCodeAt(0) + position.col,
);
return file + String(position.row + 1);
}
abstract class Piece {
public abstract readonly name: string;
public constructor(public readonly color: Color) {}
public abstract canMove(
board: Board,
from: Position,
to: Position,
): boolean;
protected canLand(board: Board, to: Position): boolean {
return board.canOccupy(this.color, to);
}
}
class Rook extends Piece {
public readonly name = "Rook";
public canMove(
board: Board,
from: Position,
to: Position,
): boolean {
if (samePosition(from, to) || !this.canLand(board, to)) {
return false;
}
const movesStraight =
from.row === to.row || from.col === to.col;
return movesStraight && board.isPathClear(from, to);
}
}
class Knight extends Piece {
public readonly name = "Knight";
public canMove(
board: Board,
from: Position,
to: Position,
): boolean {
if (!this.canLand(board, to)) return false;
const rowDistance = Math.abs(to.row - from.row);
const colDistance = Math.abs(to.col - from.col);
return (
(rowDistance === 2 && colDistance === 1) ||
(rowDistance === 1 && colDistance === 2)
);
}
}
class King extends Piece {
public readonly name = "King";
public canMove(
board: Board,
from: Position,
to: Position,
): boolean {
if (!this.canLand(board, to)) return false;
const rowDistance = Math.abs(to.row - from.row);
const colDistance = Math.abs(to.col - from.col);
return (
Math.max(rowDistance, colDistance) === 1
);
}
}
class Cell {
private currentPiece: Piece | null = null;
public constructor(public readonly position: Position) {}
public get piece(): Piece | null {
return this.currentPiece;
}
public replace(piece: Piece | null): void {
this.currentPiece = piece;
}
}
interface PositionedPiece {
readonly position: Position;
readonly piece: Piece;
}
class Board {
private readonly cells: readonly (readonly Cell[])[];
public constructor() {
this.cells = Array.from(
{ length: BOARD_SIZE },
(_, row) =>
Array.from(
{ length: BOARD_SIZE },
(_, col) => new Cell({ row, col }),
),
);
}
public isInside(position: Position): boolean {
return (
Number.isInteger(position.row) &&
Number.isInteger(position.col) &&
position.row >= 0 &&
position.row < BOARD_SIZE &&
position.col >= 0 &&
position.col < BOARD_SIZE
);
}
public pieceAt(position: Position): Piece | null {
return this.cellAt(position).piece;
}
public canOccupy(color: Color, position: Position): boolean {
if (!this.isInside(position)) return false;
const occupant = this.pieceAt(position);
return occupant === null || occupant.color !== color;
}
public isPathClear(from: Position, to: Position): boolean {
if (
!this.isInside(from) ||
!this.isInside(to) ||
samePosition(from, to)
) {
return false;
}
const rowDistance = Math.abs(to.row - from.row);
const colDistance = Math.abs(to.col - from.col);
const isLine =
rowDistance === 0 ||
colDistance === 0 ||
rowDistance === colDistance;
if (!isLine) return false;
const rowStep = Math.sign(to.row - from.row);
const colStep = Math.sign(to.col - from.col);
let row = from.row + rowStep;
let col = from.col + colStep;
while (row !== to.row || col !== to.col) {
if (this.pieceAt({ row, col }) !== null) {
return false;
}
row += rowStep;
col += colStep;
}
return true;
}
public place(position: Position, piece: Piece): void {
const cell = this.cellAt(position);
if (cell.piece !== null) {
throw new Error(
"square " + squareName(position) + " is occupied",
);
}
cell.replace(piece);
}
public apply(from: Position, to: Position): Piece | null {
const source = this.cellAt(from);
const destination = this.cellAt(to);
const moving = source.piece;
if (moving === null) {
throw new Error("source square is empty");
}
if (
destination.piece !== null &&
destination.piece.color === moving.color
) {
throw new Error("cannot capture a friendly piece");
}
const captured = destination.piece;
source.replace(null);
destination.replace(moving);
return captured;
}
public pieces(): readonly PositionedPiece[] {
const result: PositionedPiece[] = [];
for (const row of this.cells) {
for (const cell of row) {
if (cell.piece !== null) {
result.push({
position: { ...cell.position },
piece: cell.piece,
});
}
}
}
return result;
}
public clone(): Board {
const copy = new Board();
for (const entry of this.pieces()) {
// Pieces contain immutable type and color data in this core.
copy.place(entry.position, entry.piece);
}
return copy;
}
private cellAt(position: Position): Cell {
if (!this.isInside(position)) {
throw new Error(
"position " + squareName(position) + " is outside the board",
);
}
const row = this.cells[position.row];
const cell = row?.[position.col];
if (cell === undefined) {
throw new Error("board cell is missing");
}
return cell;
}
}
class Player {
public constructor(
public readonly id: string,
public readonly name: string,
public readonly color: Color,
) {
if (id.trim().length === 0 || name.trim().length === 0) {
throw new Error("player id and name are required");
}
}
}
class Move {
public readonly from: Readonly<Position>;
public readonly to: Readonly<Position>;
public constructor(
from: Position,
to: Position,
public readonly pieceName: string,
public readonly capturedName: string | null,
) {
this.from = Object.freeze({ ...from });
this.to = Object.freeze({ ...to });
Object.freeze(this);
}
}
interface MoveResult {
readonly move: Move;
readonly status: GameStatus;
readonly opponentInCheck: boolean;
readonly nextPlayerId: string | null;
}
class Game {
private currentColor = Color.White;
private currentStatus = GameStatus.Active;
private readonly history: Move[] = [];
public constructor(
private readonly board: Board,
private readonly players: readonly [Player, Player],
) {
if (players[0].id === players[1].id) {
throw new Error("players need distinct ids");
}
if (players[0].color === players[1].color) {
throw new Error("players need opposite colors");
}
for (const color of [Color.White, Color.Black]) {
const kings = board.pieces().filter(
(entry) =>
entry.piece.color === color &&
entry.piece instanceof King,
);
if (kings.length !== 1) {
throw new Error(
color + " must have exactly one king",
);
}
}
}
public get status(): GameStatus {
return this.currentStatus;
}
public moves(): readonly Move[] {
return [...this.history];
}
public move(
playerId: string,
from: Position,
to: Position,
): MoveResult {
if (this.isTerminal()) {
throw new Error("game is already over");
}
const player = this.playerFor(this.currentColor);
if (player.id !== playerId) {
throw new Error("it is " + player.name + "'s turn");
}
const piece = this.board.pieceAt(from);
if (piece === null) {
throw new Error(
"no piece at " + squareName(from),
);
}
if (piece.color !== this.currentColor) {
throw new Error("source piece belongs to the other player");
}
const target = this.board.pieceAt(to);
if (target instanceof King) {
throw new Error("a king is checked, not captured");
}
if (!piece.canMove(this.board, from, to)) {
throw new Error(
piece.name +
" cannot move from " +
squareName(from) +
" to " +
squareName(to),
);
}
const simulated = this.board.clone();
simulated.apply(from, to);
if (this.isInCheck(this.currentColor, simulated)) {
throw new Error("move would leave own king in check");
}
const captured = this.board.apply(from, to);
const move = new Move(
from,
to,
piece.name,
captured?.name ?? null,
);
this.history.push(move);
const opponent = opposite(this.currentColor);
const opponentInCheck = this.isInCheck(
opponent,
this.board,
);
const opponentHasMove = this.hasAnyLegalMove(opponent);
if (opponentInCheck && !opponentHasMove) {
this.currentStatus = GameStatus.Checkmate;
} else if (!opponentInCheck && !opponentHasMove) {
this.currentStatus = GameStatus.Stalemate;
} else if (opponentInCheck) {
this.currentStatus = GameStatus.Check;
} else {
this.currentStatus = GameStatus.Active;
}
this.currentColor = opponent;
return {
move,
status: this.currentStatus,
opponentInCheck,
nextPlayerId: this.isTerminal()
? null
: this.playerFor(opponent).id,
};
}
private isInCheck(color: Color, board: Board): boolean {
const king = board.pieces().find(
(entry) =>
entry.piece.color === color &&
entry.piece instanceof King,
);
if (king === undefined) {
throw new Error(color + " king is missing");
}
return board.pieces().some(
(entry) =>
entry.piece.color !== color &&
entry.piece.canMove(
board,
entry.position,
king.position,
),
);
}
private hasAnyLegalMove(color: Color): boolean {
const sources = this.board.pieces().filter(
(entry) => entry.piece.color === color,
);
for (const source of sources) {
for (let row = 0; row < BOARD_SIZE; row += 1) {
for (let col = 0; col < BOARD_SIZE; col += 1) {
const to = { row, col };
const target = this.board.pieceAt(to);
if (
target instanceof King ||
!source.piece.canMove(
this.board,
source.position,
to,
)
) {
continue;
}
const simulated = this.board.clone();
simulated.apply(source.position, to);
if (!this.isInCheck(color, simulated)) {
return true;
}
}
}
}
return false;
}
private playerFor(color: Color): Player {
const player = this.players.find(
(candidate) => candidate.color === color,
);
if (player === undefined) {
throw new Error("player for " + color + " is missing");
}
return player;
}
private isTerminal(): boolean {
return (
this.currentStatus === GameStatus.Checkmate ||
this.currentStatus === GameStatus.Stalemate
);
}
}
function printMove(
player: Player,
result: MoveResult,
): void {
console.log(
player.name +
": " +
result.move.pieceName +
" " +
squareName(result.move.from) +
"→" +
squareName(result.move.to) +
" (" +
result.status +
")",
);
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
const board = new Board();
board.place(parseSquare("a1"), new Rook(Color.White));
board.place(parseSquare("b1"), new Knight(Color.White));
board.place(parseSquare("e1"), new King(Color.White));
board.place(parseSquare("e8"), new King(Color.Black));
const ada = new Player("white-1", "Ada", Color.White);
const turing = new Player("black-1", "Turing", Color.Black);
const game = new Game(board, [ada, turing]);
printMove(
ada,
game.move(ada.id, parseSquare("a1"), parseSquare("a3")),
);
printMove(
turing,
game.move(turing.id, parseSquare("e8"), parseSquare("e7")),
);
printMove(
ada,
game.move(ada.id, parseSquare("b1"), parseSquare("c3")),
);
try {
game.move(
turing.id,
parseSquare("e7"),
parseSquare("e5"),
);
} catch (error) {
console.log("Rejected: " + errorMessage(error));
}Ada: Rook a1→a3 (active) Turing: King e8→e7 (active) Ada: Knight b1→c3 (active) Rejected: King cannot move from e7 to e5
The rook's first move proves a clear two-square file; an occupied a2 would make isPathClear() reject it. The knight then jumps to c3. The black king's two-square attempt fails inside its override, and the turn remains Black because nothing was applied or recorded.
Game also performs real simulate-and-test king safety and scans legal replies for the three implemented piece types. Adding Queen, Bishop, and Pawn movement extends those scans through polymorphism. Castling, en passant, promotion, and the additional draw rules still need the extended rule layer described above; the skeleton does not pretend otherwise.
Design-review checklist
This is where an interviewer turns three representative pieces into the full rule system.
- Where check detection lives:
Gameor an extracted rules engine owns check because it needs the board, king location, and opponent attack map. A single piece does not know the position-wide answer. - Pseudo-legal versus legal moves: A piece checks geometry, destination, and path. The rules pipeline adds turn, lifecycle, and own-king safety. Naming both levels prevents
canMove()from claiming too much. - Why polymorphic pieces beat a giant switch: Each subtype keeps one movement rule beside its identity, while
Gamecalls one contract. A six-case branch in every validator, hint generator, and AI would scatter the same type knowledge. - Limits of per-piece ownership: En passant depends on the previous move, castling spans king and rook, and check examines every opponent piece. Polymorphism is not permission to force every chess rule into an override.
- Path-clear ownership:
Boardanswers intervening occupancy. Rook, Bishop, and Queen choose when that query applies; Knight does not pay for or duplicate it. - Self-check validation: Clone or reversibly apply the candidate, test the moving side's king, then discard the simulation. Only a safe candidate reaches the authoritative board.
- Checkmate and stalemate: Both require zero legal replies. The difference is whether the side to move is currently checked. Checking only the king's attack status cannot distinguish them.
- Attack maps versus legal moves: King safety needs squares attacked by opponent pseudo-legal movement. Recursively validating the attacker's own king for every attack can produce cycles and incorrect pawn or king behavior.
- Captures:
Board.apply()returns the destination occupant while changing source and destination together. Kings are attacked and mated, never captured. - Modeling castling: Track king and rook move history, verify empty and unattacked transit squares, then commit one compound move. Do not scatter a follow-up rook mutation across controllers.
- Modeling en passant: Keep the previous immutable
Move; a rule recognizes the immediate opportunity and produces a capture square separate from the pawn's destination. - Modeling promotion: Carry the selected promotion type in the command and create the replacement piece during the same atomic move. Reject an absent or invalid choice before changing the board.
- Immutable Move history: A
Moverecord supports replay, audit, and notation. Undo can treat it as a Command carrying inverse data or pair snapshots with the Memento pattern from command and memento. - Performance of legal-move generation: A board scan is clear and adequate for machine coding. An AI search may use piece indexes, cached attack maps, move ordering, or a compact bit-based board, but every cache must invalidate on moves and undo.
- Game status: Check is non-terminal; checkmate and stalemate are terminal. An illegal command preserves the previous status and turn, while an accepted move derives status from the resulting board.
- Concurrency: Serialize commands per game or update a persisted game only when its version matches. A process-local turn check cannot stop two servers from accepting White's move together.
- Testing each piece: Cover every movement direction, same-square moves, friendly destinations, captures, edges, and out-of-board coordinates. Pawns need color, first-step, diagonal capture, and promotion cases.
- Testing blocked paths: Place blockers immediately beside and several squares away from rook, bishop, and queen. Knights must remain able to jump while line pieces reject the route.
- Testing king safety: Include pinned pieces, discovered checks, a king moving into attack, blocking a check, capturing the attacker, and a candidate that appears legal until simulation.
- Testing terminal positions: Use known check, checkmate, and stalemate boards. Assert that check allows a reply, terminal positions reject later moves, and kings remain on the board.
- Testing castling legality: Cover moved king, moved rook, occupied path, king currently checked, attacked transit square, attacked destination, and both legal sides.
- With more time: Add all piece subclasses, a dedicated rules engine, special-move objects, repetition and fifty-move state, notation adapters, repositories, optimistic locking, and property tests that preserve one king per color.
Checkpoint
Answer all three to mark this lesson complete
Chess closes the case studies with a useful lesson: the prompt can be huge while the core ownership map stays small. The pattern and principle vocabulary from the whole course is what turns that size into explicit pieces, boundaries, and trade-offs instead of one impossible GameManager.