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
@@ -14,37 +14,30 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
// Fool's Mate position (after 2 moves: 1. f3 e5 2. g4 Qh5#) engine.processUserInput("f2f3")
// FEN after moves but before final checkmate move engine.processUserInput("e7e5")
EngineTestHelpers.loadFen(engine, "rnbqkbnr/pppp1ppp/8/4p2Q/6P1/5P2/PPPPP2P/RNB1KB1R b KQkq - 0 2") engine.processUserInput("g2g4")
observer.clear() observer.clear()
// Black queen to h5 is checkmate engine.processUserInput("d8h4")
engine.processUserInput("d8h4") // or the actual final move
val hasCheckmate = observer.hasEvent[CheckmateEvent] observer.hasEvent[CheckmateEvent] shouldBe true
if !hasCheckmate then
// If not quite checkmate, try a different position
val engine2 = EngineTestHelpers.makeEngine()
val observer2 = new EngineTestHelpers.MockObserver()
engine2.subscribe(observer2)
// Simplest checkmate: king in corner vs queen and king
EngineTestHelpers.loadFen(engine2, "k7/8/8/8/8/8/8/K6Q w - - 0 1")
observer2.clear()
engine2.processUserInput("h1h8")
observer2.hasEvent[CheckmateEvent] shouldBe true
test("checkmate with black winner"): test("checkmate with white winner"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
// FEN: Scholar's mate position (white king checkmated by black) engine.processUserInput("e2e4")
// After: 1. e4 e5 2. Bc4 Nc6 3. Qh5 Nf6 4. Qxf7# engine.processUserInput("e7e5")
EngineTestHelpers.loadFen(engine, "r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4") engine.processUserInput("f1c4")
engine.processUserInput("b8c6")
engine.processUserInput("d1h5")
engine.processUserInput("g8f6")
observer.clear() observer.clear()
// Black is already checkmated here; verify the event engine.processUserInput("h5f7")
val evt = observer.getEvent[CheckmateEvent] val evt = observer.getEvent[CheckmateEvent]
evt.isDefined shouldBe true evt.isDefined shouldBe true
evt.get.winner shouldBe Color.White evt.get.winner shouldBe Color.White
@@ -56,26 +49,46 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
// FEN: black king h8, white king f6, white queen g7 (stalemate) val moves = List(
EngineTestHelpers.loadFen(engine, "7k/6Q1/5K2/8/8/8/8/8 b - - 0 1") "e2e3", "a7a5",
"d1h5", "a8a6",
"h5a5", "h7h5",
"h2h4", "a6h6",
"a5c7", "f7f6",
"c7d7", "e8f7",
"d7b7", "d8d3",
"b7b8", "d3h7",
"b8c8", "f7g6"
)
moves.foreach(engine.processUserInput)
observer.clear() observer.clear()
// Black to move but has no legal moves and is not in check engine.processUserInput("c8e6")
// This should trigger stalemate detection on the next move attempt
val hasStalemate = observer.hasEvent[StalemateEvent] observer.hasEvent[StalemateEvent] shouldBe true
hasStalemate shouldBe true
test("stalemate when king has no moves and no pieces"): test("stalemate when king has no moves and no pieces"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
// FEN: king on a8, white king on b7, white queen on a7 (stalemate) val moves = List(
EngineTestHelpers.loadFen(engine, "k7/KQ6/8/8/8/8/8/8 b - - 0 1") "e2e3", "a7a5",
observer.clear() "d1h5", "a8a6",
"h5a5", "h7h5",
"h2h4", "a6h6",
"a5c7", "f7f6",
"c7d7", "e8f7",
"d7b7", "d8d3",
"b7b8", "d3h7",
"b8c8", "f7g6",
"c8e6"
)
val hasStalemate = observer.hasEvent[StalemateEvent] moves.foreach(engine.processUserInput)
hasStalemate shouldBe true
observer.hasEvent[StalemateEvent] shouldBe true
engine.turn shouldBe Color.White
// ── Check detection ──────────────────────────────────────────── // ── Check detection ────────────────────────────────────────────
@@ -84,11 +97,13 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
// FEN: white rook e4, black king e8, empty between engine.processUserInput("e2e4")
EngineTestHelpers.loadFen(engine, "4k3/8/8/8/4R3/8/8/8 w - - 0 1") engine.processUserInput("e7e5")
engine.processUserInput("f1c4")
engine.processUserInput("g8f6")
observer.clear() observer.clear()
engine.processUserInput("e4e8") // rook gives check engine.processUserInput("c4f7")
observer.hasEvent[CheckDetectedEvent] shouldBe true observer.hasEvent[CheckDetectedEvent] shouldBe true
@@ -97,35 +112,11 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
// FEN: white knight on d4, black king on f5 EngineTestHelpers.loadFen(engine, "8/4k3/8/8/3N4/8/8/4K3 w - - 0 1")
EngineTestHelpers.loadFen(engine, "8/8/5k2/8/3N4/8/8/8 w - - 0 1")
observer.clear() observer.clear()
engine.processUserInput("d4e6") // knight gives check to king on f5... actually no engine.processUserInput("d4f5")
// Let me use correct knight move: d4 to f5 gives check to king
engine.processUserInput("d4f3") // this won't give check, wrong position
// Better: set up a position where knight move does give check
val engine2 = EngineTestHelpers.makeEngine()
val observer2 = new EngineTestHelpers.MockObserver()
engine2.subscribe(observer2)
EngineTestHelpers.loadFen(engine2, "8/8/8/8/3N4/5k2/8/8 w - - 0 1")
observer2.clear()
engine2.processUserInput("d4f3") // actually d4 to f3 isn't a knight move, let me fix
// Use correct knight moves
val engine3 = EngineTestHelpers.makeEngine()
val observer3 = new EngineTestHelpers.MockObserver()
engine3.subscribe(observer3)
EngineTestHelpers.loadFen(engine3, "8/8/4k3/8/3N4/8/8/8 w - - 0 1")
observer3.clear()
// Knight from d4 can go to: c6, e6, f5, f3, e2, c2, b3, b5
// King is on e6, so Ne6 won't work (occupied), but Nf5 or other moves won't give check
// Let me just verify queen check works
observer.hasEvent[CheckDetectedEvent] shouldBe true observer.hasEvent[CheckDetectedEvent] shouldBe true
// ── Fifty-move rule ──────────────────────────────────────────── // ── Fifty-move rule ────────────────────────────────────────────
@@ -138,7 +129,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
EngineTestHelpers.loadFen(engine, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 99 1") EngineTestHelpers.loadFen(engine, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 99 1")
observer.clear() observer.clear()
engine.processUserInput("a2a3") engine.processUserInput("g1f3")
observer.hasEvent[FiftyMoveRuleAvailableEvent] shouldBe true observer.hasEvent[FiftyMoveRuleAvailableEvent] shouldBe true
@@ -154,9 +145,9 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
test("fifty-move rule clock resets on capture"): test("fifty-move rule clock resets on capture"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
// FEN: white pawn on e5, black pawn on d4, clock at 50 // FEN: white pawn on e5, black pawn on d6, clock at 50
EngineTestHelpers.loadFen(engine, "8/8/8/4P3/3p4/8/8/8 w - - 50 1") EngineTestHelpers.loadFen(engine, "4k3/8/3p4/4P3/8/8/8/4K3 w - - 50 1")
engine.processUserInput("e5d4") // capture engine.processUserInput("e5d6")
// Clock should reset to 0 after capture // Clock should reset to 0 after capture
engine.context.halfMoveClock shouldBe 0 engine.context.halfMoveClock shouldBe 0
@@ -154,8 +154,8 @@ class GameEngineScenarioTest extends AnyFunSuite with Matchers:
EngineTestHelpers.loadFen(engine, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 99 1") EngineTestHelpers.loadFen(engine, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 99 1")
observer.clear() observer.clear()
// Make a pawn move (non-capture, non-pawn-move would reset clock, but we're testing the event) // Use a legal non-pawn non-capture move so the clock increments to 100.
engine.processUserInput("a2a3") engine.processUserInput("g1f3")
observer.hasEvent[FiftyMoveRuleAvailableEvent] shouldBe true observer.hasEvent[FiftyMoveRuleAvailableEvent] shouldBe true
@@ -43,7 +43,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
EngineTestHelpers.loadFen(engine, "k7/8/8/8/8/8/8/R3K3 w q - 0 1") EngineTestHelpers.loadFen(engine, "k7/8/8/8/8/8/8/R3K3 w Q - 0 1")
observer.clear() observer.clear()
engine.processUserInput("e1c1") engine.processUserInput("e1c1")
@@ -105,7 +105,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("completePromotion to Queen executes move"): test("completePromotion to Queen executes move"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
EngineTestHelpers.loadFen(engine, "8/4P3/4k3/8/8/8/8/8 w - - 0 1") EngineTestHelpers.loadFen(engine, "8/4P3/8/8/8/8/k7/8 w - - 0 1")
engine.processUserInput("e7e8") engine.processUserInput("e7e8")
engine.completePromotion(PromotionPiece.Queen) engine.completePromotion(PromotionPiece.Queen)
@@ -115,7 +115,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("completePromotion to Rook executes move"): test("completePromotion to Rook executes move"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
EngineTestHelpers.loadFen(engine, "8/4P3/4k3/8/8/8/8/8 w - - 0 1") EngineTestHelpers.loadFen(engine, "8/4P3/8/8/8/8/k7/8 w - - 0 1")
engine.processUserInput("e7e8") engine.processUserInput("e7e8")
engine.completePromotion(PromotionPiece.Rook) engine.completePromotion(PromotionPiece.Rook)
@@ -125,7 +125,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("completePromotion to Bishop executes move"): test("completePromotion to Bishop executes move"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
EngineTestHelpers.loadFen(engine, "8/4P3/4k3/8/8/8/8/8 w - - 0 1") EngineTestHelpers.loadFen(engine, "8/4P3/8/8/8/8/k7/8 w - - 0 1")
engine.processUserInput("e7e8") engine.processUserInput("e7e8")
engine.completePromotion(PromotionPiece.Bishop) engine.completePromotion(PromotionPiece.Bishop)
@@ -135,7 +135,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("completePromotion to Knight executes move"): test("completePromotion to Knight executes move"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
EngineTestHelpers.loadFen(engine, "8/4P3/4k3/8/8/8/8/8 w - - 0 1") EngineTestHelpers.loadFen(engine, "8/4P3/8/8/8/8/k7/8 w - - 0 1")
engine.processUserInput("e7e8") engine.processUserInput("e7e8")
engine.completePromotion(PromotionPiece.Knight) engine.completePromotion(PromotionPiece.Knight)
@@ -147,8 +147,8 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
// FEN: white pawn e7, white bishop b4, black king d5 // FEN: white pawn e7, black king e6, white king e1
EngineTestHelpers.loadFen(engine, "8/4P3/8/3k4/1B6/8/8/8 w - - 0 1") EngineTestHelpers.loadFen(engine, "8/4P3/4k3/8/8/8/8/4K3 w - - 0 1")
observer.clear() observer.clear()
engine.processUserInput("e7e8") engine.processUserInput("e7e8")
@@ -156,16 +156,16 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
observer.hasEvent[CheckDetectedEvent] shouldBe true observer.hasEvent[CheckDetectedEvent] shouldBe true
test("promotion to Rook with checkmate emits CheckmateEvent"): test("promotion to Queen with checkmate emits CheckmateEvent"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver() val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer) engine.subscribe(observer)
// FEN: white pawn e7, white queen d6, black king f8 (trapped) // FEN: known promotion-mate pattern
EngineTestHelpers.loadFen(engine, "5k2/4P3/3Q4/8/8/8/8/8 w - - 0 1") EngineTestHelpers.loadFen(engine, "k7/7P/1K6/8/8/8/8/8 w - - 0 1")
observer.clear() observer.clear()
engine.processUserInput("e7e8") engine.processUserInput("h7h8")
engine.completePromotion(PromotionPiece.Queen) engine.completePromotion(PromotionPiece.Queen)
observer.hasEvent[CheckmateEvent] shouldBe true observer.hasEvent[CheckmateEvent] shouldBe true
@@ -193,7 +193,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
engine.processUserInput("e2e1") engine.processUserInput("e2e1")
engine.isPendingPromotion shouldBe true engine.isPendingPromotion shouldBe true
engine.processUserInput("q") // complete with Queen engine.completePromotion(PromotionPiece.Queen)
engine.isPendingPromotion shouldBe false engine.isPendingPromotion shouldBe false
engine.turn shouldBe Color.White engine.turn shouldBe Color.White
@@ -203,7 +203,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("pawn promotion with capture executes"): test("pawn promotion with capture executes"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
EngineTestHelpers.loadFen(engine, "1n6/4P3/4k3/8/8/8/8/8 w - - 0 1") EngineTestHelpers.loadFen(engine, "3n4/4P3/4k3/8/8/8/8/4K3 w - - 0 1")
engine.processUserInput("e7d8") engine.processUserInput("e7d8")
engine.isPendingPromotion shouldBe true engine.isPendingPromotion shouldBe true
@@ -50,7 +50,7 @@ class PgnParserTest extends AnyFunSuite with Matchers:
test("parse queenside castling O-O-O"): test("parse queenside castling O-O-O"):
val pgn = """[Event "Test"] val pgn = """[Event "Test"]
1. d4 d5 2. Nc3 Nc6 3. Bf4 Bf5 4. O-O-O""" 1. d4 d5 2. Nc3 Nc6 3. Bf4 Bf5 4. Qd2 Qd7 5. O-O-O"""
val game = PgnParser.parsePgn(pgn) val game = PgnParser.parsePgn(pgn)
game.isDefined shouldBe true game.isDefined shouldBe true
val lastMove = game.get.moves.last val lastMove = game.get.moves.last
@@ -71,7 +71,7 @@ class PgnParserTest extends AnyFunSuite with Matchers:
test("parse black queenside castling"): test("parse black queenside castling"):
val pgn = """[Event "Test"] val pgn = """[Event "Test"]
1. d4 d5 2. Nc3 Nc6 3. Bf4 Bf5 4. O-O-O O-O-O""" 1. d4 d5 2. Nc3 Nc6 3. Bf4 Bf5 4. Qd2 Qd7 5. O-O-O O-O-O"""
val game = PgnParser.parsePgn(pgn) val game = PgnParser.parsePgn(pgn)
game.isDefined shouldBe true game.isDefined shouldBe true
val lastMove = game.get.moves.last val lastMove = game.get.moves.last
+1
View File
@@ -40,6 +40,7 @@ dependencies {
implementation(project(":modules:api")) implementation(project(":modules:api"))
testImplementation(project(":modules:io"))
testImplementation(platform("org.junit:junit-bom:5.13.4")) testImplementation(platform("org.junit:junit-bom:5.13.4"))
testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}") testImplementation("org.scalatest:scalatest_3:${versions["SCALATEST"]!!}")
@@ -139,9 +139,9 @@ object DefaultRules extends RuleSet:
else else
val moves = scala.collection.mutable.ListBuffer[Move]() val moves = scala.collection.mutable.ListBuffer[Move]()
addCastleMove(context, moves, context.castlingRights.whiteKingSide, addCastleMove(context, moves, context.castlingRights.whiteKingSide,
"e1", "g1", "f1", MoveType.CastleKingside) "e1", "g1", "f1", "h1", MoveType.CastleKingside)
addCastleMove(context, moves, context.castlingRights.whiteQueenSide, addCastleMove(context, moves, context.castlingRights.whiteQueenSide,
"e1", "c1", "d1", MoveType.CastleQueenside) "e1", "c1", "d1", "a1", MoveType.CastleQueenside)
moves.toList moves.toList
private def blackCastles(context: GameContext, from: Square): List[Move] = private def blackCastles(context: GameContext, from: Square): List[Move] =
@@ -150,9 +150,9 @@ object DefaultRules extends RuleSet:
else else
val moves = scala.collection.mutable.ListBuffer[Move]() val moves = scala.collection.mutable.ListBuffer[Move]()
addCastleMove(context, moves, context.castlingRights.blackKingSide, addCastleMove(context, moves, context.castlingRights.blackKingSide,
"e8", "g8", "f8", MoveType.CastleKingside) "e8", "g8", "f8", "h8", MoveType.CastleKingside)
addCastleMove(context, moves, context.castlingRights.blackQueenSide, addCastleMove(context, moves, context.castlingRights.blackQueenSide,
"e8", "c8", "d8", MoveType.CastleQueenside) "e8", "c8", "d8", "a8", MoveType.CastleQueenside)
moves.toList moves.toList
private def addCastleMove( private def addCastleMove(
@@ -162,6 +162,7 @@ object DefaultRules extends RuleSet:
kingFromAlg: String, kingFromAlg: String,
kingToAlg: String, kingToAlg: String,
middleAlg: String, middleAlg: String,
rookFromAlg: String,
moveType: MoveType moveType: MoveType
): Unit = ): Unit =
if castlingRight then if castlingRight then
@@ -169,8 +170,20 @@ object DefaultRules extends RuleSet:
if squaresEmpty(context.board, clearSqs) then if squaresEmpty(context.board, clearSqs) then
for for
kf <- Square.fromAlgebraic(kingFromAlg) kf <- Square.fromAlgebraic(kingFromAlg)
km <- Square.fromAlgebraic(middleAlg)
kt <- Square.fromAlgebraic(kingToAlg) 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 = private def squaresEmpty(board: Board, squares: List[Square]): Boolean =
squares.forall(sq => board.pieceAt(sq).isEmpty) squares.forall(sq => board.pieceAt(sq).isEmpty)
@@ -10,20 +10,20 @@ import org.scalatest.matchers.should.Matchers
class DefaultRulesTest extends AnyFunSuite with Matchers: class DefaultRulesTest extends AnyFunSuite with Matchers:
private val rules = DefaultRules() private val rules = DefaultRules
// ── Pawn moves ────────────────────────────────────────────────── // ── Pawn moves ──────────────────────────────────────────────────
test("pawn can move forward one square"): test("pawn can move forward one square"):
val fen = "8/8/8/8/8/8/4P3/8 w - - 0 1" val fen = "8/8/8/8/8/8/4P3/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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)) val pawnMoves = moves.filter(m => m.from == Square(File.E, Rank.R2))
pawnMoves.exists(m => m.to == Square(File.E, Rank.R3)) shouldBe true pawnMoves.exists(m => m.to == Square(File.E, Rank.R3)) shouldBe true
test("pawn can move forward two squares from starting position"): test("pawn can move forward two squares from starting position"):
val context = GameContext.initial 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)) val e2Moves = moves.filter(m => m.from == Square(File.E, Rank.R2))
e2Moves.exists(m => m.to == Square(File.E, Rank.R4)) shouldBe true 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 // FEN: white pawn e4, black pawn d5
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1" val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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]) 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 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 // FEN: white pawn on e4
val fen = "8/8/8/8/4P3/8/8/8 w - - 0 1" val fen = "8/8/8/8/4P3/8/8/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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)) val pawnMoves = moves.filter(m => m.from == Square(File.E, Rank.R4))
pawnMoves.exists(m => m.to == Square(File.E, Rank.R3)) shouldBe false 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 // 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 fen = "4r3/8/8/8/8/8/8/4K3 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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 // 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.filter(m => m.from == Square(File.E, Rank.R1)).nonEmpty shouldBe true
test("king cannot move to square attacked by opponent"): test("king cannot move to square attacked by opponent"):
// FEN: white king e1, black rook on e2 // FEN: white king e1, black rook e2 defended by black king e3
val fen = "8/8/8/8/8/8/4r3/4K3 w - - 0 1" val fen = "8/8/8/8/8/4k3/4r3/4K3 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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) // 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)) 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"): 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 fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w KQkq - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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) val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
castles.nonEmpty shouldBe true 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"): 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 fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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) val castles = moves.filter(m => m.moveType == MoveType.CastleQueenside)
castles.nonEmpty shouldBe true castles.nonEmpty shouldBe true
@@ -86,16 +86,16 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
// FEN: king and rook in position, but castling rights disabled // FEN: king and rook in position, but castling rights disabled
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w - - 0 1" val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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) val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
castles.isEmpty shouldBe true castles.isEmpty shouldBe true
test("castling is illegal when king is in check"): test("castling is illegal when king is in check"):
// FEN: white king e1 in check from black rook e8 // 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 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) val castles = moves.filter(m => m.moveType == MoveType.CastleKingside || m.moveType == MoveType.CastleQueenside)
castles.isEmpty shouldBe true 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) // 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 fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBR1 w KQkq - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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) val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
castles.isEmpty shouldBe true 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 // 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 fen = "k7/8/8/3pP3/8/8/8/7K w - d6 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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) val epMoves = moves.filter(m => m.moveType == MoveType.EnPassant)
epMoves.exists(m => m.to == Square(File.D, Rank.R6)) shouldBe true 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 // 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 fen = "k7/8/8/3pP3/8/8/8/7K w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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) val epMoves = moves.filter(m => m.moveType == MoveType.EnPassant)
epMoves.isEmpty shouldBe true 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 // 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 fen = "8/8/8/8/8/8/r1B1K3/8 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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 // Bishop on d2 is pinned by rook on a2; it cannot move
val bishopMoves = moves.filter(m => m.from == Square(File.C, Rank.R2)) 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 // 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 fen = "4r3/8/8/8/8/8/4P3/4K3 w - - 0 1"
val context = FenParser.parseFen(fen).fold(_ => fail(), identity) 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 // White is in check; only moves that block or move the king are legal
moves.nonEmpty shouldBe true moves.nonEmpty shouldBe true