refactor(core): replace GameHistory with HistoryMove in PGN export logic

This commit is contained in:
2026-04-05 17:08:54 +02:00
parent a1b7cc7f4a
commit 432385c7b0
13 changed files with 110 additions and 218 deletions
@@ -2,7 +2,7 @@ package de.nowchess.io.pgn
import de.nowchess.api.board.*
import de.nowchess.api.move.{PromotionPiece, MoveType}
import de.nowchess.api.game.{GameHistory, HistoryMove, GameContext}
import de.nowchess.api.game.{GameContext, HistoryMove}
import de.nowchess.io.GameContextExport
import de.nowchess.rules.sets.DefaultRules
@@ -42,18 +42,17 @@ object PgnExporter extends GameContextExport:
historyMoves += HistoryMove(move.from, move.to, castleSide, promotionPiece, pieceType, isCapture)
ctx = DefaultRules.applyMove(ctx, move)
val history = GameHistory(historyMoves.toList, context.halfMoveClock)
exportGame(headers, history)
exportGame(headers, historyMoves.toList)
/** Export a game with headers and history to PGN format. */
def exportGame(headers: Map[String, String], history: GameHistory): String =
/** Export a game with headers and history moves to PGN format. */
def exportGame(headers: Map[String, String], moves: List[HistoryMove]): String =
val headerLines = headers.map { case (key, value) =>
s"""[$key "$value"]"""
}.mkString("\n")
val moveText = if history.moves.isEmpty then ""
val moveText = if moves.isEmpty then ""
else
val groupedMoves = history.moves.zipWithIndex.groupBy(_._2 / 2)
val groupedMoves = moves.zipWithIndex.groupBy(_._2 / 2)
val moveLines = for (moveNumber, movePairs) <- groupedMoves.toList.sortBy(_._1) yield
val moveNum = moveNumber + 1
val whiteMoveStr = movePairs.find(_._2 % 2 == 0).map(p => moveToAlgebraic(p._1)).getOrElse("")
@@ -2,8 +2,7 @@ package de.nowchess.io.pgn
import de.nowchess.api.board.{PieceType, *}
import de.nowchess.api.move.{PromotionPiece, Move, MoveType}
import de.nowchess.api.game.{GameHistory, HistoryMove, GameContext}
import de.nowchess.io.pgn.PgnParser
import de.nowchess.api.game.{GameContext, HistoryMove}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@@ -11,8 +10,7 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
test("export empty game") {
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B")
val history = GameHistory.empty
val pgn = PgnExporter.exportGame(headers, history)
val pgn = PgnExporter.exportGame(headers, List.empty)
pgn.contains("[Event \"Test\"]") shouldBe true
pgn.contains("[White \"A\"]") shouldBe true
@@ -21,97 +19,87 @@ class PgnExporterTest extends AnyFunSuite with Matchers:
test("export single move") {
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B")
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
val pgn = PgnExporter.exportGame(headers, history)
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
val pgn = PgnExporter.exportGame(headers, moves)
pgn.contains("1. e4") shouldBe true
}
test("export castling") {
val headers = Map("Event" -> "Test")
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R1), Square(File.G, Rank.R1), Some("Kingside")))
val pgn = PgnExporter.exportGame(headers, history)
val moves = List(HistoryMove(Square(File.E, Rank.R1), Square(File.G, Rank.R1), Some("Kingside")))
val pgn = PgnExporter.exportGame(headers, moves)
pgn.contains("O-O") shouldBe true
}
test("export game sequence") {
val headers = Map("Event" -> "Test", "White" -> "A", "Black" -> "B", "Result" -> "1-0")
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
.addMove(HistoryMove(Square(File.C, Rank.R7), Square(File.C, Rank.R5), None))
.addMove(HistoryMove(Square(File.G, Rank.R1), Square(File.F, Rank.R3), None, pieceType = PieceType.Knight))
val pgn = PgnExporter.exportGame(headers, history)
val moves = List(
HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None),
HistoryMove(Square(File.C, Rank.R7), Square(File.C, Rank.R5), None),
HistoryMove(Square(File.G, Rank.R1), Square(File.F, Rank.R3), None, pieceType = PieceType.Knight)
)
val pgn = PgnExporter.exportGame(headers, moves)
pgn.contains("1. e4 c5") shouldBe true
pgn.contains("2. Nf3") shouldBe true
}
test("export game with no headers returns only move text") {
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
val pgn = PgnExporter.exportGame(Map.empty, history)
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
val pgn = PgnExporter.exportGame(Map.empty, moves)
pgn shouldBe "1. e4 *"
}
test("export queenside castling") {
val headers = Map("Event" -> "Test")
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R1), Square(File.C, Rank.R1), Some("Queenside")))
val pgn = PgnExporter.exportGame(headers, history)
val moves = List(HistoryMove(Square(File.E, Rank.R1), Square(File.C, Rank.R1), Some("Queenside")))
val pgn = PgnExporter.exportGame(headers, moves)
pgn.contains("O-O-O") shouldBe true
}
test("exportGame encodes promotion to Queen as =Q suffix") {
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Queen)))
val pgn = PgnExporter.exportGame(Map.empty, history)
val moves = List(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Queen)))
val pgn = PgnExporter.exportGame(Map.empty, moves)
pgn should include ("e8=Q")
}
test("exportGame encodes promotion to Rook as =R suffix") {
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Rook)))
val pgn = PgnExporter.exportGame(Map.empty, history)
val moves = List(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Rook)))
val pgn = PgnExporter.exportGame(Map.empty, moves)
pgn should include ("e8=R")
}
test("exportGame encodes promotion to Bishop as =B suffix") {
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Bishop)))
val pgn = PgnExporter.exportGame(Map.empty, history)
val moves = List(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Bishop)))
val pgn = PgnExporter.exportGame(Map.empty, moves)
pgn should include ("e8=B")
}
test("exportGame encodes promotion to Knight as =N suffix") {
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Knight)))
val pgn = PgnExporter.exportGame(Map.empty, history)
val moves = List(HistoryMove(Square(File.E, Rank.R7), Square(File.E, Rank.R8), None, Some(PromotionPiece.Knight)))
val pgn = PgnExporter.exportGame(Map.empty, moves)
pgn should include ("e8=N")
}
test("exportGame does not add suffix for normal moves") {
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None, None))
val pgn = PgnExporter.exportGame(Map.empty, history)
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None, None))
val pgn = PgnExporter.exportGame(Map.empty, moves)
pgn should include ("e4")
pgn should not include ("=")
pgn should not include "="
}
test("exportGame uses Result header as termination marker"):
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
val pgn = PgnExporter.exportGame(Map("Result" -> "1/2-1/2"), history)
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
val pgn = PgnExporter.exportGame(Map("Result" -> "1/2-1/2"), moves)
pgn should endWith("1/2-1/2")
test("exportGame with no Result header still uses * as default"):
val history = GameHistory()
.addMove(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
val pgn = PgnExporter.exportGame(Map.empty, history)
val moves = List(HistoryMove(Square(File.E, Rank.R2), Square(File.E, Rank.R4), None))
val pgn = PgnExporter.exportGame(Map.empty, moves)
pgn shouldBe "1. e4 *"
test("exportGameContext: moves are preserved in output") {
@@ -2,7 +2,6 @@ package de.nowchess.io.pgn
import de.nowchess.api.board.*
import de.nowchess.api.move.PromotionPiece
import de.nowchess.api.game.{GameHistory, HistoryMove}
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers