Learning pathsPricingChecking sign-in…
← LLD problem library

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.

Solution.java
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.

Legal move pipeline

Attack detection is independent of legal-move recursion.

  1. Piece geometry candidates
  2. ↓ Respect occupancy, promotion and special candidates

    Apply complete move to copied Position
  3. ↓ Include rook relocation and en-passant removal

    Test moving king against attacks
  4. ↓ Reject exposed king

    Return immutable legal move list
Status decision

Other draw/adjudication rules are excluded. With moves available, status is CHECK or PLAYING.

Current side: attacked? and any legal moves?

Attacked king

No moves + check: CHECKMATE

Unattacked king

No moves + no check: STALEMATE

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.

chess_rules: relationship map

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.

chess_rules relationshipschess_rules 1 to chess_game 0..*: governs. chess_game.rules_id → chess_rules.id.governs1 parent · 0..* childrenchess_gameid [PK]rules_id [FK]versionchess_rulesid [PK]scope
chess_game: relationship map

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.

chess_game relationshipschess_game 1 to position 0..*: snapshots. position.game_id → chess_game.id. chess_game 1 to chess_move 0..*: records moves. chess_move.game_id → chess_game.id.snapshots1 parent · 0..* childrenpositionid [PK]game_id [FK]plyrecords moves1 parent · 0..* childrenchess_moveid [PK]game_id [FK]plychess_gameid [PK]rules_id [FK]version

Entity definitions and field contracts

chess_rules

Immutable supported rule set.

FieldType / keyMeaning and reference
idtext · PKRules version
scopetext · VALUEDeclared supported moves/rules

Code mapping: Rule contract.

  • Pinned semantics must identify omitted rules.

chess_game

One turn/state owner.

FieldType / keyMeaning and reference
iduuid · PKGame identity
rules_idtext · FKPinned rules · chess_rules.id
versionbigint · VALUEMove version
turnenum · VALUEWhite or black

Code mapping: Game.

  • Only legal non-stale moves advance version.

position

Versioned board snapshot.

FieldType / keyMeaning and reference
iduuid · PKSnapshot identity
game_iduuid · FKGame owner · chess_game.id
plyinteger · VALUEMove count
boardtext · VALUEEncoded pieces
rightstext · VALUESupported 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.

FieldType / keyMeaning and reference
iduuid · PKMove identity
game_iduuid · FKGame owner · chess_game.id
plyinteger · VALUEAccepted order
from_squaretext · VALUEOrigin
to_squaretext · VALUEDestination

Code mapping: Move and legality validation.

  • Unique(game_id,ply); include promotion choice when supported.

Relationship cardinalities and their reasons

Parent → childMultiplicityFK / why
chess_rules → chess_game

governs

1 parent per child; 0..* children per parentchess_game.rules_id → chess_rules.id

Games retain their supported rule semantics.

chess_game → position

snapshots

1 parent per child; 0..* children per parentposition.game_id → chess_game.id

Snapshots accelerate observation but must match accepted history.

chess_game → chess_move

records moves

1 parent per child; 0..* children per parentchess_move.game_id → chess_game.id

Historical move list and current position commit together.

Transaction boundaries

  1. Validate turn, movement and resulting king safety on a candidate board before committing history/version.
  2. Persist accepted move and matching position update in one transaction; never mutate board during a rejected candidate.

Indexes and uniqueness

  1. Unique game/ply for history and snapshots supports deterministic replay.
  2. Index active game by participant outside this scoped core; do not store all positions as independently writable truth.

Schema traps and failure cases

  1. A piece-level move method cannot alone prove king safety.
  2. Special-move rights and repetition rules require more context than piece coordinates; extend scope explicitly.

Trace the records through one operation

  1. Load pinned rules and position at ply 12.
  2. Evaluate a candidate on a temporary board; reject if it exposes the king.
  3. 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.

Solution.java Java 21
import java.util.*;
/** Immutable legal-move engine. Includes castling, en passant, promotion, mate and stalemate.
 * Excludes repetition, fifty/seventy-five-move and insufficient-material draw adjudication. */
