From ed540e38d6f02f7266f35685e5b737902925bea1 Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 7 Apr 2026 20:18:47 +0200 Subject: [PATCH] fix: NCS-32 Queenside Castle doesn't care about pieces in the way --- .../de/nowchess/rules/sets/DefaultRules.scala | 6 ++++- .../de/nowchess/rule/DefaultRulesTest.scala | 24 ++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala b/modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala index dc78274..4de0665 100644 --- a/modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala +++ b/modules/rule/src/main/scala/de/nowchess/rules/sets/DefaultRules.scala @@ -170,7 +170,11 @@ object DefaultRules extends RuleSet: castlingMove: CastlingMove ): Unit = if castlingRight then - val clearSqs = List(castlingMove.middleAlg, castlingMove.kingToAlg).flatMap(Square.fromAlgebraic) + // For queenside, also check the square between king destination and rook start + val extraSqs = if castlingMove.kingToAlg == "c1" then List("b1") + else if castlingMove.kingToAlg == "c8" then List("b8") + else List.empty + val clearSqs = (List(castlingMove.middleAlg, castlingMove.kingToAlg) ++ extraSqs).flatMap(Square.fromAlgebraic) if squaresEmpty(context.board, clearSqs) then for kf <- Square.fromAlgebraic(castlingMove.kingFromAlg) diff --git a/modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesTest.scala b/modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesTest.scala index 93d6e8d..5f7425f 100644 --- a/modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesTest.scala +++ b/modules/rule/src/test/scala/de/nowchess/rule/DefaultRulesTest.scala @@ -52,7 +52,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers: val moves = rules.allLegalMoves(context) // King must move; e2 should be valid but d1 might be blocked by rook if still on same file - moves.filter(m => m.from == Square(File.E, Rank.R1)).nonEmpty shouldBe true + moves.exists(m => m.from == Square(File.E, Rank.R1)) shouldBe true test("king cannot move to square attacked by opponent"): // FEN: white king e1, black rook e2 defended by black king e3 @@ -109,6 +109,28 @@ class DefaultRulesTest extends AnyFunSuite with Matchers: val castles = moves.filter(m => m.moveType == MoveType.CastleKingside) castles.isEmpty shouldBe true + test("castling queenside is illegal when knight blocks on b8"): + // Black king e8, black rook a8, black knight b8 (blocks queenside path) + val board = Board(Map( + Square(File.A, Rank.R8) -> Piece(Color.Black, PieceType.Rook), + Square(File.B, Rank.R8) -> Piece(Color.Black, PieceType.Knight), + Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King), + Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook), + Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King) + )) + val context = GameContext( + board = board, + turn = Color.Black, + castlingRights = CastlingRights(whiteKingSide = true, whiteQueenSide = true, blackKingSide = true, blackQueenSide = true), + enPassantSquare = None, + halfMoveClock = 0, + moves = List.empty + ) + val moves = rules.allLegalMoves(context) + + val castles = moves.filter(m => m.moveType == MoveType.CastleQueenside) + castles.isEmpty shouldBe true + // ── En passant legality ────────────────────────────────────────── test("en passant is legal when en passant square is set"):