refactor: clean up code formatting and improve readability in ChessBoardView and related files
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-12 20:58:26 +02:00
parent 5f2f12a02b
commit 52ae4244da
13 changed files with 311 additions and 317 deletions
@@ -7,8 +7,7 @@ import scala.util.Try
/** Service for persisting and loading game states to/from disk. /** Service for persisting and loading game states to/from disk.
* *
* Abstracts file I/O operations away from the UI layer. * Abstracts file I/O operations away from the UI layer. Handles both reading and writing game files.
* Handles both reading and writing game files.
*/ */
trait GameFileService: trait GameFileService:
def saveGameToFile(context: GameContext, path: Path, exporter: GameContextExport): Either[String, Unit] def saveGameToFile(context: GameContext, path: Path, exporter: GameContextExport): Either[String, Unit]
@@ -25,7 +24,7 @@ object FileSystemGameService extends GameFileService:
() ()
}.fold( }.fold(
ex => Left(s"Failed to save file: ${ex.getMessage}"), ex => Left(s"Failed to save file: ${ex.getMessage}"),
_ => Right(()) _ => Right(()),
) )
/** Load a game context from a file using the specified importer. */ /** Load a game context from a file using the specified importer. */
@@ -35,5 +34,5 @@ object FileSystemGameService extends GameFileService:
importer.importGameContext(json) importer.importGameContext(json)
}.fold( }.fold(
ex => Left(s"Failed to load file: ${ex.getMessage}"), ex => Left(s"Failed to load file: ${ex.getMessage}"),
result => result result => result,
) )
@@ -8,7 +8,7 @@ import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.io.GameContextExport import de.nowchess.io.GameContextExport
import de.nowchess.io.pgn.PgnExporter import de.nowchess.io.pgn.PgnExporter
import java.time.{LocalDate, ZonedDateTime, ZoneId} import java.time.{LocalDate, ZoneId, ZonedDateTime}
/** Exports a GameContext to a comprehensive JSON format using Jackson. /** Exports a GameContext to a comprehensive JSON format using Jackson.
* *
@@ -42,9 +42,10 @@ object JsonExporter extends GameContextExport:
formatJson(mapper.writeValueAsString(record)) formatJson(mapper.writeValueAsString(record))
private def buildGameRecord(context: GameContext): JsonGameRecord = private def buildGameRecord(context: GameContext): JsonGameRecord =
val pgn = try { val pgn =
try
Some(PgnExporter.exportGameContext(context)) Some(PgnExporter.exportGameContext(context))
} catch { catch {
case _: Exception => None case _: Exception => None
} }
JsonGameRecord( JsonGameRecord(
@@ -53,7 +54,7 @@ object JsonExporter extends GameContextExport:
moveHistory = pgn, moveHistory = pgn,
moves = Some(buildMoves(context.moves)), moves = Some(buildMoves(context.moves)),
capturedPieces = Some(buildCapturedPieces(context.board)), capturedPieces = Some(buildCapturedPieces(context.board)),
timestamp = Some(ZonedDateTime.now(ZoneId.of("UTC")).toString) timestamp = Some(ZonedDateTime.now(ZoneId.of("UTC")).toString),
) )
private def buildMetadata(): JsonMetadata = private def buildMetadata(): JsonMetadata =
@@ -61,7 +62,7 @@ object JsonExporter extends GameContextExport:
event = Some("Game"), event = Some("Game"),
players = Some(Map("white" -> "White Player", "black" -> "Black Player")), players = Some(Map("white" -> "White Player", "black" -> "Black Player")),
date = Some(LocalDate.now().toString), date = Some(LocalDate.now().toString),
result = Some("*") result = Some("*"),
) )
private def buildGameState(context: GameContext): JsonGameState = private def buildGameState(context: GameContext): JsonGameState =
@@ -70,7 +71,7 @@ object JsonExporter extends GameContextExport:
turn = Some(context.turn.label), turn = Some(context.turn.label),
castlingRights = Some(buildCastlingRights(context.castlingRights)), castlingRights = Some(buildCastlingRights(context.castlingRights)),
enPassantSquare = context.enPassantSquare.map(_.toString), enPassantSquare = context.enPassantSquare.map(_.toString),
halfMoveClock = Some(context.halfMoveClock) halfMoveClock = Some(context.halfMoveClock),
) )
private def buildBoardPieces(board: Board): List[JsonPiece] = private def buildBoardPieces(board: Board): List[JsonPiece] =
@@ -83,7 +84,7 @@ object JsonExporter extends GameContextExport:
Some(rights.whiteKingSide), Some(rights.whiteKingSide),
Some(rights.whiteQueenSide), Some(rights.whiteQueenSide),
Some(rights.blackKingSide), Some(rights.blackKingSide),
Some(rights.blackQueenSide) Some(rights.blackQueenSide),
) )
private def buildMoves(moves: List[Move]): List[JsonMove] = private def buildMoves(moves: List[Move]): List[JsonMove] =
@@ -136,4 +137,3 @@ object JsonExporter extends GameContextExport:
val whiteCaptured = captured.filter(_.color == Color.White).map(_.pieceType.label).toList val whiteCaptured = captured.filter(_.color == Color.White).map(_.pieceType.label).toList
val blackCaptured = captured.filter(_.color == Color.Black).map(_.pieceType.label).toList val blackCaptured = captured.filter(_.color == Color.Black).map(_.pieceType.label).toList
(blackCaptured, whiteCaptured) (blackCaptured, whiteCaptured)
@@ -4,20 +4,20 @@ case class JsonMetadata(
event: Option[String] = None, event: Option[String] = None,
players: Option[Map[String, String]] = None, players: Option[Map[String, String]] = None,
date: Option[String] = None, date: Option[String] = None,
result: Option[String] = None result: Option[String] = None,
) )
case class JsonPiece( case class JsonPiece(
square: Option[String] = None, square: Option[String] = None,
color: Option[String] = None, color: Option[String] = None,
piece: Option[String] = None piece: Option[String] = None,
) )
case class JsonCastlingRights( case class JsonCastlingRights(
whiteKingSide: Option[Boolean] = None, whiteKingSide: Option[Boolean] = None,
whiteQueenSide: Option[Boolean] = None, whiteQueenSide: Option[Boolean] = None,
blackKingSide: Option[Boolean] = None, blackKingSide: Option[Boolean] = None,
blackQueenSide: Option[Boolean] = None blackQueenSide: Option[Boolean] = None,
) )
case class JsonGameState( case class JsonGameState(
@@ -25,24 +25,24 @@ case class JsonGameState(
turn: Option[String] = None, turn: Option[String] = None,
castlingRights: Option[JsonCastlingRights] = None, castlingRights: Option[JsonCastlingRights] = None,
enPassantSquare: Option[String] = None, enPassantSquare: Option[String] = None,
halfMoveClock: Option[Int] = None halfMoveClock: Option[Int] = None,
) )
case class JsonCapturedPieces( case class JsonCapturedPieces(
byWhite: Option[List[String]] = None, byWhite: Option[List[String]] = None,
byBlack: Option[List[String]] = None byBlack: Option[List[String]] = None,
) )
case class JsonMoveType( case class JsonMoveType(
`type`: Option[String] = None, `type`: Option[String] = None,
isCapture: Option[Boolean] = None, isCapture: Option[Boolean] = None,
promotionPiece: Option[String] = None promotionPiece: Option[String] = None,
) )
case class JsonMove( case class JsonMove(
from: Option[String] = None, from: Option[String] = None,
to: Option[String] = None, to: Option[String] = None,
`type`: Option[JsonMoveType] = None `type`: Option[JsonMoveType] = None,
) )
case class JsonGameRecord( case class JsonGameRecord(
@@ -51,5 +51,5 @@ case class JsonGameRecord(
moveHistory: Option[String] = None, moveHistory: Option[String] = None,
moves: Option[List[JsonMove]] = None, moves: Option[List[JsonMove]] = None,
capturedPieces: Option[JsonCapturedPieces] = None, capturedPieces: Option[JsonCapturedPieces] = None,
timestamp: Option[String] = None timestamp: Option[String] = None,
) )
@@ -1,6 +1,6 @@
package de.nowchess.io.json package de.nowchess.io.json
import com.fasterxml.jackson.databind.{ObjectMapper, DeserializationFeature} import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper}
import com.fasterxml.jackson.module.scala.DefaultScalaModule import com.fasterxml.jackson.module.scala.DefaultScalaModule
import de.nowchess.api.board.* import de.nowchess.api.board.*
import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
@@ -27,8 +27,8 @@ object JsonParser extends GameContextImport:
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
def importGameContext(input: String): Either[String, GameContext] = def importGameContext(input: String): Either[String, GameContext] =
Try(mapper.readValue(input, classOf[JsonGameRecord])).toEither Try(mapper.readValue(input, classOf[JsonGameRecord])).toEither.left
.left.map(e => "JSON parsing error: " + e.getMessage) .map(e => "JSON parsing error: " + e.getMessage)
.flatMap { data => .flatMap { data =>
val gs = data.gameState.getOrElse(JsonGameState()) val gs = data.gameState.getOrElse(JsonGameState())
val rawBoard = gs.board.getOrElse(Nil) val rawBoard = gs.board.getOrElse(Nil)
@@ -49,7 +49,7 @@ object JsonParser extends GameContextImport:
castlingRights = castlingRights, castlingRights = castlingRights,
enPassantSquare = enPassantSquare, enPassantSquare = enPassantSquare,
halfMoveClock = rawHmc, halfMoveClock = rawHmc,
moves = moves moves = moves,
) )
} }
@@ -86,7 +86,7 @@ object JsonParser extends GameContextImport:
cr.whiteKingSide.getOrElse(false), cr.whiteKingSide.getOrElse(false),
cr.whiteQueenSide.getOrElse(false), cr.whiteQueenSide.getOrElse(false),
cr.blackKingSide.getOrElse(false), cr.blackKingSide.getOrElse(false),
cr.blackQueenSide.getOrElse(false) cr.blackQueenSide.getOrElse(false),
) )
private def parseMoves(moves: List[JsonMove]): Either[String, List[Move]] = private def parseMoves(moves: List[JsonMove]): Either[String, List[Move]] =
@@ -1,7 +1,7 @@
package de.nowchess.io package de.nowchess.io
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{Square, File, Rank} import de.nowchess.api.board.{File, Rank, Square}
import de.nowchess.api.move.Move import de.nowchess.api.move.Move
import de.nowchess.io.json.{JsonExporter, JsonParser} import de.nowchess.io.json.{JsonExporter, JsonParser}
import java.nio.file.{Files, Paths} import java.nio.file.{Files, Paths}
@@ -20,8 +20,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
assert(result.isRight) assert(result.isRight)
assert(Files.exists(tmpFile)) assert(Files.exists(tmpFile))
assert(Files.size(tmpFile) > 0) assert(Files.size(tmpFile) > 0)
finally finally Files.deleteIfExists(tmpFile)
Files.deleteIfExists(tmpFile)
} }
test("loadGameFromFile: reads JSON file successfully") { test("loadGameFromFile: reads JSON file successfully") {
@@ -38,8 +37,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
assert(result.isRight) assert(result.isRight)
val loaded = result.getOrElse(GameContext.initial) val loaded = result.getOrElse(GameContext.initial)
assert(loaded == originalContext) assert(loaded == originalContext)
finally finally Files.deleteIfExists(tmpFile)
Files.deleteIfExists(tmpFile)
} }
test("loadGameFromFile: returns error on missing file") { test("loadGameFromFile: returns error on missing file") {
@@ -65,8 +63,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
assert(loadResult.isRight) assert(loadResult.isRight)
val loaded = loadResult.getOrElse(GameContext.initial) val loaded = loadResult.getOrElse(GameContext.initial)
assert(loaded.moves.length == 2) assert(loaded.moves.length == 2)
finally finally Files.deleteIfExists(tmpFile)
Files.deleteIfExists(tmpFile)
} }
test("saveGameToFile: overwrites existing file") { test("saveGameToFile: overwrites existing file") {
@@ -86,8 +83,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
assert(loadResult.isRight) assert(loadResult.isRight)
val loaded = loadResult.getOrElse(GameContext.initial) val loaded = loadResult.getOrElse(GameContext.initial)
assert(loaded.moves.length == 1) assert(loaded.moves.length == 1)
finally finally Files.deleteIfExists(tmpFile)
Files.deleteIfExists(tmpFile)
} }
test("loadGameFromFile: handles invalid JSON in file") { test("loadGameFromFile: handles invalid JSON in file") {
@@ -97,8 +93,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
val result = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser) val result = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser)
assert(result.isLeft) assert(result.isLeft)
finally finally Files.deleteIfExists(tmpFile)
Files.deleteIfExists(tmpFile)
} }
test("round-trip: save and load preserves game state") { test("round-trip: save and load preserves game state") {
@@ -118,8 +113,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
val loaded = loadResult.getOrElse(GameContext.initial) val loaded = loadResult.getOrElse(GameContext.initial)
assert(loaded.moves.length == 2) assert(loaded.moves.length == 2)
assert(loaded.halfMoveClock == 3) assert(loaded.halfMoveClock == 3)
finally finally Files.deleteIfExists(tmpFile)
Files.deleteIfExists(tmpFile)
} }
test("saveGameToFile: handles exporter that throws exception") { test("saveGameToFile: handles exporter that throws exception") {
@@ -134,6 +128,5 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
val result = FileSystemGameService.saveGameToFile(context, tmpFile, faultyExporter) val result = FileSystemGameService.saveGameToFile(context, tmpFile, faultyExporter)
assert(result.isLeft) assert(result.isLeft)
assert(result.left.toOption.get.contains("Failed to save file")) assert(result.left.toOption.get.contains("Failed to save file"))
finally finally Files.deleteIfExists(tmpFile)
Files.deleteIfExists(tmpFile)
} }
@@ -1,7 +1,7 @@
package de.nowchess.io.json package de.nowchess.io.json
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{Square, File, Rank, Board, Color, CastlingRights, Piece, PieceType} import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
@@ -13,10 +13,10 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
(PromotionPiece.Queen, "queen"), (PromotionPiece.Queen, "queen"),
(PromotionPiece.Rook, "rook"), (PromotionPiece.Rook, "rook"),
(PromotionPiece.Bishop, "bishop"), (PromotionPiece.Bishop, "bishop"),
(PromotionPiece.Knight, "knight") (PromotionPiece.Knight, "knight"),
) )
for ((piece, expectedName) <- promotions) do for (piece, expectedName) <- promotions do
val move = Move(Square(File.A, Rank.R7), Square(File.A, Rank.R8), MoveType.Promotion(piece)) val move = Move(Square(File.A, Rank.R7), Square(File.A, Rank.R8), MoveType.Promotion(piece))
// Empty boards can cause issues in PgnExporter, using initial // Empty boards can cause issues in PgnExporter, using initial
val ctx = GameContext.initial.copy(moves = List(move)) val ctx = GameContext.initial.copy(moves = List(move))
@@ -1,7 +1,7 @@
package de.nowchess.io.json package de.nowchess.io.json
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{Board, Square, Piece, Color, PieceType, File, Rank, CastlingRights} import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
@@ -40,7 +40,7 @@ class JsonModelExtraTestSuite extends AnyFunSuite with Matchers:
Some("White"), Some("White"),
Some(JsonCastlingRights()), Some(JsonCastlingRights()),
Some("e3"), Some("e3"),
Some(5) Some(5),
) )
assert(gs.board.contains(Nil)) assert(gs.board.contains(Nil))
assert(gs.halfMoveClock.contains(5)) assert(gs.halfMoveClock.contains(5))
@@ -88,7 +88,7 @@ class JsonModelExtraTestSuite extends AnyFunSuite with Matchers:
Some(""), Some(""),
Some(Nil), Some(Nil),
Some(JsonCapturedPieces()), Some(JsonCapturedPieces()),
Some("2026-04-08T00:00:00Z") Some("2026-04-08T00:00:00Z"),
) )
assert(record.metadata.nonEmpty) assert(record.metadata.nonEmpty)
assert(record.timestamp.nonEmpty) assert(record.timestamp.nonEmpty)
@@ -124,7 +124,12 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
assert(result.isRight) assert(result.isRight)
val ctx = result.toOption.get val ctx = result.toOption.get
assert(ctx.board.pieces.size == 6) assert(ctx.board.pieces.size == 6)
assert(ctx.board.pieceAt(de.nowchess.api.board.Square(de.nowchess.api.board.File.A, de.nowchess.api.board.Rank.R1)).get.pieceType == PieceType.Pawn) assert(
ctx.board
.pieceAt(de.nowchess.api.board.Square(de.nowchess.api.board.File.A, de.nowchess.api.board.Rank.R1))
.get
.pieceType == PieceType.Pawn,
)
} }
test("parse with all castling rights false") { test("parse with all castling rights false") {
@@ -1,7 +1,7 @@
package de.nowchess.io.json package de.nowchess.io.json
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{Color, PieceType, Piece, Square, File, Rank} import de.nowchess.api.board.{Color, File, Piece, PieceType, Rank, Square}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
@@ -1,7 +1,7 @@
package de.nowchess.io.json package de.nowchess.io.json
import de.nowchess.api.game.GameContext import de.nowchess.api.game.GameContext
import de.nowchess.api.board.{Color, File, Rank, Square, CastlingRights} import de.nowchess.api.board.{CastlingRights, Color, File, Rank, Square}
import de.nowchess.api.move.{Move, MoveType, PromotionPiece} import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
@@ -68,7 +68,8 @@ class JsonParserSuite extends AnyFunSuite with Matchers:
} }
test("importGameContext: handles missing fields with defaults") { test("importGameContext: handles missing fields with defaults") {
val json = "{\"metadata\": {}, \"gameState\": {\"board\": [], \"turn\": \"White\", \"castlingRights\": {\"whiteKingSide\": true, \"whiteQueenSide\": true, \"blackKingSide\": true, \"blackQueenSide\": true}, \"enPassantSquare\": null, \"halfMoveClock\": 0}, \"moves\": [], \"moveHistory\": \"\", \"capturedPieces\": {\"byWhite\": [], \"byBlack\": []}, \"timestamp\": \"2026-01-01T00:00:00Z\"}" val json =
"{\"metadata\": {}, \"gameState\": {\"board\": [], \"turn\": \"White\", \"castlingRights\": {\"whiteKingSide\": true, \"whiteQueenSide\": true, \"blackKingSide\": true, \"blackQueenSide\": true}, \"enPassantSquare\": null, \"halfMoveClock\": 0}, \"moves\": [], \"moveHistory\": \"\", \"capturedPieces\": {\"byWhite\": [], \"byBlack\": []}, \"timestamp\": \"2026-01-01T00:00:00Z\"}"
val result = JsonParser.importGameContext(json) val result = JsonParser.importGameContext(json)
assert(result.isRight) assert(result.isRight)
@@ -17,14 +17,13 @@ import de.nowchess.chess.engine.GameEngine
import de.nowchess.io.fen.{FenExporter, FenParser} import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser} import de.nowchess.io.pgn.{PgnExporter, PgnParser}
import de.nowchess.io.json.{JsonExporter, JsonParser} import de.nowchess.io.json.{JsonExporter, JsonParser}
import de.nowchess.io.{GameContextExport, GameContextImport, GameFileService, FileSystemGameService} import de.nowchess.io.{FileSystemGameService, GameContextExport, GameContextImport, GameFileService}
import java.nio.file.Paths import java.nio.file.Paths
import scalafx.stage.FileChooser import scalafx.stage.FileChooser
import scalafx.stage.FileChooser.ExtensionFilter import scalafx.stage.FileChooser.ExtensionFilter
/** ScalaFX chess board view that displays the game state. /** ScalaFX chess board view that displays the game state. Uses chess sprites and color palette. Handles user
* Uses chess sprites and color palette. * interactions (clicks) and sends moves to GameEngine.
* Handles user interactions (clicks) and sends moves to GameEngine.
*/ */
class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends BorderPane: class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends BorderPane:
@@ -58,7 +57,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
font = Font.font(comicSansFontFamily, 24) font = Font.font(comicSansFontFamily, 24)
style = "-fx-font-weight: bold;" style = "-fx-font-weight: bold;"
}, },
messageLabel messageLabel,
) )
} }
@@ -86,8 +85,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
disable = !engine.canUndo disable = !engine.canUndo
} }
undoButton undoButton
}, }, {
{
redoButton = new Button("Redo") { redoButton = new Button("Redo") {
font = Font.font(comicSansFontFamily, 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => if engine.canRedo then engine.redo() onAction = _ => if engine.canRedo then engine.redo()
@@ -100,7 +98,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
font = Font.font(comicSansFontFamily, 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => engine.reset() onAction = _ => engine.reset()
style = "-fx-background-radius: 8; -fx-background-color: #E1EAA9;" style = "-fx-background-radius: 8; -fx-background-color: #E1EAA9;"
} },
) )
}, },
new HBox { new HBox {
@@ -126,7 +124,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
font = Font.font(comicSansFontFamily, 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => doPgnImport() onAction = _ => doPgnImport()
style = "-fx-background-radius: 8; -fx-background-color: #B9DAC4;" style = "-fx-background-radius: 8; -fx-background-color: #B9DAC4;"
} },
) )
}, },
new HBox { new HBox {
@@ -142,9 +140,9 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
font = Font.font(comicSansFontFamily, 12) font = Font.font(comicSansFontFamily, 12)
onAction = _ => doJsonImport() onAction = _ => doJsonImport()
style = "-fx-background-radius: 8; -fx-background-color: #C4B9DA;" style = "-fx-background-radius: 8; -fx-background-color: #C4B9DA;"
} },
) )
} },
) )
} }
@@ -185,8 +183,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
square square
private def handleSquareClick(rank: Int, file: Int): Unit = private def handleSquareClick(rank: Int, file: Int): Unit =
if engine.isPendingPromotion then if engine.isPendingPromotion then return // Don't allow moves during promotion
return // Don't allow moves during promotion
val clickedSquare = Square(File.values(file), Rank.values(rank)) val clickedSquare = Square(File.values(file), Rank.values(rank))
@@ -198,7 +195,8 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
selectedSquare = Some(clickedSquare) selectedSquare = Some(clickedSquare)
highlightSquare(rank, file, PieceSprites.SquareColors.Selected) highlightSquare(rank, file, PieceSprites.SquareColors.Selected)
val legalDests = engine.ruleSet.legalMoves(engine.context)(clickedSquare) val legalDests = engine.ruleSet
.legalMoves(engine.context)(clickedSquare)
.collect { case move if move.from == clickedSquare => move.to } .collect { case move if move.from == clickedSquare => move.to }
legalDests.foreach { sq => legalDests.foreach { sq =>
highlightSquare(sq.rank.ordinal, sq.file.ordinal, PieceSprites.SquareColors.ValidMove) highlightSquare(sq.rank.ordinal, sq.file.ordinal, PieceSprites.SquareColors.ValidMove)
@@ -322,7 +320,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
val result = FileSystemGameService.saveGameToFile( val result = FileSystemGameService.saveGameToFile(
engine.context, engine.context,
selectedFile.toPath, selectedFile.toPath,
JsonExporter JsonExporter,
) )
result match result match
case Right(_) => showMessage(s"✓ Game saved to: ${selectedFile.getName}") case Right(_) => showMessage(s"✓ Game saved to: ${selectedFile.getName}")
@@ -339,7 +337,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
if selectedFile != null then if selectedFile != null then
val result = FileSystemGameService.loadGameFromFile( val result = FileSystemGameService.loadGameFromFile(
selectedFile.toPath, selectedFile.toPath,
JsonParser JsonParser,
) )
result match result match
case Right(gameContext) => case Right(gameContext) =>
@@ -353,7 +351,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
showCopyDialog(s"$formatName Export", exported) showCopyDialog(s"$formatName Export", exported)
} }
private def doImport(importer: GameContextImport, formatName: String): Unit = { private def doImport(importer: GameContextImport, formatName: String): Unit =
showInputDialog(s"$formatName Import", rows = 5).foreach { input => showInputDialog(s"$formatName Import", rows = 5).foreach { input =>
importer.importGameContext(input) match importer.importGameContext(input) match
case Right(gameContext) => case Right(gameContext) =>
@@ -362,7 +360,6 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
case Left(err) => case Left(err) =>
showMessage(s"⚠️ $formatName 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)
@@ -386,7 +383,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
dialog.getDialogPane.setContent(area) dialog.getDialogPane.setContent(area)
dialog.getDialogPane.getButtonTypes.addAll( dialog.getDialogPane.getButtonTypes.addAll(
javafx.scene.control.ButtonType.OK, javafx.scene.control.ButtonType.OK,
javafx.scene.control.ButtonType.CANCEL javafx.scene.control.ButtonType.CANCEL,
) )
dialog.setResultConverter { bt => dialog.setResultConverter { bt =>
if bt == javafx.scene.control.ButtonType.OK then area.getText else null if bt == javafx.scene.control.ButtonType.OK then area.getText else null
@@ -394,4 +391,3 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
dialog.initOwner(stage.delegate) dialog.initOwner(stage.delegate)
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