From fe8e3c05397f433bfa34d1999e9738c82790adf7 Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 7 Apr 2026 20:32:48 +0200 Subject: [PATCH] fix: NCS-32 Queenside Castle doesn't care about pieces in the way (#23) Reviewed-on: https://git.janis-eccarius.de/NowChess/NowChessSystems/pulls/23 Co-authored-by: Janis Co-committed-by: Janis --- .../de/nowchess/rules/sets/DefaultRules.scala | 9 ++++++- .../de/nowchess/rule/DefaultRulesTest.scala | 24 ++++++++++++++++++- 2 files changed, 31 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..e9e4474 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 @@ -163,6 +163,12 @@ object DefaultRules extends RuleSet: CastlingMove("e8", "c8", "d8", "a8", MoveType.CastleQueenside)) moves.toList + private def queensideBSquare(kingToAlg: String): List[String] = + kingToAlg match + case "c1" => List("b1") + case "c8" => List("b8") + case _ => List.empty + private def addCastleMove( context: GameContext, moves: scala.collection.mutable.ListBuffer[Move], @@ -170,7 +176,8 @@ object DefaultRules extends RuleSet: castlingMove: CastlingMove ): Unit = if castlingRight then - val clearSqs = List(castlingMove.middleAlg, castlingMove.kingToAlg).flatMap(Square.fromAlgebraic) + val clearSqs = (List(castlingMove.middleAlg, castlingMove.kingToAlg) ++ queensideBSquare(castlingMove.kingToAlg)) + .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"):