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()
engine.subscribe(observer)
// Fool's Mate position (after 2 moves: 1. f3 e5 2. g4 Qh5#)
// FEN after moves but before final checkmate move
EngineTestHelpers.loadFen(engine, "rnbqkbnr/pppp1ppp/8/4p2Q/6P1/5P2/PPPPP2P/RNB1KB1R b KQkq - 0 2")
engine.processUserInput("f2f3")
engine.processUserInput("e7e5")
engine.processUserInput("g2g4")
observer.clear()
// Black queen to h5 is checkmate
engine.processUserInput("d8h4") // or the actual final move
engine.processUserInput("d8h4")
val hasCheckmate = observer.hasEvent[CheckmateEvent]
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
observer.hasEvent[CheckmateEvent] shouldBe true
test("checkmate with black winner"):
test("checkmate with white winner"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// FEN: Scholar's mate position (white king checkmated by black)
// After: 1. e4 e5 2. Bc4 Nc6 3. Qh5 Nf6 4. Qxf7#
EngineTestHelpers.loadFen(engine, "r1bqkb1r/pppp1Qpp/2n2n2/4p3/2B1P3/8/PPPP1PPP/RNB1K1NR b KQkq - 0 4")
engine.processUserInput("e2e4")
engine.processUserInput("e7e5")
engine.processUserInput("f1c4")
engine.processUserInput("b8c6")
engine.processUserInput("d1h5")
engine.processUserInput("g8f6")
observer.clear()
// Black is already checkmated here; verify the event
engine.processUserInput("h5f7")
val evt = observer.getEvent[CheckmateEvent]
evt.isDefined shouldBe true
evt.get.winner shouldBe Color.White
@@ -56,26 +49,46 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// FEN: black king h8, white king f6, white queen g7 (stalemate)
EngineTestHelpers.loadFen(engine, "7k/6Q1/5K2/8/8/8/8/8 b - - 0 1")
val moves = List(
"e2e3", "a7a5",
"d1h5", "a8a6",
"h5a5", "h7h5",
"h2h4", "a6h6",
"a5c7", "f7f6",
"c7d7", "e8f7",
"d7b7", "d8d3",
"b7b8", "d3h7",
"b8c8", "f7g6"
)
moves.foreach(engine.processUserInput)
observer.clear()
// Black to move but has no legal moves and is not in check
// This should trigger stalemate detection on the next move attempt
val hasStalemate = observer.hasEvent[StalemateEvent]
hasStalemate shouldBe true
engine.processUserInput("c8e6")
observer.hasEvent[StalemateEvent] shouldBe true
test("stalemate when king has no moves and no pieces"):
val engine = EngineTestHelpers.makeEngine()
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// FEN: king on a8, white king on b7, white queen on a7 (stalemate)
EngineTestHelpers.loadFen(engine, "k7/KQ6/8/8/8/8/8/8 b - - 0 1")
observer.clear()
val moves = List(
"e2e3", "a7a5",
"d1h5", "a8a6",
"h5a5", "h7h5",
"h2h4", "a6h6",
"a5c7", "f7f6",
"c7d7", "e8f7",
"d7b7", "d8d3",
"b7b8", "d3h7",
"b8c8", "f7g6",
"c8e6"
)
val hasStalemate = observer.hasEvent[StalemateEvent]
hasStalemate shouldBe true
moves.foreach(engine.processUserInput)
observer.hasEvent[StalemateEvent] shouldBe true
engine.turn shouldBe Color.White
// ── Check detection ────────────────────────────────────────────
@@ -84,11 +97,13 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// FEN: white rook e4, black king e8, empty between
EngineTestHelpers.loadFen(engine, "4k3/8/8/8/4R3/8/8/8 w - - 0 1")
engine.processUserInput("e2e4")
engine.processUserInput("e7e5")
engine.processUserInput("f1c4")
engine.processUserInput("g8f6")
observer.clear()
engine.processUserInput("e4e8") // rook gives check
engine.processUserInput("c4f7")
observer.hasEvent[CheckDetectedEvent] shouldBe true
@@ -97,35 +112,11 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// FEN: white knight on d4, black king on f5
EngineTestHelpers.loadFen(engine, "8/8/5k2/8/3N4/8/8/8 w - - 0 1")
EngineTestHelpers.loadFen(engine, "8/4k3/8/8/3N4/8/8/4K3 w - - 0 1")
observer.clear()
engine.processUserInput("d4e6") // knight gives check to king on f5... actually no
// Let me use correct knight move: d4 to f5 gives check to king
engine.processUserInput("d4f3") // this won't give check, wrong position
engine.processUserInput("d4f5")
// 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
// ── 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")
observer.clear()
engine.processUserInput("a2a3")
engine.processUserInput("g1f3")
observer.hasEvent[FiftyMoveRuleAvailableEvent] shouldBe true
@@ -154,9 +145,9 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
test("fifty-move rule clock resets on capture"):
val engine = EngineTestHelpers.makeEngine()
// FEN: white pawn on e5, black pawn on d4, clock at 50
EngineTestHelpers.loadFen(engine, "8/8/8/4P3/3p4/8/8/8 w - - 50 1")
engine.processUserInput("e5d4") // capture
// FEN: white pawn on e5, black pawn on d6, clock at 50
EngineTestHelpers.loadFen(engine, "4k3/8/3p4/4P3/8/8/8/4K3 w - - 50 1")
engine.processUserInput("e5d6")
// Clock should reset to 0 after capture
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")
observer.clear()
// Make a pawn move (non-capture, non-pawn-move would reset clock, but we're testing the event)
engine.processUserInput("a2a3")
// Use a legal non-pawn non-capture move so the clock increments to 100.
engine.processUserInput("g1f3")
observer.hasEvent[FiftyMoveRuleAvailableEvent] shouldBe true
@@ -43,7 +43,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver()
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()
engine.processUserInput("e1c1")
@@ -105,7 +105,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("completePromotion to Queen executes move"):
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.completePromotion(PromotionPiece.Queen)
@@ -115,7 +115,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("completePromotion to Rook executes move"):
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.completePromotion(PromotionPiece.Rook)
@@ -125,7 +125,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("completePromotion to Bishop executes move"):
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.completePromotion(PromotionPiece.Bishop)
@@ -135,7 +135,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("completePromotion to Knight executes move"):
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.completePromotion(PromotionPiece.Knight)
@@ -147,8 +147,8 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
val observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// FEN: white pawn e7, white bishop b4, black king d5
EngineTestHelpers.loadFen(engine, "8/4P3/8/3k4/1B6/8/8/8 w - - 0 1")
// FEN: white pawn e7, black king e6, white king e1
EngineTestHelpers.loadFen(engine, "8/4P3/4k3/8/8/8/8/4K3 w - - 0 1")
observer.clear()
engine.processUserInput("e7e8")
@@ -156,16 +156,16 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
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 observer = new EngineTestHelpers.MockObserver()
engine.subscribe(observer)
// FEN: white pawn e7, white queen d6, black king f8 (trapped)
EngineTestHelpers.loadFen(engine, "5k2/4P3/3Q4/8/8/8/8/8 w - - 0 1")
// FEN: known promotion-mate pattern
EngineTestHelpers.loadFen(engine, "k7/7P/1K6/8/8/8/8/8 w - - 0 1")
observer.clear()
engine.processUserInput("e7e8")
engine.processUserInput("h7h8")
engine.completePromotion(PromotionPiece.Queen)
observer.hasEvent[CheckmateEvent] shouldBe true
@@ -193,7 +193,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
engine.processUserInput("e2e1")
engine.isPendingPromotion shouldBe true
engine.processUserInput("q") // complete with Queen
engine.completePromotion(PromotionPiece.Queen)
engine.isPendingPromotion shouldBe false
engine.turn shouldBe Color.White
@@ -203,7 +203,7 @@ class GameEngineSpecialMovesTest extends AnyFunSuite with Matchers:
test("pawn promotion with capture executes"):
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.isPendingPromotion shouldBe true