diff --git a/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala b/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala index 32f36b8..3d04c3d 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala @@ -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. */ diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala index f33d470..1d7b4e9 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala @@ -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) diff --git a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadPgnTest.scala b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadPgnTest.scala index 8a30b0a..42c106f 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadPgnTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineLoadPgnTest.scala @@ -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() diff --git a/modules/core/src/test/scala/de/nowchess/chess/notation/PgnValidatorTest.scala b/modules/core/src/test/scala/de/nowchess/chess/notation/PgnValidatorTest.scala index a4bbe86..3fc752f 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/notation/PgnValidatorTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/notation/PgnValidatorTest.scala @@ -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),