Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.0 KiB
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
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:
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):
CastlingRightsflag istruefor that side- King is on its home square (e1 for White, e8 for Black)
- Relevant rook is on its home square (h-file for kingside, a-file for queenside)
- All squares between king and rook are empty
- King is not currently in check (
GameRules.isInCheck) - 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:
- Call
board.withCastle(color, side)to move both pieces atomically. - 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:
a1move → revoke white queensideh1move → revoke white kingsidea8move → revoke black queensideh8move → 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
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 castlee1c1→ White queenside castlee8g8→ Black kingside castlee8c8→ 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 = falseinCastlingRights - Castling blocked when
queenSide = falseinCastlingRights - Castling blocked when rook is not on its home square
GameControllerTest
processMovewithe1g1returnsMovedwith king on g1 and rook on f1processMovewithe1c1returnsMovedwith king on c1 and rook on d1processMovecastle attempt after king has moved returnsIllegalMoveprocessMovecastle attempt after rook has moved returnsIllegalMove- Normal rook move from h1 revokes kingside rights in the returned context
GameRulesTest
legalMovesincludes castling destinations when availablelegalMovesexcludes 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 |