diff --git a/modules/core/src/main/scala/de/nowchess/chess/notation/PgnParser.scala b/modules/core/src/main/scala/de/nowchess/chess/notation/PgnParser.scala index a362daf..58bb2cd 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/notation/PgnParser.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/notation/PgnParser.scala @@ -1,6 +1,7 @@ package de.nowchess.chess.notation import de.nowchess.api.board.* +import de.nowchess.api.move.PromotionPiece import de.nowchess.chess.logic.{CastleSide, GameHistory, HistoryMove, GameRules, MoveValidator, withCastle} /** A parsed PGN game containing headers and the resolved move list. */ @@ -45,7 +46,17 @@ object PgnParser: case Some(move) => val newBoard = move.castleSide match case Some(side) => board.withCastle(color, side) - case None => board.withMove(move.from, move.to)._1 + case None => + val (boardAfterMove, _) = board.withMove(move.from, move.to) + move.promotionPiece match + case Some(pp) => + val pieceType = pp match + case PromotionPiece.Queen => PieceType.Queen + case PromotionPiece.Rook => PieceType.Rook + case PromotionPiece.Bishop => PieceType.Bishop + case PromotionPiece.Knight => PieceType.Knight + boardAfterMove.updated(move.to, Piece(color, pieceType)) + case None => boardAfterMove val newHistory = history.addMove(move) (newBoard, newHistory, color.opposite, acc :+ move) @@ -128,7 +139,8 @@ object PgnParser: if hint.isEmpty then byPiece else byPiece.filter(from => matchesHint(from, hint)) - disambiguated.headOption.map(from => HistoryMove(from, toSquare, None)) + val promotion = extractPromotion(notation) + disambiguated.headOption.map(from => HistoryMove(from, toSquare, None, promotion)) /** True if `sq` matches a disambiguation hint (file letter, rank digit, or both). */ private def matchesHint(sq: Square, hint: String): Boolean = @@ -139,6 +151,18 @@ object PgnParser: else true ) + /** Extract a promotion piece from a notation string containing =Q/=R/=B/=N. */ + private def extractPromotion(notation: String): Option[PromotionPiece] = + val promotionPattern = """=([QRBN])""".r + promotionPattern.findFirstMatchIn(notation).flatMap { m => + m.group(1) match + case "Q" => Some(PromotionPiece.Queen) + case "R" => Some(PromotionPiece.Rook) + case "B" => Some(PromotionPiece.Bishop) + case "N" => Some(PromotionPiece.Knight) + case _ => None + } + /** Convert a piece-letter character to a PieceType. */ private def charToPieceType(c: Char): Option[PieceType] = c match diff --git a/modules/core/src/test/scala/de/nowchess/chess/notation/PgnParserTest.scala b/modules/core/src/test/scala/de/nowchess/chess/notation/PgnParserTest.scala index 687d1b1..1957d54 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/notation/PgnParserTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/notation/PgnParserTest.scala @@ -1,7 +1,9 @@ package de.nowchess.chess.notation import de.nowchess.api.board.* +import de.nowchess.api.move.PromotionPiece import de.nowchess.chess.logic.{GameHistory, HistoryMove, CastleSide} +import de.nowchess.chess.notation.FenParser import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -332,3 +334,47 @@ class PgnParserTest extends AnyFunSuite with Matchers: result.isDefined shouldBe true result.get.to shouldBe Square(File.D, Rank.R1) } + + test("parseAlgebraicMove preserves promotion to Queen in HistoryMove") { + val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get + val result = PgnParser.parseAlgebraicMove("e7e8=Q", board, GameHistory.empty, Color.White) + result.isDefined should be (true) + result.get.promotionPiece should be (Some(PromotionPiece.Queen)) + result.get.to should be (Square(File.E, Rank.R8)) + } + + test("parseAlgebraicMove preserves promotion to Rook") { + val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get + val result = PgnParser.parseAlgebraicMove("e7e8=R", board, GameHistory.empty, Color.White) + result.get.promotionPiece should be (Some(PromotionPiece.Rook)) + } + + test("parseAlgebraicMove preserves promotion to Bishop") { + val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get + val result = PgnParser.parseAlgebraicMove("e7e8=B", board, GameHistory.empty, Color.White) + result.get.promotionPiece should be (Some(PromotionPiece.Bishop)) + } + + test("parseAlgebraicMove preserves promotion to Knight") { + val board = FenParser.parseBoard("8/4P3/4k3/8/8/8/8/8").get + val result = PgnParser.parseAlgebraicMove("e7e8=N", board, GameHistory.empty, Color.White) + result.get.promotionPiece should be (Some(PromotionPiece.Knight)) + } + + test("parsePgn applies promoted piece to board for subsequent moves") { + // Build a board with a white pawn on e7 plus the two kings + import de.nowchess.api.board.{Board, Square, File, Rank, Piece, Color, PieceType} + val pieces: Map[Square, Piece] = Map( + Square(File.E, Rank.R7) -> Piece(Color.White, PieceType.Pawn), + Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King), + Square(File.H, Rank.R1) -> Piece(Color.Black, PieceType.King) + ) + val board = Board(pieces) + val move = PgnParser.parseAlgebraicMove("e7e8=Q", board, GameHistory.empty, Color.White) + move.isDefined should be (true) + move.get.promotionPiece should be (Some(PromotionPiece.Queen)) + // After applying the promotion the square e8 should hold a White Queen + val (boardAfterPawnMove, _) = board.withMove(move.get.from, move.get.to) + val promotedBoard = boardAfterPawnMove.updated(move.get.to, Piece(Color.White, PieceType.Queen)) + promotedBoard.pieceAt(Square(File.E, Rank.R8)) should be (Some(Piece(Color.White, PieceType.Queen))) + }