From b6ab8ed6aca07def80039718e8ab4a9b168c47fb Mon Sep 17 00:00:00 2001 From: LQ63 Date: Tue, 24 Mar 2026 10:39:03 +0100 Subject: [PATCH] docs: add castling design spec Co-Authored-By: Claude Sonnet 4.6 --- .../specs/2026-03-24-castling-design.md | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-24-castling-design.md diff --git a/docs/superpowers/specs/2026-03-24-castling-design.md b/docs/superpowers/specs/2026-03-24-castling-design.md new file mode 100644 index 0000000..01a1ea0 --- /dev/null +++ b/docs/superpowers/specs/2026-03-24-castling-design.md @@ -0,0 +1,170 @@ +# 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 |