feat: implement legal castling #1

Merged
Janis merged 12 commits from castling into main 2026-03-24 17:55:01 +01:00
3 changed files with 30 additions and 15 deletions
Showing only changes of commit c9a59d3ad1 - Show all commits
@@ -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.{MoveValidator, GameRules, PositionStatus}
import de.nowchess.chess.logic.{GameContext, MoveValidator, GameRules, PositionStatus}
import de.nowchess.chess.view.Renderer
// ---------------------------------------------------------------------------
@@ -49,7 +49,7 @@ object GameController:
MoveResult.IllegalMove
else
val (newBoard, captured) = board.withMove(from, to)
GameRules.gameStatus(newBoard, turn.opposite) match
GameRules.gameStatus(GameContext(newBoard), turn.opposite) match
case PositionStatus.Normal => MoveResult.Moved(newBoard, captured, turn.opposite)
case PositionStatus.InCheck => MoveResult.MovedInCheck(newBoard, captured, turn.opposite)
case PositionStatus.Mated => MoveResult.Checkmate(turn)
@@ -38,9 +38,9 @@ object GameRules:
.toSet
/** Position status for the side whose turn it is (`color`). */
def gameStatus(board: Board, color: Color): PositionStatus =
val moves = legalMoves(GameContext(board), color)
val inCheck = isInCheck(board, color)
def gameStatus(ctx: GameContext, color: Color): PositionStatus =
val moves = legalMoves(ctx, color)
val inCheck = isInCheck(ctx.board, color)
if moves.isEmpty && inCheck then PositionStatus.Mated
else if moves.isEmpty then PositionStatus.Drawn
else if inCheck then PositionStatus.InCheck
@@ -62,33 +62,30 @@ class GameRulesTest extends AnyFunSuite with Matchers:
test("gameStatus: checkmate returns Mated"):
// White Qh8, Ka6; Black Ka8
// Qh8 attacks Ka8 along rank 8; all escape squares covered (spec-verified position)
val b = board(
GameRules.gameStatus(ctx(
sq(File.H, Rank.R8) -> Piece.WhiteQueen,
sq(File.A, Rank.R6) -> Piece.WhiteKing,
sq(File.A, Rank.R8) -> Piece.BlackKing
)
GameRules.gameStatus(b, Color.Black) shouldBe PositionStatus.Mated
), Color.Black) shouldBe PositionStatus.Mated
test("gameStatus: stalemate returns Drawn"):
// White Qb6, Kc6; Black Ka8
// Black king has no legal moves and is not in check (spec-verified position)
val b = board(
GameRules.gameStatus(ctx(
sq(File.B, Rank.R6) -> Piece.WhiteQueen,
sq(File.C, Rank.R6) -> Piece.WhiteKing,
sq(File.A, Rank.R8) -> Piece.BlackKing
)
GameRules.gameStatus(b, Color.Black) shouldBe PositionStatus.Drawn
), Color.Black) shouldBe PositionStatus.Drawn
test("gameStatus: king in check with legal escape returns InCheck"):
// White Ra8 attacks Black Ke8 along rank 8; king can escape to d7, e7, f7
val b = board(
GameRules.gameStatus(ctx(
sq(File.A, Rank.R8) -> Piece.WhiteRook,
sq(File.E, Rank.R8) -> Piece.BlackKing
)
GameRules.gameStatus(b, Color.Black) shouldBe PositionStatus.InCheck
), Color.Black) shouldBe PositionStatus.InCheck
test("gameStatus: normal starting position returns Normal"):
GameRules.gameStatus(Board.initial, Color.White) shouldBe PositionStatus.Normal
GameRules.gameStatus(GameContext(Board.initial), Color.White) shouldBe PositionStatus.Normal
test("legalMoves: includes castling destination when available"):
val c = GameContext(
@@ -114,3 +111,21 @@ class GameRulesTest extends AnyFunSuite with Matchers:
blackCastling = CastlingRights.None
)
GameRules.legalMoves(c, Color.White) should not contain (sq(File.E, Rank.R1) -> sq(File.G, Rank.R1))
test("gameStatus: returns Normal (not Drawn) when castling is the only legal move"):
// White King e1, Rook h1 (kingside castling available).
// Black Rooks d2 and f2 box the king: d1 attacked by d2, e2 attacked by both,
// f1 attacked by f2. King cannot move to any adjacent square without entering
// an attacked square or an enemy piece. Only legal move: castle to g1.
val c = GameContext(
board = board(
sq(File.E, Rank.R1) -> Piece.WhiteKing,
sq(File.H, Rank.R1) -> Piece.WhiteRook,
sq(File.D, Rank.R2) -> Piece.BlackRook,
sq(File.F, Rank.R2) -> Piece.BlackRook,
sq(File.A, Rank.R8) -> Piece.BlackKing
),
whiteCastling = CastlingRights(kingSide = true, queenSide = false),
blackCastling = CastlingRights.None
)
GameRules.gameStatus(c, Color.White) shouldBe PositionStatus.Normal