feat: add castling execution to processMove

Detect castle moves via MoveValidator.isCastle and dispatch to
Board.withCastle so both king and rook are moved atomically.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LQ63
2026-03-24 16:28:48 +01:00
parent 62e180c6d9
commit 150e78e080
5 changed files with 45 additions and 1972 deletions
@@ -2,7 +2,7 @@ package de.nowchess.chess.controller
import scala.io.StdIn
import de.nowchess.api.board.{Board, Color, Piece}
import de.nowchess.chess.logic.{GameContext, MoveValidator, GameRules, PositionStatus}
import de.nowchess.chess.logic.{GameContext, MoveValidator, GameRules, PositionStatus, CastleSide, withCastle}
import de.nowchess.chess.view.Renderer
// ---------------------------------------------------------------------------
@@ -48,7 +48,12 @@ object GameController:
if !MoveValidator.isLegal(ctx, from, to) then
MoveResult.IllegalMove
else
val (newBoard, captured) = ctx.board.withMove(from, to)
val castleOpt = if MoveValidator.isCastle(ctx.board, from, to)
then Some(MoveValidator.castleSide(from, to))
else None
val (newBoard, captured) = castleOpt match
case Some(side) => (ctx.board.withCastle(turn, side), None)
case None => ctx.board.withMove(from, to)
val newCtx = ctx.copy(board = newBoard)
GameRules.gameStatus(newCtx, turn.opposite) match
case PositionStatus.Normal => MoveResult.Moved(newCtx, captured, turn.opposite)
@@ -203,3 +203,41 @@ class GameControllerTest extends AnyFunSuite with Matchers:
GameController.gameLoop(GameContext(b), Color.White)
output should include("captures")
output should include("Black is in check!")
// ──── castling execution ─────────────────────────────────────────────
test("processMove: e1g1 returns Moved with king on g1 and rook on f1"):
val ctx = GameContext(
board = Board(Map(
sq(File.E, Rank.R1) -> Piece.WhiteKing,
sq(File.H, Rank.R1) -> Piece.WhiteRook,
sq(File.H, Rank.R8) -> Piece.BlackKing
)),
whiteCastling = CastlingRights.Both,
blackCastling = CastlingRights.None
)
GameController.processMove(ctx, Color.White, "e1g1") match
case MoveResult.Moved(newCtx, captured, newTurn) =>
newCtx.board.pieceAt(sq(File.G, Rank.R1)) shouldBe Some(Piece.WhiteKing)
newCtx.board.pieceAt(sq(File.F, Rank.R1)) shouldBe Some(Piece.WhiteRook)
newCtx.board.pieceAt(sq(File.E, Rank.R1)) shouldBe None
newCtx.board.pieceAt(sq(File.H, Rank.R1)) shouldBe None
captured shouldBe None
newTurn shouldBe Color.Black
case other => fail(s"Expected Moved, got $other")
test("processMove: e1c1 returns Moved with king on c1 and rook on d1"):
val ctx = GameContext(
board = Board(Map(
sq(File.E, Rank.R1) -> Piece.WhiteKing,
sq(File.A, Rank.R1) -> Piece.WhiteRook,
sq(File.H, Rank.R8) -> Piece.BlackKing
)),
whiteCastling = CastlingRights.Both,
blackCastling = CastlingRights.None
)
GameController.processMove(ctx, Color.White, "e1c1") match
case MoveResult.Moved(newCtx, _, _) =>
newCtx.board.pieceAt(sq(File.C, Rank.R1)) shouldBe Some(Piece.WhiteKing)
newCtx.board.pieceAt(sq(File.D, Rank.R1)) shouldBe Some(Piece.WhiteRook)
case other => fail(s"Expected Moved, got $other")