13. Chess Game Intermediate → Advanced

A. Realistic Interview Prompt

"Design a two-player chess game. Support standard piece movement, detecting check and checkmate, and undoing a move. You do not need a graphical interface."

B. Difficulty

Intermediate → Advanced. Per-piece movement rules are mechanical; the real difficulty is check/checkmate detection (which requires simulating a move before committing to it) and building undo cleanly without an ad hoc "remember the last few fields I changed" hack.

C. What This Problem Tests

D. Requirements

Functional

  1. Standard 8×8 board, standard piece set and starting position for two players (White, Black)
  2. Enforce each piece type's legal movement, including captures
  3. Detect check, checkmate, and stalemate
  4. Reject any move that would leave the moving player's own king in check
  5. Support undo of the most recent move

Non-Functional

E. Clarifying Questions

#QuestionWhy It MattersDesign Decision Affected
1Are special moves (castling, en passant, pawn promotion) in scope?Each adds meaningful state and edge-case handling well beyond basic movementScope of Move's special-move flags (see F, V)
2Is this local two-player, or does it need to support two remote players over a network?Determines whether server-authoritative move validation is in scopeScope of W
3Is a full undo/redo history required, or just single-move undo?Affects whether moves are stored as a stack or a full navigable listData structure in S
4Do we need move legality only, or also position evaluation (for an AI opponent)?An AI opponent is an entirely separate, much larger scope (search + evaluation)Explicitly excluded by assumption (see F)

F. Assumptions

G. Actors

ActorResponsibility
PlayerSubmits moves for their color on their turn
ChessGame (facade)Validates, applies, and records moves; detects check/checkmate

H. Core Domain Entities

ClassResponsibilityKey AttributesKey MethodsDoes NOT Handle
Piece (abstract)Identity + its own movement rulecolor, positiongetValidMoves(board)Whether a move leaves its own king in check — that's the Game's job, one level up
Pawn, Rook, Knight, Bishop, Queen, KingConcrete movement rules—getValidMoves(board)—
Board8×8 grid of pieces, occupancy queriesgridgetPieceAt(), isSquareAttacked(square, byColor)Move legality by chess rules — only raw occupancy/attack facts
Move (Command)One executed move, reversiblefrom, to, movedPiece, capturedPiece, specialFlagexecute(board), undo(board)Deciding whether the move was legal in the first place
MoveHistoryStack of executed Move commands—push(), popAndUndo()—
ChessGame (facade)Turn order, move validation, check/checkmate detectionboard, currentTurn, historymakeMove(), undo(), isInCheck(color), isCheckmate(color)Piece-level movement math

I. Class Relationships

J. Class Diagram

*

creates

«abstract»

Piece

color

position

+getValidMoves(board)

Pawn

Rook

Knight

Bishop

Queen

King

Board

grid

+getPieceAt()

+isSquareAttacked()

Move

from

to

movedPiece

capturedPiece

+execute(board)

+undo(board)

MoveHistory

+push()

+popAndUndo()

ChessGame

board

currentTurn

+makeMove()

+undo()

+isInCheck()

+isCheckmate()

View Mermaid source
classDiagram
  class Piece {
    <<abstract>>
    color
    position
    +getValidMoves(board)
  }
  Piece <|-- Pawn
  Piece <|-- Rook
  Piece <|-- Knight
  Piece <|-- Bishop
  Piece <|-- Queen
  Piece <|-- King
  class Board {
    grid
    +getPieceAt()
    +isSquareAttacked()
  }
  class Move {
    from
    to
    movedPiece
    capturedPiece
    +execute(board)
    +undo(board)
  }
  class MoveHistory {
    +push()
    +popAndUndo()
  }
  class ChessGame {
    board
    currentTurn
    +makeMove()
    +undo()
    +isInCheck()
    +isCheckmate()
  }
  Board --> "*" Piece
  ChessGame --> Board
  ChessGame --> MoveHistory
  ChessGame --> Move : creates

K. Responsibility Mapping

