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) =>
|
case Left(err) =>
|
||||||
Left(err)
|
Left(err)
|
||||||
case Right(game) =>
|
case Right(game) =>
|
||||||
|
val initialBoardBeforeLoad = currentBoard
|
||||||
|
val initialHistoryBeforeLoad = currentHistory
|
||||||
|
val initialTurnBeforeLoad = currentTurn
|
||||||
|
|
||||||
currentBoard = Board.initial
|
currentBoard = Board.initial
|
||||||
currentHistory = GameHistory.empty
|
currentHistory = GameHistory.empty
|
||||||
currentTurn = Color.White
|
currentTurn = Color.White
|
||||||
pendingPromotion = None
|
pendingPromotion = None
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
game.moves.foreach { move =>
|
|
||||||
handleParsedMove(move.from, move.to, s"${move.from}${move.to}")
|
var error: Option[String] = None
|
||||||
move.promotionPiece.foreach(completePromotion)
|
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. */
|
/** Load an arbitrary board position, clearing all history and undo/redo state. */
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ object MoveValidator:
|
|||||||
val fi = from.file.ordinal
|
val fi = from.file.ordinal
|
||||||
val ri = from.rank.ordinal
|
val ri = from.rank.ordinal
|
||||||
val dir = if color == Color.White then 1 else -1
|
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)
|
val oneStep = squareAt(fi, ri + dir)
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package de.nowchess.chess.engine
|
package de.nowchess.chess.engine
|
||||||
|
|
||||||
import scala.collection.mutable
|
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.logic.GameHistory
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
@@ -91,10 +91,22 @@ class GameEngineLoadPgnTest extends AnyFunSuite with Matchers:
|
|||||||
val pgn =
|
val pgn =
|
||||||
"""[Event "T"]
|
"""[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.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 ─────────────────────────────────────────────
|
// ── undo/redo notation events ─────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -150,9 +162,8 @@ class GameEngineLoadPgnTest extends AnyFunSuite with Matchers:
|
|||||||
val engine = new GameEngine()
|
val engine = new GameEngine()
|
||||||
val cap = new EventCapture()
|
val cap = new EventCapture()
|
||||||
engine.subscribe(cap)
|
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("e2e4")
|
||||||
engine.processUserInput("e7e5")
|
|
||||||
engine.processUserInput("d7d5")
|
engine.processUserInput("d7d5")
|
||||||
engine.processUserInput("e4d5") // white pawn captures black pawn
|
engine.processUserInput("e4d5") // white pawn captures black pawn
|
||||||
engine.undo()
|
engine.undo()
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class PgnValidatorTest extends AnyFunSuite with Matchers:
|
|||||||
val pgn =
|
val pgn =
|
||||||
"""[Event "Test"]
|
"""[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
|
PgnParser.validatePgn(pgn) match
|
||||||
case Right(game) =>
|
case Right(game) =>
|
||||||
@@ -108,6 +108,20 @@ class PgnValidatorTest extends AnyFunSuite with Matchers:
|
|||||||
game.moves.last.promotionPiece shouldBe Some(PromotionPiece.Queen)
|
game.moves.last.promotionPiece shouldBe Some(PromotionPiece.Queen)
|
||||||
case Left(err) => fail(s"Expected Right but got Left($err)")
|
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"):
|
test("validatePgn: disambiguation with two rooks is accepted"):
|
||||||
val pieces: Map[Square, Piece] = Map(
|
val pieces: Map[Square, Piece] = Map(
|
||||||
Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook),
|
Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook),
|
||||||
|
|||||||
Reference in New Issue
Block a user