feat: improve PGN loading with error handling for promotions and add tests for en passant capture
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -221,17 +221,40 @@ class GameEngine(
|
||||
case Left(err) =>
|
||||
Left(err)
|
||||
case Right(game) =>
|
||||
val initialBoardBeforeLoad = currentBoard
|
||||
val initialHistoryBeforeLoad = currentHistory
|
||||
val initialTurnBeforeLoad = currentTurn
|
||||
|
||||
currentBoard = Board.initial
|
||||
currentHistory = GameHistory.empty
|
||||
currentTurn = Color.White
|
||||
pendingPromotion = None
|
||||
invoker.clear()
|
||||
game.moves.foreach { move =>
|
||||
handleParsedMove(move.from, move.to, s"${move.from}${move.to}")
|
||||
move.promotionPiece.foreach(completePromotion)
|
||||
|
||||
var error: Option[String] = None
|
||||
import scala.util.control.Breaks._
|
||||
breakable {
|
||||
game.moves.foreach { move =>
|
||||
handleParsedMove(move.from, move.to, s"${move.from}${move.to}")
|
||||
move.promotionPiece.foreach(completePromotion)
|
||||
|
||||
// If the move failed to execute properly, stop and report
|
||||
// (validatePgn should have caught this, but we're being safe)
|
||||
if pendingPromotion.isDefined && move.promotionPiece.isEmpty then
|
||||
error = Some(s"Promotion required for move ${move.from}${move.to}")
|
||||
break()
|
||||
}
|
||||
}
|
||||
notifyObservers(PgnLoadedEvent(currentBoard, currentHistory, currentTurn))
|
||||
Right(())
|
||||
|
||||
error match
|
||||
case Some(err) =>
|
||||
currentBoard = initialBoardBeforeLoad
|
||||
currentHistory = initialHistoryBeforeLoad
|
||||
currentTurn = initialTurnBeforeLoad
|
||||
Left(err)
|
||||
case None =>
|
||||
notifyObservers(PgnLoadedEvent(currentBoard, currentHistory, currentTurn))
|
||||
Right(())
|
||||
}
|
||||
|
||||
/** Load an arbitrary board position, clearing all history and undo/redo state. */
|
||||
|
||||
@@ -73,7 +73,7 @@ object MoveValidator:
|
||||
val fi = from.file.ordinal
|
||||
val ri = from.rank.ordinal
|
||||
val dir = if color == Color.White then 1 else -1
|
||||
val startRank = if color == Color.White then 1 else 6 // R2 = ordinal 1, R7 = ordinal 6
|
||||
val startRank = if color == Color.White then Rank.R2.ordinal else Rank.R7.ordinal
|
||||
|
||||
val oneStep = squareAt(fi, ri + dir)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package de.nowchess.chess.engine
|
||||
|
||||
import scala.collection.mutable
|
||||
import de.nowchess.api.board.{Board, Color}
|
||||
import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.chess.logic.GameHistory
|
||||
import de.nowchess.chess.observer.*
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
@@ -91,10 +91,22 @@ class GameEngineLoadPgnTest extends AnyFunSuite with Matchers:
|
||||
val pgn =
|
||||
"""[Event "T"]
|
||||
|
||||
1. a3 h6 2. a4 h5 3. a5 h4 4. a6 h3 5. a7 h2 6. a8=Q
|
||||
1. b4 h6 2. b5 h5 3. b6 h4 4. bxa7 h3 5. a8=Q
|
||||
"""
|
||||
engine.loadPgn(pgn) shouldBe Right(())
|
||||
engine.history.moves.length shouldBe 12
|
||||
engine.history.moves.length shouldBe 9
|
||||
|
||||
test("loadPgn: en passant capture in PGN is replayed correctly"):
|
||||
val engine = new GameEngine()
|
||||
val pgn =
|
||||
"""[Event "T"]
|
||||
|
||||
1. e4 e6 2. e5 d5 3. exd6
|
||||
"""
|
||||
engine.loadPgn(pgn) shouldBe Right(())
|
||||
engine.history.moves.length shouldBe 5
|
||||
engine.board.pieceAt(Square(File.D, Rank.R6)) shouldBe Some(Piece(Color.White, PieceType.Pawn))
|
||||
engine.board.pieceAt(Square(File.D, Rank.R5)) shouldBe None
|
||||
|
||||
// ── undo/redo notation events ─────────────────────────────────────────────
|
||||
|
||||
@@ -150,9 +162,8 @@ class GameEngineLoadPgnTest extends AnyFunSuite with Matchers:
|
||||
val engine = new GameEngine()
|
||||
val cap = new EventCapture()
|
||||
engine.subscribe(cap)
|
||||
// e4, e5, then Black plays d5, White pawn on e4 captures on d5
|
||||
// e4, then Black plays d5, White pawn on e4 captures on d5
|
||||
engine.processUserInput("e2e4")
|
||||
engine.processUserInput("e7e5")
|
||||
engine.processUserInput("d7d5")
|
||||
engine.processUserInput("e4d5") // white pawn captures black pawn
|
||||
engine.undo()
|
||||
|
||||
@@ -100,7 +100,7 @@ class PgnValidatorTest extends AnyFunSuite with Matchers:
|
||||
val pgn =
|
||||
"""[Event "Test"]
|
||||
|
||||
1. a4 h6 2. a5 h5 3. a6 h4 4. a7 h3 5. a8=Q h2
|
||||
1. b4 h6 2. b5 h5 3. b6 h4 4. bxa7 h3 5. a8=Q
|
||||
"""
|
||||
PgnParser.validatePgn(pgn) match
|
||||
case Right(game) =>
|
||||
@@ -108,6 +108,20 @@ class PgnValidatorTest extends AnyFunSuite with Matchers:
|
||||
game.moves.last.promotionPiece shouldBe Some(PromotionPiece.Queen)
|
||||
case Left(err) => fail(s"Expected Right but got Left($err)")
|
||||
|
||||
test("validatePgn: en passant capture is parsed correctly"):
|
||||
val pgn =
|
||||
"""[Event "Test"]
|
||||
|
||||
1. e4 e6 2. e5 d5 3. exd6
|
||||
"""
|
||||
PgnParser.validatePgn(pgn) match
|
||||
case Right(game) =>
|
||||
game.moves.length shouldBe 5
|
||||
val epMove = game.moves.last
|
||||
epMove.isCapture shouldBe true
|
||||
epMove.pieceType shouldBe PieceType.Pawn
|
||||
case Left(err) => fail(s"Expected Right but got Left($err)")
|
||||
|
||||
test("validatePgn: disambiguation with two rooks is accepted"):
|
||||
val pieces: Map[Square, Piece] = Map(
|
||||
Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook),
|
||||
|
||||
Reference in New Issue
Block a user