feat: add PGN import support for pawn promotion notation (=Q/=R/=B/=N)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user