ClassResponsibilityDoes NOT Handle
Piece subclassesOwn movement-generation ruleWhether the resulting position leaves the mover's king in check
BoardOccupancy and attack queriesChess-rule legality itself
MoveExecuting and reversing one specific move, including side effectsDeciding legality upfront
ChessGameTurn order, legality gate (including self-check rejection), checkmate detectionPiece-level movement math, raw board storage

L. SOLID Analysis

PrincipleHow It's AppliedNaive Violation
SRPEach piece subclass owns only its own movement rule; ChessGame owns turn/check orchestration; Move owns execute/undoOne Piece class with a type enum and a giant switch inside getValidMoves()
OCPA chess variant with a new piece is a new Piece subclass, zero changes to ChessGameAdding a new piece type means editing a central switch statement everywhere piece behavior is referenced
LSPEvery Piece subclass honestly returns only its own legal geometric moves; ChessGame layers the check-rejection filter identically on top of any piece typeA King subclass secretly also implementing "am I in check" logic that other pieces don't have, breaking substitutability in getValidMoves() callers
ISPPiece stays a small interface (position, color, movement); undo/history concerns live entirely in Move/MoveHistory, not forced onto PieceForcing every Piece to implement its own undo() instead of centralizing that in Move
DIPChessGame depends on the abstract Piece.getValidMoves(), never on a concrete piece typeChessGame.isInCheck() doing instanceof Rook/instanceof Bishop checks inline instead of delegating uniformly

M. Design Patterns

PatternProblem It SolvesWhere It AppearsWhen NOT to Use
CommandMoves need to be executed, undone, and recorded uniformlyMove, MoveHistorySkip only if undo/history is explicitly out of scope
Template Method (optional)Every piece's move-generation shares a "generate candidates, filter off-board, filter same-color-occupied" skeletonOptional shared base logic in Piece with a generateCandidates() hookSkip if each piece's geometry is different enough that a shared skeleton adds more ceremony than it saves

Explicitly avoided: Strategy for piece movement — see I for why plain polymorphism is the correct, deliberate choice, not a missed opportunity.

N. What Is Likely to Change?

BehaviorCurrent DesignAnticipated ChangeProtecting Abstraction
Special movesBasic movement onlyCastling, en passant, promotionMove's optional specialFlag and side-effect fields, without changing the base execute/undo contract
Network playLocal, trusted calls to makeMove()Two remote clients submitting movesChessGame becomes the single server-authoritative validator; no change to Piece/Board
Opponent typeTwo human playersAn AI opponentA new evaluation/search layer consuming getValidMoves() as its move generator — entirely additive

O. Design Evolution

What stays stable: Piece.getValidMoves()'s signature and ChessGame.makeMove()'s public contract. Special moves and network play both layer on without touching the base movement model.

P. API / Interface Design

abstract class Piece {
  Color color; Position position;
  abstract List<Position> getValidMoves(Board board);
}

class Move {
  void execute(Board board);
  void undo(Board board);
}

class ChessGame {
  MoveResult makeMove(Position from, Position to);
  void undo();
  boolean isInCheck(Color color);
  boolean isCheckmate(Color color);
}

Ownership/validation notes: makeMove() first asks the piece at from for its candidate moves, confirms to is among them, then simulates the move on the board, checks isInCheck(movingColor) against the simulated result, and only commits (pushes to MoveHistory) if the simulated position is legal — otherwise it reverts the simulation and rejects the move.

Q. Pseudocode — Critical Logic

ChessGame.makeMove(from, to):
  piece = board.getPieceAt(from)
  if piece == null or piece.color != currentTurn: reject
  if to not in piece.getValidMoves(board): reject

  move = new Move(from, to, piece, board.getPieceAt(to))
  move.execute(board)                      // simulate
  if isInCheck(currentTurn):
    move.undo(board)                       // illegal — leaves own king in check
    reject
  history.push(move)                        // commit
  currentTurn = opponent(currentTurn)
  if isCheckmate(currentTurn): declareWinner(piece.color)

Board.isSquareAttacked(square, byColor):
  for each piece on board where piece.color == byColor:
    if square in piece.getValidMoves(board): return true
  return false

R. End-to-End Use Case

makeMove(e2, e4)  — White pawn
  │  e4 in Pawn.getValidMoves(board)? yes → simulate → isInCheck(WHITE)? no → commit
  ▼
