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.
*
* Abstracts file I/O operations away from the UI layer.
* Handles both reading and writing game files.
* Abstracts file I/O operations away from the UI layer. Handles both reading and writing game files.
*/
trait GameFileService:
def saveGameToFile(context: GameContext, path: Path, exporter: GameContextExport): Either[String, Unit]
@@ -25,7 +24,7 @@ object FileSystemGameService extends GameFileService:
()
}.fold(
ex => Left(s"Failed to save file: ${ex.getMessage}"),
_ => Right(())
_ => Right(()),
)
/** Load a game context from a file using the specified importer. */
@@ -35,5 +34,5 @@ object FileSystemGameService extends GameFileService:
importer.importGameContext(json)
}.fold(
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.io.GameContextExport
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.
*
@@ -42,9 +42,10 @@ object JsonExporter extends GameContextExport:
formatJson(mapper.writeValueAsString(record))
private def buildGameRecord(context: GameContext): JsonGameRecord =
val pgn = try {
val pgn =
try
Some(PgnExporter.exportGameContext(context))
} catch {
catch {
case _: Exception => None
}
JsonGameRecord(
@@ -53,7 +54,7 @@ object JsonExporter extends GameContextExport:
moveHistory = pgn,
moves = Some(buildMoves(context.moves)),
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 =
@@ -61,7 +62,7 @@ object JsonExporter extends GameContextExport:
event = Some("Game"),
players = Some(Map("white" -> "White Player", "black" -> "Black Player")),
date = Some(LocalDate.now().toString),
result = Some("*")
result = Some("*"),
)
private def buildGameState(context: GameContext): JsonGameState =
@@ -70,7 +71,7 @@ object JsonExporter extends GameContextExport:
turn = Some(context.turn.label),
castlingRights = Some(buildCastlingRights(context.castlingRights)),
enPassantSquare = context.enPassantSquare.map(_.toString),
halfMoveClock = Some(context.halfMoveClock)
halfMoveClock = Some(context.halfMoveClock),
)
private def buildBoardPieces(board: Board): List[JsonPiece] =
@@ -83,7 +84,7 @@ object JsonExporter extends GameContextExport:
Some(rights.whiteKingSide),
Some(rights.whiteQueenSide),
Some(rights.blackKingSide),
Some(rights.blackQueenSide)
Some(rights.blackQueenSide),
)
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 blackCaptured = captured.filter(_.color == Color.Black).map(_.pieceType.label).toList
(blackCaptured, whiteCaptured)
@@ -4,20 +4,20 @@ case class JsonMetadata(
event: Option[String] = None,
players: Option[Map[String, String]] = None,
date: Option[String] = None,
result: Option[String] = None
result: Option[String] = None,
)
case class JsonPiece(
square: Option[String] = None,
color: Option[String] = None,
piece: Option[String] = None
piece: Option[String] = None,
)
case class JsonCastlingRights(
whiteKingSide: Option[Boolean] = None,
whiteQueenSide: Option[Boolean] = None,
blackKingSide: Option[Boolean] = None,
blackQueenSide: Option[Boolean] = None
blackQueenSide: Option[Boolean] = None,
)
case class JsonGameState(
@@ -25,24 +25,24 @@ case class JsonGameState(
turn: Option[String] = None,
castlingRights: Option[JsonCastlingRights] = None,
enPassantSquare: Option[String] = None,
halfMoveClock: Option[Int] = None
halfMoveClock: Option[Int] = None,
)
case class JsonCapturedPieces(
byWhite: Option[List[String]] = None,
byBlack: Option[List[String]] = None
byBlack: Option[List[String]] = None,
)
case class JsonMoveType(
`type`: Option[String] = None,
isCapture: Option[Boolean] = None,
promotionPiece: Option[String] = None
promotionPiece: Option[String] = None,
)
case class JsonMove(
from: Option[String] = None,
to: Option[String] = None,
`type`: Option[JsonMoveType] = None
`type`: Option[JsonMoveType] = None,
)
case class JsonGameRecord(
@@ -51,5 +51,5 @@ case class JsonGameRecord(
moveHistory: Option[String] = None,
moves: Option[List[JsonMove]] = None,
capturedPieces: Option[JsonCapturedPieces] = None,
timestamp: Option[String] = None
timestamp: Option[String] = None,
)
@@ -1,6 +1,6 @@
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 de.nowchess.api.board.*
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
@@ -27,8 +27,8 @@ object JsonParser extends GameContextImport:
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
def importGameContext(input: String): Either[String, GameContext] =
Try(mapper.readValue(input, classOf[JsonGameRecord])).toEither
.left.map(e => "JSON parsing error: " + e.getMessage)
Try(mapper.readValue(input, classOf[JsonGameRecord])).toEither.left
.map(e => "JSON parsing error: " + e.getMessage)
.flatMap { data =>
val gs = data.gameState.getOrElse(JsonGameState())
val rawBoard = gs.board.getOrElse(Nil)
@@ -49,7 +49,7 @@ object JsonParser extends GameContextImport:
castlingRights = castlingRights,
enPassantSquare = enPassantSquare,
halfMoveClock = rawHmc,
moves = moves
moves = moves,
)
}
@@ -86,7 +86,7 @@ object JsonParser extends GameContextImport:
cr.whiteKingSide.getOrElse(false),
cr.whiteQueenSide.getOrElse(false),
cr.blackKingSide.getOrElse(false),
cr.blackQueenSide.getOrElse(false)
cr.blackQueenSide.getOrElse(false),
)
private def parseMoves(moves: List[JsonMove]): Either[String, List[Move]] =
@@ -1,7 +1,7 @@
package de.nowchess.io
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.io.json.{JsonExporter, JsonParser}
import java.nio.file.{Files, Paths}
@@ -20,8 +20,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
assert(result.isRight)
assert(Files.exists(tmpFile))
assert(Files.size(tmpFile) > 0)
finally
Files.deleteIfExists(tmpFile)
finally Files.deleteIfExists(tmpFile)
}
test("loadGameFromFile: reads JSON file successfully") {
@@ -38,8 +37,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
assert(result.isRight)
val loaded = result.getOrElse(GameContext.initial)
assert(loaded == originalContext)
finally
Files.deleteIfExists(tmpFile)
finally Files.deleteIfExists(tmpFile)
}
test("loadGameFromFile: returns error on missing file") {
@@ -65,8 +63,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
assert(loadResult.isRight)
val loaded = loadResult.getOrElse(GameContext.initial)
assert(loaded.moves.length == 2)
finally
Files.deleteIfExists(tmpFile)
finally Files.deleteIfExists(tmpFile)
}
test("saveGameToFile: overwrites existing file") {
@@ -86,8 +83,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
assert(loadResult.isRight)
val loaded = loadResult.getOrElse(GameContext.initial)
assert(loaded.moves.length == 1)
finally
Files.deleteIfExists(tmpFile)
finally Files.deleteIfExists(tmpFile)
}
test("loadGameFromFile: handles invalid JSON in file") {
@@ -97,8 +93,7 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
val result = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser)
assert(result.isLeft)
finally
Files.deleteIfExists(tmpFile)
finally Files.deleteIfExists(tmpFile)
}
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)
assert(loaded.moves.length == 2)
assert(loaded.halfMoveClock == 3)
finally
Files.deleteIfExists(tmpFile)
finally Files.deleteIfExists(tmpFile)
}
test("saveGameToFile: handles exporter that throws exception") {
@@ -134,6 +128,5 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers:
val result = FileSystemGameService.saveGameToFile(context, tmpFile, faultyExporter)
assert(result.isLeft)
assert(result.left.toOption.get.contains("Failed to save file"))
finally
Files.deleteIfExists(tmpFile)
finally Files.deleteIfExists(tmpFile)
}
@@ -1,7 +1,7 @@
package de.nowchess.io.json
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 org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@@ -13,17 +13,17 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
(PromotionPiece.Queen, "queen"),
(PromotionPiece.Rook, "rook"),
(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))
// Empty boards can cause issues in PgnExporter, using initial
val ctx = GameContext.initial.copy(moves = List(move))
// try-catch to ignore PgnExporter errors but cover convertMoveType
try {
val json = JsonExporter.exportGameContext(ctx)
json should include (s""""$expectedName"""")
json should include(s""""$expectedName"""")
} catch { case _: Exception => }
}
@@ -31,7 +31,7 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
val quietMove = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4), MoveType.Normal(false))
val ctx = GameContext.initial.copy(moves = List(quietMove))
val json = JsonExporter.exportGameContext(ctx)
json should include ("\"normal\"")
json should include("\"normal\"")
}
test("export normal capture move manually") {
@@ -39,8 +39,8 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include ("\"normal\"")
json should include ("\"isCapture\": true")
json should include("\"normal\"")
json should include("\"isCapture\": true")
} catch { case _: Exception => }
}
@@ -49,9 +49,9 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
val ctx = GameContext.initial.copy(moves = List(move))
val json = JsonExporter.exportGameContext(ctx)
json should include ("\"moves\"")
json should include ("\"from\"")
json should include ("\"to\"")
json should include("\"moves\"")
json should include("\"from\"")
json should include("\"to\"")
}
test("export castle queenside move") {
@@ -59,7 +59,7 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include ("\"castleQueenside\"")
json should include("\"castleQueenside\"")
} catch { case _: Exception => }
}
@@ -68,7 +68,7 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include ("\"castleKingside\"")
json should include("\"castleKingside\"")
} catch { case _: Exception => }
}
@@ -77,7 +77,7 @@ class JsonExporterBranchCoverageSuite extends AnyFunSuite with Matchers:
val ctx = GameContext.initial.copy(moves = List(move))
try {
val json = JsonExporter.exportGameContext(ctx)
json should include ("\"enPassant\"")
json should include ("\"isCapture\": true")
json should include("\"enPassant\"")
json should include("\"isCapture\": true")
} catch { case _: Exception => }
}
@@ -1,7 +1,7 @@
package de.nowchess.io.json
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 org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@@ -40,7 +40,7 @@ class JsonModelExtraTestSuite extends AnyFunSuite with Matchers:
Some("White"),
Some(JsonCastlingRights()),
Some("e3"),
Some(5)
Some(5),
)
assert(gs.board.contains(Nil))
assert(gs.halfMoveClock.contains(5))
@@ -88,7 +88,7 @@ class JsonModelExtraTestSuite extends AnyFunSuite with Matchers:
Some(""),
Some(Nil),
Some(JsonCapturedPieces()),
Some("2026-04-08T00:00:00Z")
Some("2026-04-08T00:00:00Z"),
)
assert(record.metadata.nonEmpty)
assert(record.timestamp.nonEmpty)
@@ -124,7 +124,12 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers:
assert(result.isRight)
val ctx = result.toOption.get
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") {
@@ -1,7 +1,7 @@
package de.nowchess.io.json
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 org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@@ -1,7 +1,7 @@
package de.nowchess.io.json
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 org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
@@ -68,7 +68,8 @@ class JsonParserSuite extends AnyFunSuite with Matchers:
}
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)
assert(result.isRight)
@@ -17,14 +17,13 @@ import de.nowchess.chess.engine.GameEngine
import de.nowchess.io.fen.{FenExporter, FenParser}
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
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 scalafx.stage.FileChooser
import scalafx.stage.FileChooser.ExtensionFilter
/** ScalaFX chess board view that displays the game state.
* Uses chess sprites and color palette.
* Handles user interactions (clicks) and sends moves to GameEngine.
/** ScalaFX chess board view that displays the game state. Uses chess sprites and color palette. Handles user
* interactions (clicks) and sends moves to GameEngine.
*/
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)
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
}
undoButton
},
{
}, {
redoButton = new Button("Redo") {
font = Font.font(comicSansFontFamily, 12)
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)
onAction = _ => engine.reset()
style = "-fx-background-radius: 8; -fx-background-color: #E1EAA9;"
}
},
)
},
new HBox {
@@ -126,7 +124,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
font = Font.font(comicSansFontFamily, 12)
onAction = _ => doPgnImport()
style = "-fx-background-radius: 8; -fx-background-color: #B9DAC4;"
}
},
)
},
new HBox {
@@ -142,9 +140,9 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
font = Font.font(comicSansFontFamily, 12)
onAction = _ => doJsonImport()
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
private def handleSquareClick(rank: Int, file: Int): Unit =
if engine.isPendingPromotion then
return // Don't allow moves during promotion
if engine.isPendingPromotion then return // Don't allow moves during promotion
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)
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 }
legalDests.foreach { sq =>
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(
engine.context,
selectedFile.toPath,
JsonExporter
JsonExporter,
)
result match
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
val result = FileSystemGameService.loadGameFromFile(
selectedFile.toPath,
JsonParser
JsonParser,
)
result match
case Right(gameContext) =>
@@ -353,7 +351,7 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
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 =>
importer.importGameContext(input) match
case Right(gameContext) =>
@@ -362,7 +360,6 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
case Left(err) =>
showMessage(s"⚠️ $formatName Error: $err")
}
}
private def showCopyDialog(title: String, content: String): Unit =
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.getButtonTypes.addAll(
javafx.scene.control.ButtonType.OK,
javafx.scene.control.ButtonType.CANCEL
javafx.scene.control.ButtonType.CANCEL,
)
dialog.setResultConverter { bt =>
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)
val result = dialog.showAndWait()
if result.isPresent && result.get != null && result.get.nonEmpty then Some(result.get) else None