feat: add PGN exporter for game notation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package de.nowchess.chess.notation
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.chess.logic.{CastleSide, GameHistory, HistoryMove}
|
||||
|
||||
object PgnExporter:
|
||||
|
||||
/** Export a game with headers and history to PGN format. */
|
||||
def exportGame(headers: Map[String, String], history: GameHistory): String =
|
||||
val headerLines = headers.map { case (key, value) =>
|
||||
s"""[$key "$value"]"""
|
||||
}.mkString("\n")
|
||||
|
||||
val moveText = if history.moves.isEmpty then ""
|
||||
else
|
||||
val groupedMoves = history.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("")
|
||||
val blackMoveStr = movePairs.find(_._2 % 2 == 1).map(p => moveToAlgebraic(p._1)).getOrElse("")
|
||||
if blackMoveStr.isEmpty then s"$moveNum. $whiteMoveStr"
|
||||
else s"$moveNum. $whiteMoveStr $blackMoveStr"
|
||||
|
||||
moveLines.mkString(" ") + " *"
|
||||
|
||||
if headerLines.isEmpty then moveText
|
||||
else if moveText.isEmpty then headerLines
|
||||
else s"$headerLines\n\n$moveText"
|
||||
|
||||
/** Convert a HistoryMove to algebraic notation. */
|
||||
private def moveToAlgebraic(move: HistoryMove): String =
|
||||
move.castleSide match
|
||||
case Some(CastleSide.Kingside) => "O-O"
|
||||
case Some(CastleSide.Queenside) => "O-O-O"
|
||||
case None => s"${move.from}${move.to}"
|
||||
@@ -0,0 +1,48 @@
|
||||
package de.nowchess.chess.notation
|
||||
|
||||
import de.nowchess.api.board.*
|
||||
import de.nowchess.chess.logic.{GameHistory, HistoryMove, CastleSide}
|
||||
import org.scalatest.funsuite.AnyFunSuite
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
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)
|
||||
|
||||
pgn.contains("[Event \"Test\"]") shouldBe true
|
||||
pgn.contains("[White \"A\"]") shouldBe true
|
||||
pgn.contains("[Black \"B\"]") shouldBe true
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
pgn.contains("1. e2e4") 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(CastleSide.Kingside)))
|
||||
val pgn = PgnExporter.exportGame(headers, history)
|
||||
|
||||
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))
|
||||
val pgn = PgnExporter.exportGame(headers, history)
|
||||
|
||||
pgn.contains("1. e2e4 c7c5") shouldBe true
|
||||
pgn.contains("2. g1f3") shouldBe true
|
||||
}
|
||||
Reference in New Issue
Block a user