LLD-006 · DESIGN PRACTICE
Chess Engine
Separate piece geometry from king safety, and make special-move state explicit.
Local Java 21 reference verified: 67 checks. Learner drafts are not executed. Author self-review; independent review pending.
Existing introductory lesson · Version 1
Understand the problem
Design an immutable chess position engine that enumerates legal moves and returns a new position after an accepted move. Include ordinary movement, check, checkmate, stalemate, castling, en passant and promotion. Explain why piece classes alone cannot establish legality and why position state includes more than the visible board.
Before you start
- Immutable state and value objects
- Matrix/ray traversal
- Rules versus representation
- State transition validation and simulation
Scope and explicit decisions
- Implemented: standard 8×8 piece movement, side-to-move, king safety, legal move enumeration, check/mate/stalemate, castling rights, en passant and four promotion choices. This is not a complete tournament adjudicator.
- Excluded: repetition draws, fifty/seventy-five-move adjudication, insufficient-material/dead-position adjudication, agreement/resignation, chess clocks, touch-move, notation annotations, Chess960, AI/search and network play. Full draw-rule completion remains tracked work.
- The core Position is immutable. play accepts a Move only if present in legalMoves, then returns a new Position. Rejection never mutates the original. Shared multiplayer command/version ownership belongs to a separate Game service extension.
- Coordinates use algebraic squares converted to a 0..63 board index. UCI-like input has four coordinate characters plus optional lowercase q/r/b/n promotion. Missing promotion on a last-rank pawn move is rejected.
- The fixture parser accepts exactly four FEN fields: placement, side, castling flags and en-passant target. It validates representation and one king per color, not historical reachability of arbitrary positions. It does not parse halfmove/fullmove counters.
- Legal candidate generation never captures a king. King safety is checked after applying the entire move, including en-passant removal and rook relocation during castling.
- Castling requires the declared right, correct king/rook starting squares, empty path and safe king start/transit/destination. Rights are removed when a king moves or a relevant rook corner is vacated/captured.
- En-passant target lasts one reply only and is reset on other moves. A capture is generated only when the adjacent enemy pawn and empty target exist. Promotion creates Q/R/B/N of the mover’s color, including underpromotion.
Worked examples
Input: Initial position: f2f3, e7e5, g2g4, d8h4.
Outcome: White is in CHECKMATE: attacked king and zero legal replies.
Mate is a property of full legal transitions, not simply a queen attacking a king.
Input: White king e1, white pawn e5, black pawn d5, black rook e8; en-passant target d6.
Outcome: e5d6 is pseudo-legal but excluded because removing the pawn opens e-file check.
Special captures must be simulated completely before checking king safety.
Your design attempt
Each language has a separate draft across all three levels. Saving is device-only and does not sync to your account. Anyone using this browser profile can load it. Loading is always explicit.
Java 21 reference code is provided for local study. Use Run program in the editor to execute your code and test driver. Program runs show output; they do not grade your design.
Run program · Java 21
No unsaved changes · 208/20000
Self-review
Checks apply to this visit. Self-review is not a passed submission or verified mastery.
Try before revealing
Hint 1
What does piece movement fail to tell you about its own king?
Hint 2
Which position facts cannot be reconstructed from the visible board alone?
Hint 3
Apply the entire special move to a copy before checking attacks.
Requirements and invariants
R1 — Accepted moves obey piece geometry and leave the moving side king safe. legalMoves protects I1.
R2 — Special-move rights and en-passant state change with the entire move. applyUnchecked protects I2.
R3 — Playing or simulating a move cannot mutate its source position. play protects I3.
R4 — Checkmate and stalemate are distinguished by king attack status. status protects I1.
I1: Every returned legal move leaves the moving king unattacked in the resulting board; no move captures a king.
I2: Castling rights and en-passant opportunity are part of position state and update together with board and side-to-move.
I3: All positions own copied arrays; derivations and public play leave prior positions unchanged.
Derive the design
Geometric movement first, then legal transitions
Piece-specific movement rules can generate pseudo-legal candidates: a rook follows clear orthogonal rays; a knight jumps; a pawn has distinct advance and capture behavior. That is a useful baseline, but it does not know whether the move exposes its own king. The refined engine simulates each full candidate and tests the resulting king square against enemy attacks. A hierarchy of Piece subclasses would still need this game-level filter.
Alternative: Mutable apply/undo can reduce allocation in deep search, but the undo delta must restore captured pieces, en-passant capture, rook relocation, rights and turn exactly. This chapter chooses immutable copies for correctness clarity; optimize only with replay/restoration and move-count regression evidence.
Start from operation contracts, not piece inheritance
Ask which rules are included, how positions enter the system and whether the engine is mutable. Different representations can implement the same legal-move contract; inheritance alone is not correctness.
Use a separate attack evaluator
Pawn attack squares differ from forward moves, kings attack adjacent squares, and sliders stop at occupancy. This evaluator must not call legalMoves recursively.
Apply special moves as one transition
Board, turn, rights and en-passant target change together. Test castling and en passant as complete transitions before expecting the king-safety filter to be meaningful.
Distinguish rule coverage from full adjudication
The implemented legal-move engine handles special moves and mate/stalemate, but excludes several draw and tournament rules. Make that limitation visible in the reader and progress tracker, rather than labelling it a complete FIDE game service.
Responsibilities and ownership
Position
Owns: Board, side-to-move, castling mask and en-passant square
Methods: legalMoves, play, status, fen
Immutable state; no public board setter.
Move
Owns: From/to squares and explicit promotion choice
Methods: Immutable record equality
Coordinates are candidate intent, not proof of legal movement.
pseudo / attacked
Owns: Piece geometry / geometric enemy attack detection
Methods: Candidate rays, knight jumps, pawn and king attacks
Attack detection does not recursively call legal move generation.
applyUnchecked
Owns: Complete candidate simulation
Methods: Board copy, captures, special pieces, rights, side switch
Private; public play validates against legalMoves first.
Attack detection is independent of legal-move recursion.
- Piece geometry candidates
↓ Respect occupancy, promotion and special candidates
Apply complete move to copied Position↓ Include rook relocation and en-passant removal
Test moving king against attacks↓ Reject exposed king
Return immutable legal move list
Other draw/adjudication rules are excluded. With moves available, status is CHECK or PLAYING.
Attacked king
Unattacked king
Data model, schema and relationships
Chess needs position history and rule context, not merely a set of Piece subclasses. Legal movement, king safety, turn and special-move rights belong to one position transaction.
Implementation boundary: The reference is a scoped chess model; persisted position/move/rule metadata below illustrates extension boundaries. Do not infer unimplemented tournament rules from a table.
These are proposed logical schemas with field contracts, not executable database migrations. PK identifies a row, FK references an existing parent, UK is a uniqueness rule. A composite PK identifies the combination. Nullable references are marked with ?. A foreign key protects identity; cross-row limits, timing and state transitions also require the transaction rules below.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Connectors name the relationship; multiplicities state parents per child and children per parent. 0..1 means optional, 1 required, 0..* any number. Self-links show two roles of the same entity.
Entity definitions and field contracts
chess_rules
Immutable supported rule set.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | text · PK | Rules version |
scope | text · VALUE | Declared supported moves/rules |
Code mapping: Rule contract.
- Pinned semantics must identify omitted rules.
chess_game
One turn/state owner.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Game identity |
rules_id | text · FK | Pinned rules · chess_rules.id |
version | bigint · VALUE | Move version |
turn | enum · VALUE | White or black |
Code mapping: Game.
- Only legal non-stale moves advance version.
position
Versioned board snapshot.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Snapshot identity |
game_id | uuid · FK | Game owner · chess_game.id |
ply | integer · VALUE | Move count |
board | text · VALUE | Encoded pieces |
rights | text · VALUE | Supported special-rule context |
Code mapping: Board/position projection.
- Unique(game_id,ply); encoding needs a version and all supported legality context.
chess_move
Accepted move command.
| Field | Type / key | Meaning and reference |
|---|---|---|
id | uuid · PK | Move identity |
game_id | uuid · FK | Game owner · chess_game.id |
ply | integer · VALUE | Accepted order |
from_square | text · VALUE | Origin |
to_square | text · VALUE | Destination |
Code mapping: Move and legality validation.
- Unique(game_id,ply); include promotion choice when supported.
Relationship cardinalities and their reasons
| Parent → child | Multiplicity | FK / why |
|---|---|---|
| chess_rules → chess_game governs | 1 parent per child; 0..* children per parent | chess_game.rules_id → chess_rules.idGames retain their supported rule semantics. |
| chess_game → position snapshots | 1 parent per child; 0..* children per parent | position.game_id → chess_game.idSnapshots accelerate observation but must match accepted history. |
| chess_game → chess_move records moves | 1 parent per child; 0..* children per parent | chess_move.game_id → chess_game.idHistorical move list and current position commit together. |
Transaction boundaries
- Validate turn, movement and resulting king safety on a candidate board before committing history/version.
- Persist accepted move and matching position update in one transaction; never mutate board during a rejected candidate.
Indexes and uniqueness
- Unique game/ply for history and snapshots supports deterministic replay.
- Index active game by participant outside this scoped core; do not store all positions as independently writable truth.
Schema traps and failure cases
- A piece-level move method cannot alone prove king safety.
- Special-move rights and repetition rules require more context than piece coordinates; extend scope explicitly.
Trace the records through one operation
- Load pinned rules and position at ply 12.
- Evaluate a candidate on a temporary board; reject if it exposes the king.
- A legal command commits ply 13 and the derived position together.
Method contracts
Position.initial(); Position.fromFen(fourFieldFixture)
Create immutable positions. The fixture parser validates syntax and king count, not full legal-history reachability; it is not a production game importer.
legalMoves() → immutable Move list
Generate geometric candidates, apply each to a copy and retain only those preserving moving king safety.
play(Move) → new Position
Reject a move not present in legalMoves. Original remains unchanged; there is no partial capture or promotion mutation on error.
status() → PLAYING / CHECK / CHECKMATE / STALEMATE
Combine current-side attack status and presence of legal moves. Other draw rules are deliberately not inferred.
fen(); perft(depth)
Serialize the implemented four-field state. perft counts legal move-tree leaves for bounded depth 0..4; it is a test aid, not a chess AI or exhaustive correctness proof.
What, when, why and how
Each concept is attached to the requirement it protects, its implementation, an alternative and a test.
Pseudo-legal versus legal moves
- what
- Separate movement geometry from the whole-position king-safety rule.
- when
- A piece is pinned or a king might move into attack.
- why
- A geometrically valid move can expose the moving king, making it illegal.
- how
- pseudo enumerates candidates. legalMoves applies each through a private complete transition, finds the moving side king and rejects the candidate if attacked.
Code: pseudo, legalMoves, king · Invariants: I1
Alternatives, cost and failure example
Alternative: Dedicated pin/check-evasion generation can be faster but introduces more coupled rules and needs extensive equivalence tests.
Cost: Each candidate copies a 64-cell board and performs attack detection; fixed board size does not mean negligible search cost.
Counterexample: Pinned white rook e2 moves sideways while black rook e8 attacks king e1 along the opened file.
Test: Reject e2f2 in the pin fixture; accept capturing the attacking rook when otherwise legal.
Attack map is not legal-move recursion
- what
- Detect geometric attacks independently of whether the attacking piece can legally move.
- when
- Checking king movement or castling transit squares.
- why
- Computing attacks by asking for opponent legalMoves can recurse and mis-handle pinned-piece attack semantics.
- how
- attacked checks pawn diagonals, knight jumps, adjacent king and blocked sliding rays directly. It never calls legalMoves.
Code: attacked · Invariants: I1
Alternatives, cost and failure example
Alternative: A cached attack map is possible but must be invalidated for every occupancy-changing move, including special moves.
Cost: Rays stop at the first occupied square. The fixed board bounds each scan, but candidate branching multiplies work.
Counterexample: A king is allowed onto a square because the code omitted pawn attack direction or ignored a pinned enemy attack.
Test: Check pawn/king safety through legal-move fixtures, and reject castling through an attacked transit square.
Special-move state and complete simulation
- what
- Treat rights and transient opportunities as part of the position, not merely board appearance.
- when
- Castling or en passant may be legal only because of history.
- why
- Identical piece placement can have different legal moves depending on rights and the previous pawn advance.
- how
- Position stores rights and ep. applyUnchecked moves the castling rook, removes an en-passant pawn, updates corner rights, applies promotion and flips side together on a copied board.
Code: castles, applyUnchecked · Invariants: I1, I2
Alternatives, cost and failure example
Alternative: A validated command/delta object could encode the transition, but omitting one auxiliary field still corrupts subsequent legality.
Cost: More state makes serialization and replay compatibility important; four-field FEN here intentionally omits draw counters.
Counterexample: En passant removes a pawn from a different square and opens an attack line that a normal-capture simulation would miss.
Test: Reject en-passant capture exposing e-file check; accept normal case; verify both castling relocation and rights changes.
Immutable positions and differential evidence
- what
- Make every candidate derivation independent and compare behavior with varied rule fixtures.
- when
- Move generation branches and failed moves must leave source state intact.
- why
- Leaked simulation mutation can corrupt later candidates, producing intermittent legal-move errors.
- how
- Position constructor copies the array; applyUnchecked modifies a separate clone. Tests preserve initial FEN across perft/play branches, check round trips and verify known shallow move-tree counts.
Code: Position, play, perft, fen · Invariants: I3
Alternatives, cost and failure example
Alternative: Apply/undo saves allocations for search engines but requires an exact reversible state delta and restoration assertions.
Cost: Copy-per-candidate is appropriate for a teaching engine, not a claim of competitive engine performance.
Counterexample: A simulated en-passant candidate removes a pawn from the shared board even when rejected.
Test: Initial perft leaf counts 20/400/8902 at depths1/2/3 plus special-move, pin, mate and stalemate fixtures.
Read and run the Java reference
Reveal complete Solution.java
Java 21. Save as Solution.java; compile with javac --release 21. The repository also contains Test.java and the lld:java verification command. This reference implements the local contract above.
pseudo handles geometry without capturing kings
All generated captures exclude the enemy king; legal games terminate by mate. Pawn advance/capture/promotion rules are distinct from the common slider/knight/king loops.
castles validates start, path and transit attack
Rights alone are insufficient. The starting king and rook must actually be present, path clear and king squares safe. An attacked rook square by itself is not the king-path condition.
applyUnchecked owns every auxiliary state update
The candidate removes en-passant captures from the adjacent rank, moves the rook on castling, sets promotion color, removes rights and clears or sets the next ep target.
legalMoves filters after complete simulation
After creating a candidate position with side switched, it explicitly finds the prior moving side king and tests attacks by the opponent. It must not accidentally check only the new side’s king.
Complexity and limits
- Let M be pseudo-legal candidates, B=64 cells and A be bounded attack-scan work. legalMoves costs O(M×(B+A)); the fixed standard board bounds B/A but tree search still grows exponentially with depth.
- play currently regenerates legalMoves for validation and then applies the selected candidate. status also generates legal moves; avoid claiming these are a single constant-time check.
- Immutable positions store O(B) state. legalMoves retains O(M) moves plus temporary candidate copies. perft recursively explores branching factor b to depth d, approximately O(b^d) nodes with depth-proportional retained traversal state.
- No transposition table, move ordering, evaluation or AI search is implemented. perft depth is bounded to four by the public helper; tests use shallow known fixtures.
Walk through the actual reference
These walkthrough fixtures are emitted only after the Java harness verifies their operations and expected state. Concurrency tests additionally exercise competing callers.
Fool's mate
Step 1 of 2: f2f3, e7e5, g2g4
White king exposed on diagonal
Invariant: Each move legal in its own immutable position
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| f2f3, e7e5, g2g4 | White king exposed on diagonal | Each move legal in its own immutable position |
| d8h4 | White to move in check with no legal response | CHECKMATE requires check plus zero legal moves |
En passant exposing a king
Step 1 of 2: Consider e5d6 with rook on e8 and king on e1
Pawn geometry permits the capture
Invariant: Pseudo-legality is only the first stage
Read the complete trace table
| Action | State after action | Invariant |
|---|---|---|
| Consider e5d6 with rook on e8 and king on e1 | Pawn geometry permits the capture | Pseudo-legality is only the first stage |
| Simulate capture and remove d5 pawn | e-file opens; king attacked; move excluded | A legal move cannot expose its own king |
Mistakes and counterexamples
Use piece geometry as the final legal result
Failing case: A pinned rook moves away from its own king.
Repair: Simulate full move and reject king exposure.
Compute attacks through legalMoves
Failing case: King validation calls opponent legal generation recursively.
Repair: Use a separate geometric attack evaluator.
Handle en passant as an ordinary destination capture
Failing case: The captured pawn remains on d5 while the mover lands on d6.
Repair: Remove the adjacent captured pawn before king-safety evaluation.
Castle based only on empty squares
Failing case: The king crosses an attacked f1 square.
Repair: Check rights, actual king/rook, path and all king transit conditions.
Call this a complete tournament engine
Failing case: A repeated or dead position remains PLAYING.
Repair: State excluded draw/adjudication rules and implement them as explicit future work.
Engineer → Senior → Staff
Model movement and immutable board ownership.
Executed: piece movement, turn switching, captures, explicit promotion and immutable play.
Change exercise
Compare an ordinary rook capture with en passant.
Worked answer guidance
A rook captures the destination piece. En passant removes an adjacent pawn on a different square; the transition representation must account for that before evaluating legality.
Interview follow-ups
Why is Move not itself legal?
It describes candidate intent. Legality depends on the full Position: side, occupancy, attack state and historical rights.
Why allow underpromotion?
Promotion choice is part of the move contract. Assuming queen only removes legal moves and can change outcomes.
Provenance and scope of evidence
Original design/code explanation; implemented rule scope and limitations are declared above.
Primary rule reference checked: FIDE Laws of Chess, Articles 3 and 5, https://handbook.fide.com/chapter/E012023 . Special-move and king-safety rules inform fixtures; this limited engine is not claimed to implement all FIDE adjudication or competition procedures.
Reference verification is not independent editorial review, production deployment verification or a claim that distributed adapters are implemented.