docs: add castling design spec

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LQ63
2026-03-24 10:39:03 +01:00
parent 7662b8ea10
commit b6ab8ed6ac
@@ -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 |