public final class Solution {
    private Solution() {}
    public enum Status { PLAYING, CHECK, CHECKMATE, STALEMATE }
    public record Move(int from,int to,char promotion){}
    private static boolean white(char p){return Character.isUpperCase(p);}
    private static boolean own(char p,boolean side){return p!='.'&&white(p)==side;}
    private static boolean inside(int r,int c){return r>=0&&r<8&&c>=0&&c<8;}
    public static int square(String algebraic){if(algebraic==null||!algebraic.matches("[a-h][1-8]"))throw new IllegalArgumentException("square");return (8-(algebraic.charAt(1)-'0'))*8+algebraic.charAt(0)-'a';}
    public static String algebraic(int square){if(square<0||square>=64)throw new IllegalArgumentException("square");return ""+(char)('a'+square%8)+(8-square/8);}
    public static Move move(String uci){if(uci==null||!uci.matches("[a-h][1-8][a-h][1-8][qrbn]?"))throw new IllegalArgumentException("move");return new Move(square(uci.substring(0,2)),square(uci.substring(2,4)),uci.length()==5?uci.charAt(4):'\0');}
    public static final class Position {
        private final char[] board;
        private final boolean whiteTurn;
        private final int rights,ep;
        private Position(char[] board,boolean side,int rights,int ep){this.board=board.clone();whiteTurn=side;this.rights=rights;this.ep=ep;}
        public static Position initial(){return fromFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -");}
        /** Four-field fixture FEN. Checks representation, not historical reachability. */
        public static Position fromFen(String fen){
            String[] fields=fen.split(" ");if(fields.length!=4)throw new IllegalArgumentException("four FEN fields required");String[] ranks=fields[0].split("/");if(ranks.length!=8)throw new IllegalArgumentException("ranks");
            char[] board=new char[64];Arrays.fill(board,'.');int wk=0,bk=0;
            for(int r=0;r<8;r++){int c=0;for(char p:ranks[r].toCharArray()){if(p>='1'&&p<='8')c+=p-'0';else{if("prnbqkPRNBQK".indexOf(p)<0||c>=8)throw new IllegalArgumentException("piece");board[r*8+c++]=p;if(p=='K')wk++;if(p=='k')bk++;}}if(c!=8)throw new IllegalArgumentException("rank width");}
            if(wk!=1||bk!=1||!Set.of("w","b").contains(fields[1])||!fields[2].matches("-|K?Q?k?q?"))throw new IllegalArgumentException("position");
            int rights=0;for(char p:fields[2].toCharArray())rights|=switch(p){case 'K'->1;case 'Q'->2;case 'k'->4;case 'q'->8;default->0;};
            int ep=fields[3].equals("-")?-1:square(fields[3]);if(ep>=0&&ep/8!=(fields[1].equals("w")?2:5))throw new IllegalArgumentException("en passant rank");
            return new Position(board,fields[1].equals("w"),rights,ep);
        }
        public String fen(){
            var s=new StringBuilder();for(int r=0;r<8;r++){int empty=0;for(int c=0;c<8;c++){char p=board[r*8+c];if(p=='.')empty++;else{if(empty>0){s.append(empty);empty=0;}s.append(p);}}if(empty>0)s.append(empty);if(r<7)s.append('/');}
            s.append(whiteTurn?" w ":" b ");String flags="";if((rights&1)!=0)flags+="K";if((rights&2)!=0)flags+="Q";if((rights&4)!=0)flags+="k";if((rights&8)!=0)flags+="q";return s+(flags.isEmpty()?"-":flags)+" "+(ep<0?"-":algebraic(ep));
        }
        private int king(boolean side){char k=side?'K':'k';for(int i=0;i<64;i++)if(board[i]==k)return i;throw new IllegalStateException("king missing");}
        public boolean inCheck(){return attacked(king(whiteTurn),!whiteTurn);}
        private boolean attacked(int target,boolean byWhite){
            int r=target/8,c=target%8,pawnRow=r+(byWhite?1:-1);
            for(int dc:new int[]{-1,1})if(inside(pawnRow,c+dc)&&board[pawnRow*8+c+dc]==(byWhite?'P':'p'))return true;
            int[][] jumps={{1,2},{2,1},{-1,2},{-2,1},{1,-2},{2,-1},{-1,-2},{-2,-1}};
            for(var d:jumps)if(inside(r+d[0],c+d[1])&&board[(r+d[0])*8+c+d[1]]==(byWhite?'N':'n'))return true;
            for(int dr=-1;dr<=1;dr++)for(int dc=-1;dc<=1;dc++){
                if(dr==0&&dc==0)continue;
                for(int step=1;inside(r+dr*step,c+dc*step);step++){
                    char p=board[(r+dr*step)*8+c+dc*step];if(p=='.')continue;
                    if(own(p,byWhite)){
                        char kind=Character.toLowerCase(p);
                        if(step==1&&kind=='k')return true;
                        if(kind=='q'||(kind=='b'&&dr!=0&&dc!=0)||(kind=='r'&&(dr==0||dc==0)))return true;
                    }break;
                }
            }return false;
        }
        private void add(List<Move> moves,int from,int to){char p=board[from];if(Character.toLowerCase(p)=='p'&&(to/8==0||to/8==7)){for(char promotion:new char[]{'q','r','b','n'})moves.add(new Move(from,to,promotion));}else moves.add(new Move(from,to,'\0'));}
        private List<Move> pseudo(){
            var moves=new ArrayList<Move>();
            for(int from=0;from<64;from++){
                char piece=board[from];if(!own(piece,whiteTurn))continue;int r=from/8,c=from%8;char kind=Character.toLowerCase(piece);
                if(kind=='p'){
                    int dr=whiteTurn?-1:1,nr=r+dr;
                    if(inside(nr,c)&&board[nr*8+c]=='.'){add(moves,from,nr*8+c);int start=whiteTurn?6:1;if(r==start&&board[(r+2*dr)*8+c]=='.')add(moves,from,(r+2*dr)*8+c);}
                    for(int dc:new int[]{-1,1})if(inside(nr,c+dc)){
                        int to=nr*8+c+dc;char target=board[to];
                        boolean capture=target!='.'&&!own(target,whiteTurn)&&Character.toLowerCase(target)!='k';
                        boolean passing=to==ep&&target=='.'&&board[r*8+c+dc]==(whiteTurn?'p':'P');
                        if(capture||passing)add(moves,from,to);
                    }
                }else{
                    int[][] dirs=kind=='n'?new int[][]{{1,2},{2,1},{-1,2},{-2,1},{1,-2},{2,-1},{-1,-2},{-2,-1}}:new int[][]{{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}};
                    for(var d:dirs){if(kind=='b'&&(d[0]==0||d[1]==0)||kind=='r'&&d[0]!=0&&d[1]!=0)continue;
                        for(int step=1;inside(r+d[0]*step,c+d[1]*step);step++){
                            int to=(r+d[0]*step)*8+c+d[1]*step;char target=board[to];if(own(target,whiteTurn)||Character.toLowerCase(target)=='k')break;
                            add(moves,from,to);if(target!='.'||kind=='k'||kind=='n')break;
                        }
                    }
                    if(kind=='k')castles(moves,from);
                }
            }return moves;
        }
        private void castles(List<Move> moves,int from){
            int base=whiteTurn?56:0,k=base+4;char rook=whiteTurn?'R':'r';if(from!=k||attacked(k,!whiteTurn))return;
            int shortFlag=whiteTurn?1:4,longFlag=whiteTurn?2:8;
            if((rights&shortFlag)!=0&&board[base+7]==rook&&board[base+5]=='.'&&board[base+6]=='.'&&!attacked(base+5,!whiteTurn)&&!attacked(base+6,!whiteTurn))add(moves,from,base+6);
            if((rights&longFlag)!=0&&board[base]==rook&&board[base+1]=='.'&&board[base+2]=='.'&&board[base+3]=='.'&&!attacked(base+3,!whiteTurn)&&!attacked(base+2,!whiteTurn))add(moves,from,base+2);
        }
        private Position applyUnchecked(Move m){
            char[] next=board.clone();char piece=next[m.from()],target=next[m.to()];next[m.from()]='.';next[m.to()]=m.promotion()=='\0'?piece:(whiteTurn?Character.toUpperCase(m.promotion()):m.promotion());
            if(Character.toLowerCase(piece)=='p'&&m.to()==ep&&target=='.'&&m.from()%8!=m.to()%8)next[m.to()+(whiteTurn?8:-8)]='.';
            if(Character.toLowerCase(piece)=='k'&&Math.abs(m.to()-m.from())==2){int rookFrom=m.to()>m.from()?m.from()+3:m.from()-4,rookTo=m.to()>m.from()?m.from()+1:m.from()-1;next[rookTo]=next[rookFrom];next[rookFrom]='.';}
            int flags=rights;if(piece=='K')flags&=~3;if(piece=='k')flags&=~12;
            if(m.from()==63||m.to()==63)flags&=~1;if(m.from()==56||m.to()==56)flags&=~2;if(m.from()==7||m.to()==7)flags&=~4;if(m.from()==0||m.to()==0)flags&=~8;
            int enPassant=Character.toLowerCase(piece)=='p'&&Math.abs(m.to()-m.from())==16?(m.from()+m.to())/2:-1;
            return new Position(next,!whiteTurn,flags,enPassant);
        }
        public List<Move> legalMoves(){var result=new ArrayList<Move>();for(var m:pseudo()){var next=applyUnchecked(m);if(!next.attacked(next.king(whiteTurn),!whiteTurn))result.add(m);}return List.copyOf(result);}
        public Position play(Move move){if(!legalMoves().contains(move))throw new IllegalArgumentException("illegal move");return applyUnchecked(move);}
        public Status status(){boolean check=inCheck();if(legalMoves().isEmpty())return check?Status.CHECKMATE:Status.STALEMATE;return check?Status.CHECK:Status.PLAYING;}
        public long perft(int depth){if(depth<0||depth>4)throw new IllegalArgumentException("depth");if(depth==0)return 1;long result=0;for(var m:legalMoves())result=Math.addExact(result,applyUnchecked(m).perft(depth-1));return result;}
    }
}

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
ActionState after actionInvariant
f2f3, e7e5, g2g4White king exposed on diagonalEach move legal in its own immutable position
d8h4White to move in check with no legal responseCHECKMATE 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
ActionState after actionInvariant
Consider e5d6 with rook on e8 and king on e1Pawn geometry permits the capturePseudo-legality is only the first stage
Simulate capture and remove d5 pawne-file opens; king attacked; move excludedA 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

Choose a deep dive

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.

Return to the lesson →

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.