makeMove(e7, e5)  — Black pawn (symmetric)
  ▼
... play continues ...
  ▼
makeMove(d8, h4)  — Black queen delivers check
  │  after commit: isInCheck(WHITE) == true
  ▼
White's next makeMove() candidates are filtered: any move that doesn't resolve
the check is rejected the same way as any other self-check-inducing move
  ▼
If no legal move exists for WHITE while in check → isCheckmate(WHITE) == true → game ends

S. Data Structures

StructureUsed ForWhyAlternative & Trade-off
8×8 array of Piece referencesBoard storageO(1) position lookup, matches the physical board directlyA bitboard (64-bit integer per piece type) is the real high-performance chess-engine representation, but is overkill complexity for an LLD interview unless explicitly asked for engine-level performance
Stack of Move (Command) objectsUndo historyO(1) push and pop-and-undo of the most recent moveFull board-snapshot-per-move (Memento) trades higher memory for simpler reasoning — see AB

T. Complexity Analysis

OperationTimeSpace
getValidMoves() per pieceO(1) to O(8) depending on piece type (sliding pieces scan up to board length)O(1)
isInCheck() / isSquareAttacked()O(pieces) × O(moves per piece) ≈ O(32 × 8) worst caseO(1)
isCheckmate()O(pieces × moves per piece) to enumerate all candidate escapes, each simulatedO(1) per simulation

U. Testing Strategy

V. Interviewer Follow-Ups

