refactor(core): enhance castling logic to include rook movement and improve safety checks

This commit is contained in:
2026-04-05 19:17:23 +02:00
parent 2cd3ea35f6
commit 4cf39e3e97
7 changed files with 111 additions and 106 deletions
@@ -139,9 +139,9 @@ object DefaultRules extends RuleSet:
else
val moves = scala.collection.mutable.ListBuffer[Move]()
addCastleMove(context, moves, context.castlingRights.whiteKingSide,
"e1", "g1", "f1", MoveType.CastleKingside)
"e1", "g1", "f1", "h1", MoveType.CastleKingside)
addCastleMove(context, moves, context.castlingRights.whiteQueenSide,
"e1", "c1", "d1", MoveType.CastleQueenside)
"e1", "c1", "d1", "a1", MoveType.CastleQueenside)
moves.toList
private def blackCastles(context: GameContext, from: Square): List[Move] =
@@ -150,9 +150,9 @@ object DefaultRules extends RuleSet:
else
val moves = scala.collection.mutable.ListBuffer[Move]()
addCastleMove(context, moves, context.castlingRights.blackKingSide,
"e8", "g8", "f8", MoveType.CastleKingside)
"e8", "g8", "f8", "h8", MoveType.CastleKingside)
addCastleMove(context, moves, context.castlingRights.blackQueenSide,
"e8", "c8", "d8", MoveType.CastleQueenside)
"e8", "c8", "d8", "a8", MoveType.CastleQueenside)
moves.toList
private def addCastleMove(
@@ -162,6 +162,7 @@ object DefaultRules extends RuleSet:
kingFromAlg: String,
kingToAlg: String,
middleAlg: String,
rookFromAlg: String,
moveType: MoveType
): Unit =
if castlingRight then
@@ -169,8 +170,20 @@ object DefaultRules extends RuleSet:
if squaresEmpty(context.board, clearSqs) then
for
kf <- Square.fromAlgebraic(kingFromAlg)
km <- Square.fromAlgebraic(middleAlg)
kt <- Square.fromAlgebraic(kingToAlg)
do moves += Move(kf, kt, moveType)
rf <- Square.fromAlgebraic(rookFromAlg)
do
val color = context.turn
val kingPresent = context.board.pieceAt(kf).exists(p => p.color == color && p.pieceType == PieceType.King)
val rookPresent = context.board.pieceAt(rf).exists(p => p.color == color && p.pieceType == PieceType.Rook)
val squaresSafe =
!isAttackedBy(context.board, kf, color.opposite) &&
!isAttackedBy(context.board, km, color.opposite) &&
!isAttackedBy(context.board, kt, color.opposite)
if kingPresent && rookPresent && squaresSafe then
moves += Move(kf, kt, moveType)
private def squaresEmpty(board: Board, squares: List[Square]): Boolean =
squares.forall(sq => board.pieceAt(sq).isEmpty)
@@ -10,20 +10,20 @@ import org.scalatest.matchers.should.Matchers
class DefaultRulesTest extends AnyFunSuite with Matchers:
private val rules = DefaultRules()
private val rules = DefaultRules
// ── Pawn moves ──────────────────────────────────────────────────
test("pawn can move forward one square"):
val fen = "8/8/8/8/8/8/4P3/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val pawnMoves = moves.filter(m => m.from == Square(File.E, Rank.R2))
pawnMoves.exists(m => m.to == Square(File.E, Rank.R3)) shouldBe true
test("pawn can move forward two squares from starting position"):
val context = GameContext.initial
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val e2Moves = moves.filter(m => m.from == Square(File.E, Rank.R2))
e2Moves.exists(m => m.to == Square(File.E, Rank.R4)) shouldBe true
@@ -31,7 +31,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: white pawn e4, black pawn d5
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val captures = moves.filter(m => m.from == Square(File.E, Rank.R4) && m.moveType.isInstanceOf[MoveType.Normal])
captures.exists(m => m.to == Square(File.D, Rank.R5)) shouldBe true
@@ -39,7 +39,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: white pawn on e4
val fen = "8/8/8/8/4P3/8/8/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val pawnMoves = moves.filter(m => m.from == Square(File.E, Rank.R4))
pawnMoves.exists(m => m.to == Square(File.E, Rank.R3)) shouldBe false
@@ -49,16 +49,16 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: white king e1, black rook e8, white tries to move away
val fen = "4r3/8/8/8/8/8/8/4K3 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
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
test("king cannot move to square attacked by opponent"):
// FEN: white king e1, black rook on e2
val fen = "8/8/8/8/8/8/4r3/4K3 w - - 0 1"
// FEN: white king e1, black rook e2 defended by black king e3
val fen = "8/8/8/8/8/4k3/4r3/4K3 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
// King cannot move to e2 (occupied and attacked)
val kingMovesToE2 = moves.filter(m => m.from == Square(File.E, Rank.R1) && m.to == Square(File.E, Rank.R2))
@@ -69,7 +69,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
test("castling kingside is legal when king and rook unmoved and path clear"):
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w KQkq - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
castles.nonEmpty shouldBe true
@@ -77,7 +77,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
test("castling queenside is legal when king and rook unmoved and path clear"):
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val castles = moves.filter(m => m.moveType == MoveType.CastleQueenside)
castles.nonEmpty shouldBe true
@@ -86,16 +86,16 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: king and rook in position, but castling rights disabled
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
castles.isEmpty shouldBe true
test("castling is illegal when king is in check"):
// FEN: white king e1 in check from black rook e8
val fen = "4r3/8/8/8/8/8/PPPPPPPP/RNBQK2R w KQkq - 0 1"
val fen = "4r3/8/8/8/8/8/8/R3K2R w KQ - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val castles = moves.filter(m => m.moveType == MoveType.CastleKingside || m.moveType == MoveType.CastleQueenside)
castles.isEmpty shouldBe true
@@ -104,7 +104,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: white king e1, white rook h1, white bishop f1 (blocks f-file)
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBR1 w KQkq - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
castles.isEmpty shouldBe true
@@ -115,7 +115,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: white pawn e5, black pawn d5 (just double-pushed), en passant square d6
val fen = "k7/8/8/3pP3/8/8/8/7K w - d6 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val epMoves = moves.filter(m => m.moveType == MoveType.EnPassant)
epMoves.exists(m => m.to == Square(File.D, Rank.R6)) shouldBe true
@@ -124,7 +124,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: white pawn e5, black pawn d5, but no en passant square
val fen = "k7/8/8/3pP3/8/8/8/7K w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
val epMoves = moves.filter(m => m.moveType == MoveType.EnPassant)
epMoves.isEmpty shouldBe true
@@ -135,7 +135,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: white king e1, white bishop d2 (pinned), black rook a2
val fen = "8/8/8/8/8/8/r1B1K3/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
// Bishop on d2 is pinned by rook on a2; it cannot move
val bishopMoves = moves.filter(m => m.from == Square(File.C, Rank.R2))
@@ -146,7 +146,7 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// Actually, this is complex. Let's use: white king e1, black rook e8, white pawn blocks on e2
val fen = "4r3/8/8/8/8/8/4P3/4K3 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
val moves = rules.generateMoves(context)
val moves = rules.allLegalMoves(context)
// White is in check; only moves that block or move the king are legal
moves.nonEmpty shouldBe true