Files
NowChessSystems/docs/superpowers/specs/2026-03-24-castling-design.md
T
LQ63 b6ab8ed6ac docs: add castling design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-24 10:39:03 +01:00

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):

  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

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