docs: add castling design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 |
|
||||
Reference in New Issue
Block a user