refactor(core): streamline FEN and PGN export/import methods in ChessBoardView

This commit is contained in:
2026-04-05 17:01:34 +02:00
parent 17fa13c82a
commit a1b7cc7f4a
4 changed files with 28 additions and 70 deletions
-18
View File
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CopilotDiffPersistence">
<option name="pendingDiffs">
<map>
<entry key="$PROJECT_DIR$/modules/ui/build.gradle.kts">
<value>
<PendingDiffInfo>
<option name="filePath" value="$PROJECT_DIR$/modules/ui/build.gradle.kts" />
<option name="originalContent" value="plugins {&#10; id(&quot;scala&quot;)&#10; id(&quot;org.scoverage&quot;)&#10; application&#10;}&#10;&#10;group = &quot;de.nowchess&quot;&#10;version = &quot;1.0-SNAPSHOT&quot;&#10;&#10;@Suppress(&quot;UNCHECKED_CAST&quot;)&#10;val versions = rootProject.extra[&quot;VERSIONS&quot;] as Map&lt;String, String&gt;&#10;&#10;repositories {&#10; mavenCentral()&#10;}&#10;&#10;scala {&#10; scalaVersion = versions[&quot;SCALA3&quot;]!!&#10;}&#10;&#10;scoverage {&#10; scoverageVersion.set(versions[&quot;SCOVERAGE&quot;]!!)&#10; excludedPackages.set(listOf(&#10; &quot;de.nowchess.ui.gui&quot;&#10; ))&#10;}&#10;&#10;application {&#10; mainClass.set(&quot;de.nowchess.ui.Main&quot;)&#10;}&#10;&#10;tasks.withType&lt;ScalaCompile&gt; {&#10; scalaCompileOptions.additionalParameters = listOf(&quot;-encoding&quot;, &quot;UTF-8&quot;)&#10;}&#10;&#10;tasks.named&lt;JavaExec&gt;(&quot;run&quot;) {&#10; jvmArgs(&quot;-Dfile.encoding=UTF-8&quot;, &quot;-Dstdout.encoding=UTF-8&quot;, &quot;-Dstderr.encoding=UTF-8&quot;)&#10; standardInput = System.`in`&#10;}&#10;&#10;dependencies {&#10;&#10; implementation(&quot;org.scala-lang:scala3-compiler_3&quot;) {&#10; version {&#10; strictly(versions[&quot;SCALA3&quot;]!!)&#10; }&#10; }&#10; implementation(&quot;org.scala-lang:scala3-library_3&quot;) {&#10; version {&#10; strictly(versions[&quot;SCALA3&quot;]!!)&#10; }&#10; }&#10;&#10; implementation(project(&quot;:modules:core&quot;))&#10; implementation(project(&quot;:modules:rule&quot;))&#10; implementation(project(&quot;:modules:api&quot;))&#10; implementation(project(&quot;:modules:io&quot;))&#10;&#10; // ScalaFX dependencies&#10; implementation(&quot;org.scalafx:scalafx_3:${versions[&quot;SCALAFX&quot;]!!}&quot;)&#10; &#10; // JavaFX dependencies for the current platform&#10; val javaFXVersion = versions[&quot;JAVAFX&quot;]!!&#10; val osName = System.getProperty(&quot;os.name&quot;).lowercase()&#10; val platform = when {&#10; osName.contains(&quot;win&quot;) -&gt; &quot;win&quot;&#10; osName.contains(&quot;mac&quot;) -&gt; &quot;mac&quot;&#10; osName.contains(&quot;linux&quot;) -&gt; &quot;linux&quot;&#10; else -&gt; &quot;linux&quot;&#10; }&#10; &#10; listOf(&quot;base&quot;, &quot;controls&quot;, &quot;graphics&quot;, &quot;media&quot;).forEach { module -&gt;&#10; implementation(&quot;org.openjfx:javafx-$module:$javaFXVersion:$platform&quot;)&#10; }&#10;&#10; testImplementation(platform(&quot;org.junit:junit-bom:${versions[&quot;JUNIT_BOM&quot;]!!}&quot;))&#10; testImplementation(&quot;org.junit.jupiter:junit-jupiter&quot;)&#10; testImplementation(&quot;org.scalatest:scalatest_3:${versions[&quot;SCALATEST&quot;]!!}&quot;)&#10; testImplementation(&quot;co.helmethair:scalatest-junit-runner:${versions[&quot;SCALATEST_JUNIT&quot;]!!}&quot;)&#10;&#10; testRuntimeOnly(&quot;org.junit.platform:junit-platform-launcher&quot;)&#10;}&#10;&#10;tasks.test {&#10; useJUnitPlatform {&#10; includeEngines(&quot;scalatest&quot;)&#10; testLogging {&#10; events(&quot;skipped&quot;, &quot;failed&quot;)&#10; }&#10; }&#10; finalizedBy(tasks.reportScoverage)&#10;}&#10;tasks.reportScoverage {&#10; dependsOn(tasks.test)&#10;}&#10;" />
<option name="updatedContent" value="import org.gradle.api.file.DuplicatesStrategy&#10;import org.gradle.jvm.tasks.Jar&#10;&#10;plugins {&#10; id(&quot;scala&quot;)&#10; id(&quot;org.scoverage&quot;)&#10; application&#10;}&#10;&#10;group = &quot;de.nowchess&quot;&#10;version = &quot;1.0-SNAPSHOT&quot;&#10;&#10;@Suppress(&quot;UNCHECKED_CAST&quot;)&#10;val versions = rootProject.extra[&quot;VERSIONS&quot;] as Map&lt;String, String&gt;&#10;&#10;repositories {&#10; mavenCentral()&#10;}&#10;&#10;scala {&#10; scalaVersion = versions[&quot;SCALA3&quot;]!!&#10;}&#10;&#10;scoverage {&#10; scoverageVersion.set(versions[&quot;SCOVERAGE&quot;]!!)&#10; excludedPackages.set(listOf(&#10; &quot;de.nowchess.ui.gui&quot;&#10; ))&#10;}&#10;&#10;application {&#10; mainClass.set(&quot;de.nowchess.ui.Main&quot;)&#10;}&#10;&#10;tasks.withType&lt;ScalaCompile&gt; {&#10; scalaCompileOptions.additionalParameters = listOf(&quot;-encoding&quot;, &quot;UTF-8&quot;)&#10;}&#10;&#10;tasks.named&lt;JavaExec&gt;(&quot;run&quot;) {&#10; jvmArgs(&quot;-Dfile.encoding=UTF-8&quot;, &quot;-Dstdout.encoding=UTF-8&quot;, &quot;-Dstderr.encoding=UTF-8&quot;)&#10; standardInput = System.`in`&#10;}&#10;&#10;tasks.named&lt;Jar&gt;(&quot;jar&quot;) {&#10; duplicatesStrategy = DuplicatesStrategy.EXCLUDE&#10;}&#10;&#10;dependencies {&#10;&#10; implementation(&quot;org.scala-lang:scala3-compiler_3&quot;) {&#10; version {&#10; strictly(versions[&quot;SCALA3&quot;]!!)&#10; }&#10; }&#10; implementation(&quot;org.scala-lang:scala3-library_3&quot;) {&#10; version {&#10; strictly(versions[&quot;SCALA3&quot;]!!)&#10; }&#10; }&#10;&#10; implementation(project(&quot;:modules:core&quot;))&#10; implementation(project(&quot;:modules:rule&quot;))&#10; implementation(project(&quot;:modules:api&quot;))&#10; implementation(project(&quot;:modules:io&quot;))&#10;&#10; // ScalaFX dependencies&#10; implementation(&quot;org.scalafx:scalafx_3:${versions[&quot;SCALAFX&quot;]!!}&quot;)&#10; &#10; // JavaFX dependencies for the current platform&#10; val javaFXVersion = versions[&quot;JAVAFX&quot;]!!&#10; val osName = System.getProperty(&quot;os.name&quot;).lowercase()&#10; val platform = when {&#10; osName.contains(&quot;win&quot;) -&gt; &quot;win&quot;&#10; osName.contains(&quot;mac&quot;) -&gt; &quot;mac&quot;&#10; osName.contains(&quot;linux&quot;) -&gt; &quot;linux&quot;&#10; else -&gt; &quot;linux&quot;&#10; }&#10; &#10; listOf(&quot;base&quot;, &quot;controls&quot;, &quot;graphics&quot;, &quot;media&quot;).forEach { module -&gt;&#10; implementation(&quot;org.openjfx:javafx-$module:$javaFXVersion:$platform&quot;)&#10; }&#10;&#10; testImplementation(platform(&quot;org.junit:junit-bom:${versions[&quot;JUNIT_BOM&quot;]!!}&quot;))&#10; testImplementation(&quot;org.junit.jupiter:junit-jupiter&quot;)&#10; testImplementation(&quot;org.scalatest:scalatest_3:${versions[&quot;SCALATEST&quot;]!!}&quot;)&#10; testImplementation(&quot;co.helmethair:scalatest-junit-runner:${versions[&quot;SCALATEST_JUNIT&quot;]!!}&quot;)&#10;&#10; testRuntimeOnly(&quot;org.junit.platform:junit-platform-launcher&quot;)&#10;}&#10;&#10;tasks.test {&#10; useJUnitPlatform {&#10; includeEngines(&quot;scalatest&quot;)&#10; testLogging {&#10; events(&quot;skipped&quot;, &quot;failed&quot;)&#10; }&#10; }&#10; finalizedBy(tasks.reportScoverage)&#10;}&#10;tasks.reportScoverage {&#10; dependsOn(tasks.test)&#10;}&#10;" />
</PendingDiffInfo>
</value>
</entry>
</map>
</option>
</component>
</project>
@@ -217,8 +217,12 @@ class GameEngine(
if ruleSet.isCheckmate(currentContext) then if ruleSet.isCheckmate(currentContext) then
val winner = currentContext.turn.opposite val winner = currentContext.turn.opposite
notifyObservers(CheckmateEvent(currentContext, winner)) notifyObservers(CheckmateEvent(currentContext, winner))
invoker.clear()
currentContext = GameContext.initial
else if ruleSet.isStalemate(currentContext) then else if ruleSet.isStalemate(currentContext) then
notifyObservers(StalemateEvent(currentContext)) notifyObservers(StalemateEvent(currentContext))
invoker.clear()
currentContext = GameContext.initial
else if ruleSet.isCheck(currentContext) then else if ruleSet.isCheck(currentContext) then
notifyObservers(CheckDetectedEvent(currentContext)) notifyObservers(CheckDetectedEvent(currentContext))
@@ -73,7 +73,7 @@ class GameEngineNotationTest extends AnyFunSuite with Matchers:
// Verify the captured pawn was found (computeCaptured EnPassant branch) // Verify the captured pawn was found (computeCaptured EnPassant branch)
val moveEvt = events.collect { case e: MoveExecutedEvent => e }.head val moveEvt = events.collect { case e: MoveExecutedEvent => e }.head
moveEvt.capturedPiece shouldBe defined moveEvt.capturedPiece shouldBe defined
moveEvt.capturedPiece.get should include ("black") moveEvt.capturedPiece.get should include ("Black")
events.clear() events.clear()
engine.undo() engine.undo()
@@ -5,16 +5,19 @@ import scalafx.Includes.*
import scalafx.application.Platform import scalafx.application.Platform
import scalafx.geometry.{Insets, Pos} import scalafx.geometry.{Insets, Pos}
import scalafx.scene.control.{Button, ButtonType, ChoiceDialog, Label} import scalafx.scene.control.{Button, ButtonType, ChoiceDialog, Label}
import scalafx.scene.layout.{BorderPane, GridPane, HBox, VBox, StackPane} import scalafx.scene.layout.{BorderPane, GridPane, HBox, StackPane, VBox}
import scalafx.scene.paint.Color as FXColor import scalafx.scene.paint.Color as FXColor
import scalafx.scene.shape.Rectangle import scalafx.scene.shape.Rectangle
import scalafx.scene.text.{Font, Text} import scalafx.scene.text.{Font, Text}
import scalafx.stage.Stage import scalafx.stage.Stage
import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square, File, Rank} import de.nowchess.api.board.{Board, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.game.{GameHistory, HistoryMove} import de.nowchess.api.game.{GameHistory, HistoryMove}
import de.nowchess.api.move.PromotionPiece import de.nowchess.api.move.PromotionPiece
import de.nowchess.chess.command.{MoveCommand, MoveResult} import de.nowchess.chess.command.{MoveCommand, MoveResult}
import de.nowchess.chess.engine.GameEngine import de.nowchess.chess.engine.GameEngine
import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import de.nowchess.io.{GameContextExport, GameContextImport}
/** ScalaFX chess board view that displays the game state. /** ScalaFX chess board view that displays the game state.
* Uses chess sprites and color palette. * Uses chess sprites and color palette.
@@ -276,33 +279,32 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
case _ => engine.completePromotion(PromotionPiece.Queen) // Default case _ => engine.completePromotion(PromotionPiece.Queen) // Default
private def doFenExport(): Unit = private def doFenExport(): Unit =
val fen = de.nowchess.io.fen.FenExporter.gameContextToFen(engine.context) doExport(FenExporter, "FEN")
showCopyDialog("FEN Export", fen)
private def doFenImport(): Unit = private def doFenImport(): Unit =
showInputDialog("FEN Import", rows = 1).foreach { fen => doImport(FenParser, "FEN")
de.nowchess.io.fen.FenParser.parseFen(fen) match
case Some(gameContext) =>
engine.loadPosition(gameContext)
case None =>
showMessage("⚠️ Invalid FEN string")
}
private def doPgnExport(): Unit = private def doPgnExport(): Unit =
val pgn = de.nowchess.io.pgn.PgnExporter.exportGame( doExport(PgnExporter, "PGN")
Map("Event" -> "NowChess Game", "Date" -> "2026.04.04"),
exportableGameHistory()
)
showCopyDialog("PGN Export", pgn)
private def doPgnImport(): Unit = private def doPgnImport(): Unit =
showInputDialog("PGN Import", rows = 5).foreach { pgn => doImport(PgnParser, "PGN")
engine.loadPgn(pgn) match
case Right(_) => private def doExport(exporter: GameContextExport, formatName: String): Unit = {
showMessage("✓ PGN loaded successfully!") val exported = exporter.exportGameContext(engine.context)
showCopyDialog(s"$formatName Export", exported)
}
private def doImport(importer: GameContextImport, formatName: String): Unit = {
showInputDialog(s"$formatName Import", rows = 5).foreach { input =>
importer.importGameContext(input) match
case Right(gameContext) =>
engine.loadPosition(gameContext)
showMessage(s"$formatName loaded successfully!")
case Left(err) => case Left(err) =>
showMessage(s"⚠️ PGN Error: $err") showMessage(s"⚠️ $formatName Error: $err")
} }
}
private def showCopyDialog(title: String, content: String): Unit = private def showCopyDialog(title: String, content: String): Unit =
val area = new javafx.scene.control.TextArea(content) val area = new javafx.scene.control.TextArea(content)
@@ -335,33 +337,3 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
val result = dialog.showAndWait() val result = dialog.showAndWait()
if result.isPresent && result.get != null && result.get.nonEmpty then Some(result.get) else None if result.isPresent && result.get != null && result.get.nonEmpty then Some(result.get) else None
private def exportableGameHistory(): GameHistory =
val moveCommands = engine.commandHistory.collect { case moveCmd: MoveCommand => moveCmd }
val activeMoveCount = engine.context.moves.length
val historyMoves = moveCommands.take(activeMoveCount).flatMap: moveCmd =>
moveCmd.previousContext.flatMap: previousContext =>
moveCmd.moveResult.collect:
case MoveResult.Successful(_, captured) =>
val movingPiece = previousContext.board.pieceAt(moveCmd.from)
val pieceType = movingPiece.map(_.pieceType).getOrElse(PieceType.Pawn)
val castleSide = moveCmd.notation match
case "O-O" => Some("Kingside")
case "O-O-O" => Some("Queenside")
case _ => None
val promotionPiece = moveCmd.notation.split("=").lastOption.flatMap:
case "Q" => Some(PromotionPiece.Queen)
case "R" => Some(PromotionPiece.Rook)
case "B" => Some(PromotionPiece.Bishop)
case "N" => Some(PromotionPiece.Knight)
case _ => None
HistoryMove(
from = moveCmd.from,
to = moveCmd.to,
castleSide = castleSide,
promotionPiece = promotionPiece,
pieceType = pieceType,
isCapture = captured.isDefined
)
GameHistory(historyMoves.toList)