# Castling Implementation Design **Date:** 2026-03-24 **Status:** Approved **Branch:** castling --- ## Context The NowChessSystems chess engine currently operates on a raw `Board` (opaque `Map[Square, Piece]`) paired with a `Color` for turn tracking. Castling requires tracking whether the king and rooks have previously moved — state that does not exist in the current engine layer. The `CastlingRights` and `MoveType.Castle*` types are already defined in the `api` module but are not wired into the engine. --- ## Approach: `GameContext` Wrapper (Option B) Introduce a thin `GameContext` wrapper in `modules/core` that bundles `Board` with castling rights for both sides. This is the single seam through which the engine learns about castling availability without pulling in the full FEN-structured `GameState` type. --- ## Section 1 — `GameContext` Type **Location:** `modules/core/src/main/scala/de/nowchess/chess/logic/GameContext.scala` ```scala case class GameContext( board: Board, whiteCastling: CastlingRights, blackCastling: CastlingRights ): def castlingFor(color: Color): CastlingRights = if color == Color.White then whiteCastling else blackCastling def withUpdatedRights(color: Color, rights: CastlingRights): GameContext = if color == Color.White then copy(whiteCastling = rights) else copy(blackCastling = rights) ``` `GameContext.initial` wraps `Board.initial` with `CastlingRights.Both` for both sides. `gameLoop` and `processMove` replace `(board: Board, turn: Color)` with `(ctx: GameContext, turn: Color)`. All `MoveResult` variants that previously carried `newBoard: Board` now carry `newCtx: GameContext`. --- ## Section 2 — Board Extension for Castle Moves `Board.withMove(from, to)` moves a single piece. Castling moves two pieces atomically. A new extension method is added to `Board`: ```scala def withCastle(color: Color, side: CastleSide): Board ``` - **Kingside White:** King e1→g1, Rook h1→f1 - **Queenside White:** King e1→c1, Rook a1→d1 - **Kingside Black:** King e8→g8, Rook h8→f8 - **Queenside Black:** King e8→c8, Rook a8→d8 `CastleSide` is a two-value enum (`Kingside | Queenside`) defined alongside `GameContext` in `core`. --- ## Section 3 — `MoveValidator` Castling Logic A new method `castlingTargets(ctx: GameContext, color: Color): Set[Square]` is added to `MoveValidator`. It returns the king destination squares for which castling is currently legal. For each side, it checks all six conditions in order (failing fast): 1. `CastlingRights` flag is `true` for that side 2. King is on its home square (e1 for White, e8 for Black) 3. Relevant rook is on its home square (h-file for kingside, a-file for queenside) 4. All squares between king and rook are empty 5. King is **not currently in check** (`GameRules.isInCheck`) 6. Each square the king **passes through and lands on** is not attacked by any enemy piece Transit and landing squares: - Kingside: f1/g1 (White), f8/g8 (Black) - Queenside: d1/c1 (White), d8/c8 (Black) — b1/b8 must be empty but king does not pass through them `legalTargets(board, from)` is extended to accept `GameContext` instead of `Board`. When the piece on `from` is a King, the result unions normal king moves with `castlingTargets`. All other piece types are unaffected. The public `isLegal(ctx, from, to)` delegate is updated accordingly. --- ## Section 4 — `GameController` Changes ### Move detection `processMove` identifies a castling move by the king moving exactly two files laterally from its home square: - White: e1→g1 (kingside) or e1→c1 (queenside) - Black: e8→g8 (kingside) or e8→c8 (queenside) ### Castle execution When a castling move is detected and validated: 1. Call `board.withCastle(color, side)` to move both pieces atomically. 2. Revoke **both** castling rights for the moving color in the new `GameContext`. ### Rights revocation on rook moves When any piece moves from a rook's home square, the corresponding right is revoked: - `a1` move → revoke white queenside - `h1` move → revoke white kingside - `a8` move → revoke black queenside - `h8` move → revoke black kingside This is evaluated on every normal move in `processMove`, not just rook moves (a king capturing on a1 should also revoke queenside rights). ### Signatures ```scala def processMove(ctx: GameContext, turn: Color, raw: String): MoveResult def gameLoop(ctx: GameContext, turn: Color): Unit ``` `MoveResult.Moved`, `MovedInCheck` carry `newCtx: GameContext` instead of `newBoard: Board`. On checkmate/stalemate reset, `GameContext.initial` is used. --- ## Section 5 — Move Notation The player types standard coordinate notation: - `e1g1` → White kingside castle - `e1c1` → White queenside castle - `e8g8` → Black kingside castle - `e8c8` → Black queenside castle No parser changes required. The controller identifies castling by the king moving 2 files from the home square. --- ## Section 6 — Testing ### `MoveValidatorTest` - Castling target is returned when all conditions are met (kingside White) - Castling target is returned when all conditions are met (queenside White) - Castling target is returned for Black kingside and queenside - Castling blocked when transit square is occupied - Castling blocked when king is in check - Castling blocked when transit/landing square is attacked - Castling blocked when `kingSide = false` in `CastlingRights` - Castling blocked when `queenSide = false` in `CastlingRights` - Castling blocked when rook is not on its home square ### `GameControllerTest` - `processMove` with `e1g1` returns `Moved` with king on g1 and rook on f1 - `processMove` with `e1c1` returns `Moved` with king on c1 and rook on d1 - `processMove` castle attempt after king has moved returns `IllegalMove` - `processMove` castle attempt after rook has moved returns `IllegalMove` - Normal rook move from h1 revokes kingside rights in the returned context ### `GameRulesTest` - `legalMoves` includes castling destinations when available - `legalMoves` excludes castling when king is in check --- ## Files to Create / Modify | Action | File | |--------|------| | **Create** | `modules/core/src/main/scala/de/nowchess/chess/logic/GameContext.scala` | | **Modify** | `modules/api/src/main/scala/de/nowchess/api/board/Board.scala` — add `withCastle` | | **Modify** | `modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala` — add `castlingTargets`, update signatures | | **Modify** | `modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala` — update `legalMoves` signature | | **Modify** | `modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala` — use `GameContext` | | **Modify** | `modules/core/src/main/scala/de/nowchess/chess/Main.scala` — use `GameContext.initial` | | **Modify** | `modules/core/src/test/scala/de/nowchess/chess/logic/MoveValidatorTest.scala` — new tests | | **Modify** | `modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala` — update + new tests | | **Modify** | `modules/core/src/test/scala/de/nowchess/chess/logic/GameRulesTest.scala` — update + new tests |