Follow-UpTestingDesign ChangeClasses ChangedPattern/Trade-off
"Add castling, en passant, and pawn promotion."Recognizing this as the single highest-complexity increment in the whole problemMove gains special-move flags and side-effect state (rook position for castling, en passant target square); King/Rook track "has moved"Move, King, Rook, ChessGameA strong candidate names the complexity explicitly rather than hand-waving it (echoes File System's symlink follow-up)
"Support two players over a network."Server-authoritative validation reasoningChessGame becomes the single source of truth a server runs; clients submit intents, not stateChessGame (deployment context), no core-model changeValidates the Piece/Board separation from network concerns
"Add an AI opponent."LLD/HLD (and LLD/AI) boundary awarenessN/A at this layer — candidate should name minimax/alpha-beta position evaluation as a separate, much larger scopeN/ARecognizing the boundary is the actual signal

W. Concurrency

A single game is inherently turn-based and single-writer — only one move is ever being validated/applied at a time, so there is no shared-resource race analogous to Parking Lot's or Elevator's. The one real concern, and only if network play is in scope, is server-side move authority: the server (running the same ChessGame) must be the sole place a move is validated and committed, rejecting a client's move if it doesn't match the server's current turn/board state — a staleness check, not a lock.

X. Persistence

Move history doubles naturally as a persistence log — replaying the MoveHistory stack from the initial position reconstructs the full game, which is exactly how real chess notation (PGN) works. Persisting a game is therefore "serialize the move list," not "serialize the board," which is both more compact and naturally resumable.

Y. Scaling Boundary

LayerWhat Belongs Here
LLDPiece movement, board, move Command/undo, check/checkmate detection
HLDMatchmaking, ranking/rating systems, spectator broadcast at scale
Separate large domainAI opponent (search + evaluation) — related to, but distinct from, this LLD problem

Z. Failure Scenarios

AA. Common Bad Designs

AB. Design Trade-Offs

DecisionOption AOption BRecommendation
Undo mechanismCommand (store move delta + reverse operation)Memento (store a full board snapshot per move)Command for memory efficiency over a long game; Memento only if the incidental-state bookkeeping for reversal (castling rights, en passant target) becomes error-prone enough that simplicity outweighs the memory cost
Board representation2D array of piece referencesBitboards (one 64-bit integer per piece type)Array for an LLD interview — clear and directly maps to the problem; bitboards are a real chess-engine performance technique, not an LLD-interview expectation unless explicitly asked

AC. Interviewer Evaluation

Strong signal: explicitly explaining why piece movement is plain polymorphism and not Strategy; reusing getValidMoves() for check detection instead of writing a second algorithm; simulate-then-validate-then-commit as the shape of makeMove(); naming castling/en passant/promotion as a real complexity jump rather than glossing over them.

Red flag: a type-enum-and-switch Piece class; a separate hand-rolled check-detection algorithm; undo that only restores the moved piece's position; reflexively reaching for Strategy on piece movement without justification.

AD. Interview Simulation

Interviewer: Design a chess game.
Candidate: Each piece type has its own movement rule, so I'd make Piece abstract with a concrete subclass per type — Rook, Bishop, and so on — each implementing its own getValidMoves(). I'd deliberately avoid Strategy here since a piece's type and its movement rule are the same fact and always change together.
Interviewer: How do you detect check?
Candidate: "In check" just means the king's square is attacked — I'd reuse the exact same getValidMoves() every piece already implements: scan the opponent's pieces, and if any of their valid moves include my king's square, I'm in check. No separate algorithm.
Interviewer: How would you support undo?
Candidate: Model each move as a Command with execute() and undo(), storing the captured piece and any side effects, and push it onto a history stack. Undo is just pop-and-undo — and I actually reuse that same reversal path when a simulated move turns out to leave my own king in check.
Interviewer: What about castling and en passant?
Candidate: I'd flag those explicitly as the highest-complexity part of the design — castling needs "has this king or rook ever moved" tracking plus an attacked-square check along the king's path, and en passant needs one extra piece of transient state for the target square. I wouldn't want to quietly simplify those away without naming the cost.

AE. Online Solution Comparison

SourceApproachStrengthWeaknessWorth Learning
AlgoMaster.io — Design Chess GamePiece class hierarchy with per-piece movement, a Board class, and a Game orchestrator, presented as a stepping-stone LLD problemConfirms the Piece-subclass (not Strategy) shape as the expected answerPublic outline does not deeply emphasize reusing movement logic for check detection, or the Command-based undo trade-offValidates the core class shape; this document's contribution is the explicit Strategy-rejection reasoning and the Command/Memento trade-off
GeeksforGeeks — Design a Chess GameClass-diagram-first approach covering Board, Piece hierarchy, and Player, with movement validation per pieceClear separation of Board from Piece responsibilitiesLess emphasis on check/checkmate as a reused attack-query rather than a bespoke algorithmUseful as a structural cross-check for the Board/Piece boundary

AF. Interview Cheat Sheet

ProblemChess Game
DifficultyIntermediate → Advanced
Core requirementsStandard movement, check/checkmate/stalemate detection, undo
Key clarificationsSpecial moves in scope? Local or networked? Undo/redo depth? AI opponent needed?
Core entitiesPiece (+ 6 subclasses), Board, Move, MoveHistory, ChessGame
Key relationshipsPlain polymorphism for movement (Strategy deliberately rejected); Command for moves; check reuses move-generation
Key patternsCommand (headline), optional Template Method for shared move-generation steps
Most important decisionSimulate → validate (no self-check) → commit, as one atomic sequence per move
Likely-changing behaviorSpecial moves, network play, AI opponent
Concurrency concernServer-authoritative move validation for networked play; otherwise single-writer by nature
Top follow-upsCastling/en passant/promotion, networked play, AI opponent (LLD/AI boundary)
Common mistakesType-enum-and-switch Piece class; duplicated check-detection algorithm; incomplete undo; reflexive Strategy on piece movement
What NOT to over-engineerDon't build bitboards or an AI evaluation engine unless explicitly asked

AG. Full Implementation

A complete, compilable reference implementation of every class in the diagram above (composition, inheritance, interface realization, and dependency-injected strategies all preserved per section I), with a runnable demo exercising the primary flow — in Java, C++, JavaScript, and TypeScript.

// Chess Game - full LLD implementation (single-file demo).
// No "Concurrency" badge -> single-threaded, no locks needed.
//
// Design notes:
// - Piece uses plain subclassing (not Strategy): a piece's type and its movement rule
//   are the same fact and always change together, so Piece -> Rook/Bishop/... is the
//   honest simpler answer.
// - Move is a Command: it knows how to execute()/undo() itself (including restoring a
//   captured piece), so ChessGame.undo() is a one-line pop-and-undo off MoveHistory.
// - Board.isSquareAttacked() reuses each piece's own getValidMoves() rather than a
//   second parallel "attack" implementation - check detection is exactly "does any
//   opposing piece's move-set include this square".
import java.util.*;

public class p13 {

    enum Color { WHITE, BLACK }

    enum MoveResult { OK, CHECK, CHECKMATE, STALEMATE, REJECTED_ILLEGAL, REJECTED_SELF_CHECK }

    // ---------- Position ----------
    static final class Position {
        final int row, col;
        Position(int row, int col) { this.row = row; this.col = col; }
        boolean inBounds() { return row >= 0 && row < 8 && col >= 0 && col < 8; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Position)) return false;
            Position p = (Position) o;
            return p.row == row && p.col == col;
        }
        @Override public int hashCode() { return row * 8 + col; }
        @Override public String toString() { return "" + (char) ('a' + col) + (8 - row); }
    }

    // ---------- Piece hierarchy ----------
    static abstract class Piece {
        Color color;
        Position position;
        Piece(Color color, Position position) { this.color = color; this.position = position; }

        // Pseudo-legal moves: respects board occupancy/own-piece blocking, but does NOT
        // check whether the move leaves the mover's own king in check - that is
        // ChessGame's job, one level up.
        abstract List<Position> getValidMoves(Board board);
        abstract char symbol();

        // Shared helper: walk a direction until off-board, own piece, or a capture.
        void slide(Board board, List<Position> out, int dr, int dc) {
            int r = position.row + dr, c = position.col + dc;
            while (true) {
                Position p = new Position(r, c);
                if (!p.inBounds()) break;
                Piece occupant = board.getPieceAt(p);
                if (occupant == null) { out.add(p); }
                else { if (occupant.color != color) out.add(p); break; }
                r += dr; c += dc;
            }
        }
    }

    static class Rook extends Piece {
        Rook(Color c, Position p) { super(c, p); }
        List<Position> getValidMoves(Board board) {
            List<Position> moves = new ArrayList<>();
            for (int[] d : new int[][]{{1,0},{-1,0},{0,1},{0,-1}}) slide(board, moves, d[0], d[1]);
            return moves;
        }
        char symbol() { return color == Color.WHITE ? 'R' : 'r'; }
    }

    static class Bishop extends Piece {
        Bishop(Color c, Position p) { super(c, p); }
        List<Position> getValidMoves(Board board) {
            List<Position> moves = new ArrayList<>();
            for (int[] d : new int[][]{{1,1},{1,-1},{-1,1},{-1,-1}}) slide(board, moves, d[0], d[1]);
            return moves;
        }
        char symbol() { return color == Color.WHITE ? 'B' : 'b'; }
    }

    static class Queen extends Piece {
        Queen(Color c, Position p) { super(c, p); }
        List<Position> getValidMoves(Board board) {
            List<Position> moves = new ArrayList<>();
            for (int[] d : new int[][]{{1,0},{-1,0},{0,1},{0,-1},{1,1},{1,-1},{-1,1},{-1,-1}})
                slide(board, moves, d[0], d[1]);
            return moves;
        }
        char symbol() { return color == Color.WHITE ? 'Q' : 'q'; }
    }

    static class Knight extends Piece {
        Knight(Color c, Position p) { super(c, p); }
        List<Position> getValidMoves(Board board) {
            List<Position> moves = new ArrayList<>();
            int[][] deltas = {{1,2},{2,1},{-1,2},{-2,1},{1,-2},{2,-1},{-1,-2},{-2,-1}};
            for (int[] d : deltas) {
                Position p = new Position(position.row + d[0], position.col + d[1]);
                if (!p.inBounds()) continue;
                Piece occ = board.getPieceAt(p);
                if (occ == null || occ.color != color) moves.add(p);
            }
            return moves;
        }
        char symbol() { return color == Color.WHITE ? 'N' : 'n'; }
    }

    static class King extends Piece {
        King(Color c, Position p) { super(c, p); }
        List<Position> getValidMoves(Board board) {
            List<Position> moves = new ArrayList<>();
            for (int dr = -1; dr <= 1; dr++) {
                for (int dc = -1; dc <= 1; dc++) {
                    if (dr == 0 && dc == 0) continue;
                    Position p = new Position(position.row + dr, position.col + dc);
                    if (!p.inBounds()) continue;
                    Piece occ = board.getPieceAt(p);
                    if (occ == null || occ.color != color) moves.add(p);
                }
            }
            return moves; // castling intentionally omitted - out of scope for this exercise
        }
        char symbol() { return color == Color.WHITE ? 'K' : 'k'; }
    }

    static class Pawn extends Piece {
        Pawn(Color c, Position p) { super(c, p); }
        List<Position> getValidMoves(Board board) {
            List<Position> moves = new ArrayList<>();
            int dir = color == Color.WHITE ? -1 : 1;
            int startRow = color == Color.WHITE ? 6 : 1;

            Position oneStep = new Position(position.row + dir, position.col);
            if (oneStep.inBounds() && board.getPieceAt(oneStep) == null) {
                moves.add(oneStep);
                Position twoStep = new Position(position.row + 2 * dir, position.col);
                if (position.row == startRow && board.getPieceAt(twoStep) == null) moves.add(twoStep);
            }
            for (int dc : new int[]{-1, 1}) {
                Position diag = new Position(position.row + dir, position.col + dc);
                if (!diag.inBounds()) continue;
                Piece occ = board.getPieceAt(diag);
                if (occ != null && occ.color != color) moves.add(diag); // capture only diagonally
            }
            return moves; // en passant / promotion intentionally omitted
        }
        char symbol() { return color == Color.WHITE ? 'P' : 'p'; }
    }

    // ---------- Board ----------
    static class Board {
        private final Piece[][] grid = new Piece[8][8];

        Piece getPieceAt(Position p) { return p.inBounds() ? grid[p.row][p.col] : null; }

        void setPieceAt(Position p, Piece piece) {
            grid[p.row][p.col] = piece;
            if (piece != null) piece.position = p;
        }

        List<Piece> getAllPieces(Color color) {
            List<Piece> pieces = new ArrayList<>();
            for (Piece[] row : grid) for (Piece p : row) if (p != null && p.color == color) pieces.add(p);
            return pieces;
        }

        // Reuses each piece's own getValidMoves() - the same movement logic already
        // written for regular moves, not a second parallel "attack" implementation.
        boolean isSquareAttacked(Position square, Color byColor) {
            for (Piece piece : getAllPieces(byColor)) {
                if (piece.getValidMoves(this).contains(square)) return true;
            }
            return false;
        }

        void setupStandardPosition() {
            Color[] rows = {Color.BLACK, Color.WHITE};
            int[] backRanks = {0, 7};
            int[] pawnRanks = {1, 6};
            for (int i = 0; i < 2; i++) {
                Color color = rows[i];
                int back = backRanks[i];
                setPieceAt(new Position(back, 0), new Rook(color, null));
                setPieceAt(new Position(back, 1), new Knight(color, null));
                setPieceAt(new Position(back, 2), new Bishop(color, null));
                setPieceAt(new Position(back, 3), new Queen(color, null));
                setPieceAt(new Position(back, 4), new King(color, null));
                setPieceAt(new Position(back, 5), new Bishop(color, null));
                setPieceAt(new Position(back, 6), new Knight(color, null));
                setPieceAt(new Position(back, 7), new Rook(color, null));
                for (int c = 0; c < 8; c++) setPieceAt(new Position(pawnRanks[i], c), new Pawn(color, null));
            }
        }

        void print() {
            for (int r = 0; r < 8; r++) {
                StringBuilder sb = new StringBuilder();
                for (int c = 0; c < 8; c++) {
                    Piece p = grid[r][c];
                    sb.append(p == null ? '.' : p.symbol()).append(' ');
                }
                System.out.println(sb);
            }
        }
    }

    // ---------- Move (Command) ----------
    static class Move {
        final Position from, to;
        final Piece movedPiece;
        final Piece capturedPiece;
        String specialFlag = "NONE"; // extension point for castling/promotion, unused here

        Move(Position from, Position to, Piece movedPiece, Piece capturedPiece) {
            this.from = from; this.to = to; this.movedPiece = movedPiece; this.capturedPiece = capturedPiece;
        }

        void execute(Board board) {
            board.setPieceAt(to, movedPiece);
            board.setPieceAt(from, null);
        }

        // Restores both the moved piece's square AND the captured piece - this is what
        // makes undo a single call instead of manually diffing board state.
        void undo(Board board) {
            board.setPieceAt(from, movedPiece);
            board.setPieceAt(to, capturedPiece);
        }
    }

    // ---------- MoveHistory ----------
    static class MoveHistory {
        private final Deque<Move> stack = new ArrayDeque<>();
        void push(Move move) { stack.push(move); }
        Move popAndUndo(Board board) {
            if (stack.isEmpty()) return null;
            Move move = stack.pop();
            move.undo(board);
            return move;
        }
        boolean isEmpty() { return stack.isEmpty(); }
    }

    // ---------- ChessGame (facade) ----------
    static class ChessGame {
        final Board board = new Board();
        Color currentTurn = Color.WHITE;
        final MoveHistory history = new MoveHistory();

        ChessGame() { board.setupStandardPosition(); }

        Color opponent(Color c) { return c == Color.WHITE ? Color.BLACK : Color.WHITE; }

        Position findKing(Color color) {
            for (Piece p : board.getAllPieces(color)) if (p instanceof King) return p.position;
            throw new IllegalStateException("King missing for " + color);
        }

        boolean isInCheck(Color color) {
            return board.isSquareAttacked(findKing(color), opponent(color));
        }

        // Checkmate iff in check AND no legal move (for any of this color's pieces)
        // escapes check when simulated.
        boolean isCheckmate(Color color) {
            if (!isInCheck(color)) return false;
            return !hasAnyLegalMove(color);
        }

        boolean isStalemate(Color color) {
            return !isInCheck(color) && !hasAnyLegalMove(color);
        }

        private boolean hasAnyLegalMove(Color color) {
            for (Piece piece : new ArrayList<>(board.getAllPieces(color))) {
                for (Position dest : piece.getValidMoves(board)) {
                    Move trial = new Move(piece.position, dest, piece, board.getPieceAt(dest));
                    trial.execute(board);
                    boolean stillInCheck = isInCheck(color);
                    trial.undo(board);
                    if (!stillInCheck) return true;
                }
            }
            return false;
        }

        MoveResult makeMove(Position from, Position to) {
            Piece piece = board.getPieceAt(from);
            if (piece == null || piece.color != currentTurn) return MoveResult.REJECTED_ILLEGAL;
            if (!piece.getValidMoves(board).contains(to)) return MoveResult.REJECTED_ILLEGAL;

            Move move = new Move(from, to, piece, board.getPieceAt(to));
            move.execute(board); // simulate first
            if (isInCheck(currentTurn)) {
                move.undo(board); // illegal - leaves own king in check
                return MoveResult.REJECTED_SELF_CHECK;
            }
            history.push(move); // commit
            currentTurn = opponent(currentTurn);

            if (isCheckmate(currentTurn)) return MoveResult.CHECKMATE;
            if (isStalemate(currentTurn)) return MoveResult.STALEMATE;
            if (isInCheck(currentTurn)) return MoveResult.CHECK;
            return MoveResult.OK;
        }

        void undo() {
            if (history.isEmpty()) return;
            history.popAndUndo(board);
            currentTurn = opponent(currentTurn);
        }
    }

    // ---------- Demo ----------
    public static void main(String[] args) {
        ChessGame game = new ChessGame();
        game.board.print();

        // 1. e2-e4 (White pawn double step)
        System.out.println(game.makeMove(new Position(6, 4), new Position(4, 4)));
        // 2. e7-e5 (Black pawn double step)
        System.out.println(game.makeMove(new Position(1, 4), new Position(3, 4)));
        // 3. Queen out for White: d1-h5
        System.out.println(game.makeMove(new Position(7, 3), new Position(3, 7)));
        // Illegal: it's Black's turn now, but this tries to move a White pawn
        System.out.println(game.makeMove(new Position(6, 0), new Position(5, 0)));

        System.out.println("Is white in check? " + game.isInCheck(Color.WHITE));
        game.undo(); // undo the queen move
        game.board.print();
    }
}