From 52ae4244da07928f0fe612b9b30287329d518ed0 Mon Sep 17 00:00:00 2001 From: Janis Date: Sun, 12 Apr 2026 20:58:26 +0200 Subject: [PATCH] refactor: clean up code formatting and improve readability in ChessBoardView and related files --- .../de/nowchess/io/GameFileService.scala | 11 +- .../de/nowchess/io/json/JsonExporter.scala | 52 +++---- .../scala/de/nowchess/io/json/JsonModel.scala | 60 ++++---- .../de/nowchess/io/json/JsonParser.scala | 68 +++++----- .../de/nowchess/io/GameFileServiceSuite.scala | 57 ++++---- .../JsonExporterBranchCoverageSuite.scala | 44 +++--- .../nowchess/io/json/JsonExporterSuite.scala | 60 ++++---- .../io/json/JsonModelExtraTestSuite.scala | 4 +- .../io/json/JsonParserEdgeCasesSuite.scala | 25 ++-- .../json/JsonParserErrorHandlingSuite.scala | 10 +- .../io/json/JsonParserMoveTypeSuite.scala | 18 +-- .../de/nowchess/io/json/JsonParserSuite.scala | 91 +++++++------ .../de/nowchess/ui/gui/ChessBoardView.scala | 128 +++++++++--------- 13 files changed, 311 insertions(+), 317 deletions(-) diff --git a/modules/io/src/main/scala/de/nowchess/io/GameFileService.scala b/modules/io/src/main/scala/de/nowchess/io/GameFileService.scala index bd69ba5..f84d4fb 100644 --- a/modules/io/src/main/scala/de/nowchess/io/GameFileService.scala +++ b/modules/io/src/main/scala/de/nowchess/io/GameFileService.scala @@ -6,10 +6,9 @@ import java.nio.charset.StandardCharsets 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] def loadGameFromFile(path: Path, importer: GameContextImport): Either[String, GameContext] @@ -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, ) diff --git a/modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala b/modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala index 0524d46..44ba4cc 100644 --- a/modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala +++ b/modules/io/src/main/scala/de/nowchess/io/json/JsonExporter.scala @@ -8,31 +8,31 @@ 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. - * - * The JSON includes: - * - Game metadata (players, event, date, result) - * - Board state (all pieces and their positions) - * - Current game state (turn, castling rights, en passant, half-move clock) - * - Move history in both algebraic notation (PGN) and detailed move objects - * - Captured pieces tracking (which pieces have been removed) - * - Timestamp for record-keeping - */ + * + * The JSON includes: + * - Game metadata (players, event, date, result) + * - Board state (all pieces and their positions) + * - Current game state (turn, castling rights, en passant, half-move clock) + * - Move history in both algebraic notation (PGN) and detailed move objects + * - Captured pieces tracking (which pieces have been removed) + * - Timestamp for record-keeping + */ object JsonExporter extends GameContextExport: private val mapper = createMapper() - + private def createMapper(): ObjectMapper = val mapper = new ObjectMapper() .registerModule(DefaultScalaModule) - + // Configure pretty printer with custom spacing to match test expectations val indenter = new DefaultIndenter(" ", "\n") - val printer = new DefaultPrettyPrinter() + val printer = new DefaultPrettyPrinter() printer.indentArraysWith(indenter) printer.indentObjectsWith(indenter) - + mapper.setDefaultPrettyPrinter(printer) mapper.enable(SerializationFeature.INDENT_OUTPUT) mapper @@ -42,18 +42,19 @@ object JsonExporter extends GameContextExport: formatJson(mapper.writeValueAsString(record)) private def buildGameRecord(context: GameContext): JsonGameRecord = - val pgn = try { - Some(PgnExporter.exportGameContext(context)) - } catch { - case _: Exception => None - } + val pgn = + try + Some(PgnExporter.exportGameContext(context)) + catch { + case _: Exception => None + } JsonGameRecord( metadata = Some(buildMetadata()), gameState = Some(buildGameState(context)), 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] = @@ -128,12 +129,11 @@ object JsonExporter extends GameContextExport: val captured = Square.all.flatMap { square => initialBoard.pieceAt(square).flatMap { initialPiece => board.pieceAt(square) match - case None => Some(initialPiece) + case None => Some(initialPiece) case Some(_) => None } } - + val whiteCaptured = captured.filter(_.color == Color.White).map(_.pieceType.label).toList val blackCaptured = captured.filter(_.color == Color.Black).map(_.pieceType.label).toList (blackCaptured, whiteCaptured) - diff --git a/modules/io/src/main/scala/de/nowchess/io/json/JsonModel.scala b/modules/io/src/main/scala/de/nowchess/io/json/JsonModel.scala index 208140e..015a0fc 100644 --- a/modules/io/src/main/scala/de/nowchess/io/json/JsonModel.scala +++ b/modules/io/src/main/scala/de/nowchess/io/json/JsonModel.scala @@ -1,55 +1,55 @@ package de.nowchess.io.json case class JsonMetadata( - event: Option[String] = None, - players: Option[Map[String, String]] = None, - date: Option[String] = None, - result: Option[String] = None + event: Option[String] = None, + players: Option[Map[String, String]] = None, + date: Option[String] = None, + result: Option[String] = None, ) case class JsonPiece( - square: Option[String] = None, - color: Option[String] = None, - piece: Option[String] = None + square: Option[String] = None, + color: 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 + whiteKingSide: Option[Boolean] = None, + whiteQueenSide: Option[Boolean] = None, + blackKingSide: Option[Boolean] = None, + blackQueenSide: Option[Boolean] = None, ) case class JsonGameState( - board: Option[List[JsonPiece]] = None, - turn: Option[String] = None, - castlingRights: Option[JsonCastlingRights] = None, - enPassantSquare: Option[String] = None, - halfMoveClock: Option[Int] = None + board: Option[List[JsonPiece]] = None, + turn: Option[String] = None, + castlingRights: Option[JsonCastlingRights] = None, + enPassantSquare: Option[String] = None, + halfMoveClock: Option[Int] = None, ) case class JsonCapturedPieces( - byWhite: Option[List[String]] = None, - byBlack: Option[List[String]] = None + byWhite: Option[List[String]] = None, + byBlack: Option[List[String]] = None, ) case class JsonMoveType( - `type`: Option[String] = None, - isCapture: Option[Boolean] = None, - promotionPiece: Option[String] = None + `type`: Option[String] = None, + isCapture: Option[Boolean] = None, + promotionPiece: Option[String] = None, ) case class JsonMove( - from: Option[String] = None, - to: Option[String] = None, - `type`: Option[JsonMoveType] = None + from: Option[String] = None, + to: Option[String] = None, + `type`: Option[JsonMoveType] = None, ) case class JsonGameRecord( - metadata: Option[JsonMetadata] = None, - gameState: Option[JsonGameState] = None, - moveHistory: Option[String] = None, - moves: Option[List[JsonMove]] = None, - capturedPieces: Option[JsonCapturedPieces] = None, - timestamp: Option[String] = None + metadata: Option[JsonMetadata] = None, + gameState: Option[JsonGameState] = None, + moveHistory: Option[String] = None, + moves: Option[List[JsonMove]] = None, + capturedPieces: Option[JsonCapturedPieces] = None, + timestamp: Option[String] = None, ) diff --git a/modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala b/modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala index deefc4c..ca283a6 100644 --- a/modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala +++ b/modules/io/src/main/scala/de/nowchess/io/json/JsonParser.scala @@ -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} @@ -9,17 +9,17 @@ import de.nowchess.io.GameContextImport import scala.util.Try /** Imports a GameContext from JSON format using Jackson. - * - * Parses JSON exported by JsonExporter and reconstructs the GameContext including: - * - Board state - * - Current turn - * - Castling rights - * - En passant square - * - Half-move clock - * - Move history - * - * Returns Left(error message) if the JSON is malformed or invalid. - */ + * + * Parses JSON exported by JsonExporter and reconstructs the GameContext including: + * - Board state + * - Current turn + * - Castling rights + * - En passant square + * - Half-move clock + * - Move history + * + * Returns Left(error message) if the JSON is malformed or invalid. + */ object JsonParser extends GameContextImport: private val mapper = new ObjectMapper() @@ -27,20 +27,20 @@ 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 gs = data.gameState.getOrElse(JsonGameState()) val rawBoard = gs.board.getOrElse(Nil) - val rawTurn = gs.turn.getOrElse("White") - val rawCr = gs.castlingRights.getOrElse(JsonCastlingRights()) - val rawHmc = gs.halfMoveClock.getOrElse(0) + val rawTurn = gs.turn.getOrElse("White") + val rawCr = gs.castlingRights.getOrElse(JsonCastlingRights()) + val rawHmc = gs.halfMoveClock.getOrElse(0) val rawMoves = data.moves.getOrElse(Nil) for board <- parseBoard(rawBoard) - turn <- parseTurn(rawTurn) - castlingRights = parseCastlingRights(rawCr) + turn <- parseTurn(rawTurn) + castlingRights = parseCastlingRights(rawCr) enPassantSquare = gs.enPassantSquare.flatMap(s => Square.fromAlgebraic(s)) moves <- parseMoves(rawMoves) yield GameContext( @@ -49,16 +49,16 @@ object JsonParser extends GameContextImport: castlingRights = castlingRights, enPassantSquare = enPassantSquare, halfMoveClock = rawHmc, - moves = moves + moves = moves, ) } private def parseBoard(pieces: List[JsonPiece]): Either[String, Board] = val parsedPieces = pieces.flatMap { p => for - sq <- p.square.flatMap(Square.fromAlgebraic) + sq <- p.square.flatMap(Square.fromAlgebraic) color <- p.color.flatMap(parseColor) - pt <- p.piece.flatMap(parsePieceType) + pt <- p.piece.flatMap(parsePieceType) yield (sq, Piece(color, pt)) } Right(Board(parsedPieces.toMap)) @@ -73,27 +73,27 @@ object JsonParser extends GameContextImport: private def parsePieceType(pt: String): Option[PieceType] = pt match - case "Pawn" => Some(PieceType.Pawn) + case "Pawn" => Some(PieceType.Pawn) case "Knight" => Some(PieceType.Knight) case "Bishop" => Some(PieceType.Bishop) - case "Rook" => Some(PieceType.Rook) - case "Queen" => Some(PieceType.Queen) - case "King" => Some(PieceType.King) - case _ => None + case "Rook" => Some(PieceType.Rook) + case "Queen" => Some(PieceType.Queen) + case "King" => Some(PieceType.King) + case _ => None private def parseCastlingRights(cr: JsonCastlingRights): CastlingRights = CastlingRights( 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]] = Right(moves.flatMap { m => for - from <- m.from.flatMap(Square.fromAlgebraic) - to <- m.to.flatMap(Square.fromAlgebraic) + from <- m.from.flatMap(Square.fromAlgebraic) + to <- m.to.flatMap(Square.fromAlgebraic) moveType <- m.`type`.flatMap(parseMoveType) yield Move(from, to, moveType) }) @@ -110,10 +110,10 @@ object JsonParser extends GameContextImport: Some(MoveType.EnPassant) case Some("promotion") => val piece = mt.promotionPiece match - case Some("queen") => PromotionPiece.Queen - case Some("rook") => PromotionPiece.Rook + case Some("queen") => PromotionPiece.Queen + case Some("rook") => PromotionPiece.Rook case Some("bishop") => PromotionPiece.Bishop case Some("knight") => PromotionPiece.Knight - case _ => PromotionPiece.Queen // default + case _ => PromotionPiece.Queen // default Some(MoveType.Promotion(piece)) case _ => None diff --git a/modules/io/src/test/scala/de/nowchess/io/GameFileServiceSuite.scala b/modules/io/src/test/scala/de/nowchess/io/GameFileServiceSuite.scala index f4b06e9..8c0971f 100644 --- a/modules/io/src/test/scala/de/nowchess/io/GameFileServiceSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/GameFileServiceSuite.scala @@ -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} @@ -15,37 +15,35 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers: val tmpFile = Files.createTempFile("chess_test_", ".json") try val context = GameContext.initial - val result = FileSystemGameService.saveGameToFile(context, tmpFile, JsonExporter) - + val result = FileSystemGameService.saveGameToFile(context, tmpFile, JsonExporter) + 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") { val tmpFile = Files.createTempFile("chess_test_", ".json") try val originalContext = GameContext.initial - + // Save FileSystemGameService.saveGameToFile(originalContext, tmpFile, JsonExporter) - + // Load val result = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser) - + 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") { val nonExistentFile = Paths.get("/tmp/nonexistent_chess_game_file_12345.json") - val result = FileSystemGameService.loadGameFromFile(nonExistentFile, JsonParser) - + val result = FileSystemGameService.loadGameFromFile(nonExistentFile, JsonParser) + assert(result.isLeft) } @@ -57,16 +55,15 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers: val context = GameContext.initial .withMove(move1) .withMove(move2) - + val saveResult = FileSystemGameService.saveGameToFile(context, tmpFile, JsonExporter) assert(saveResult.isRight) - + val loadResult = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser) 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") { @@ -76,18 +73,17 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers: val context1 = GameContext.initial FileSystemGameService.saveGameToFile(context1, tmpFile, JsonExporter) val size1 = Files.size(tmpFile) - + // Write second file (should overwrite) - val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) val context2 = GameContext.initial.withMove(move) FileSystemGameService.saveGameToFile(context2, tmpFile, JsonExporter) - + val loadResult = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser) 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") { @@ -95,10 +91,9 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers: try Files.write(tmpFile, "{ invalid json}".getBytes()) 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") { @@ -110,16 +105,15 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers: .withMove(move1) .withMove(move2) .withHalfMoveClock(3) - + FileSystemGameService.saveGameToFile(original, tmpFile, JsonExporter) val loadResult = FileSystemGameService.loadGameFromFile(tmpFile, JsonParser) - + assert(loadResult.isRight) 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") { @@ -127,13 +121,12 @@ class GameFileServiceSuite extends AnyFunSuite with Matchers: try val context = GameContext.initial val faultyExporter = new GameContextExport { - def exportGameContext(c: GameContext): String = + def exportGameContext(c: GameContext): String = throw new RuntimeException("Export failed") } - + 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) } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala index 2ae9d59..e61cb1e 100644 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterBranchCoverageSuite.scala @@ -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,71 +13,71 @@ 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 => } } test("export normal non-capture move") { 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\"") + val ctx = GameContext.initial.copy(moves = List(quietMove)) + val json = JsonExporter.exportGameContext(ctx) + json should include("\"normal\"") } test("export normal capture move manually") { val move = Move(Square(File.E, Rank.R4), Square(File.D, Rank.R5), MoveType.Normal(true)) - val ctx = GameContext.initial.copy(moves = List(move)) + 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 => } } test("export all move type categories") { val move = Move(Square(File.D, Rank.R2), Square(File.D, Rank.R4)) - val ctx = GameContext.initial.copy(moves = List(move)) + 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") { val move = Move(Square(File.E, Rank.R1), Square(File.C, Rank.R1), MoveType.CastleQueenside) - val ctx = GameContext.initial.copy(moves = List(move)) + 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 => } } test("export castle kingside move") { val move = Move(Square(File.E, Rank.R1), Square(File.G, Rank.R1), MoveType.CastleKingside) - val ctx = GameContext.initial.copy(moves = List(move)) + 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 => } } test("export en passant move manually") { val move = Move(Square(File.E, Rank.R5), Square(File.D, Rank.R6), MoveType.EnPassant) - val ctx = GameContext.initial.copy(moves = List(move)) + 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 => } } diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala index 76f2500..0f7b70e 100644 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonExporterSuite.scala @@ -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 @@ -10,8 +10,8 @@ class JsonExporterSuite extends AnyFunSuite with Matchers: test("exportGameContext: exports initial position") { val context = GameContext.initial - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should include("\"metadata\"") json should include("\"gameState\"") json should include("\"moveHistory\"") @@ -21,8 +21,8 @@ class JsonExporterSuite extends AnyFunSuite with Matchers: test("exportGameContext: includes board pieces") { val context = GameContext.initial - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should include("\"a1\"") json should include("\"Rook\"") json should include("\"White\"") @@ -30,24 +30,24 @@ class JsonExporterSuite extends AnyFunSuite with Matchers: test("exportGameContext: includes turn information") { val context = GameContext.initial - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should include("\"turn\": \"White\"") } test("exportGameContext: includes castling rights") { val context = GameContext.initial - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should include("\"whiteKingSide\": true") json should include("\"whiteQueenSide\": true") } test("exportGameContext: exports with moves") { - val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) val context = GameContext.initial.withMove(move) - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should include("\"moves\"") json should include("\"from\"") json should include("\"to\"") @@ -57,8 +57,8 @@ class JsonExporterSuite extends AnyFunSuite with Matchers: test("exportGameContext: valid JSON structure") { val context = GameContext.initial - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should startWith("{") json should endWith("}") json should include("\"metadata\": {") @@ -67,47 +67,47 @@ class JsonExporterSuite extends AnyFunSuite with Matchers: test("exportGameContext: empty move history for initial position") { val context = GameContext.initial - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should include("\"moves\": []") } test("exportGameContext: exports en passant square") { val epSquare = Some(Square(File.E, Rank.R3)) - val context = GameContext.initial.copy(enPassantSquare = epSquare) - val json = JsonExporter.exportGameContext(context) - + val context = GameContext.initial.copy(enPassantSquare = epSquare) + val json = JsonExporter.exportGameContext(context) + json should include("\"enPassantSquare\": \"e3\"") } test("exportGameContext: exports null en passant square") { val context = GameContext.initial.copy(enPassantSquare = None) - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should include("\"enPassantSquare\": null") } test("exportGameContext: exports different move destinations") { - val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) val context = GameContext.initial.withMove(move) - val json = JsonExporter.exportGameContext(context) - + val json = JsonExporter.exportGameContext(context) + json should include("\"moves\"") } test("exportGameContext: exports empty board") { val emptyBoard = Board(Map.empty) - val context = GameContext.initial.copy(board = emptyBoard) - val json = JsonExporter.exportGameContext(context) - + val context = GameContext.initial.copy(board = emptyBoard) + val json = JsonExporter.exportGameContext(context) + json should include("\"board\": []") } test("exportGameContext: exports all castling rights disabled") { val noCastling = CastlingRights(false, false, false, false) - val context = GameContext.initial.withCastlingRights(noCastling) - val json = JsonExporter.exportGameContext(context) - + val context = GameContext.initial.withCastlingRights(noCastling) + val json = JsonExporter.exportGameContext(context) + json should include("\"whiteKingSide\": false") json should include("\"whiteQueenSide\": false") json should include("\"blackKingSide\": false") diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala index b14ea20..5307f21 100644 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonModelExtraTestSuite.scala @@ -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) diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserEdgeCasesSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserEdgeCasesSuite.scala index 8f7f717..0257ceb 100644 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserEdgeCasesSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserEdgeCasesSuite.scala @@ -8,7 +8,7 @@ import org.scalatest.matchers.should.Matchers class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: test("parse invalid turn color returns error") { - val json = """{ + val json = """{ "metadata": {}, "gameState": {"turn": "Invalid", "board": []}, "moves": [] @@ -19,7 +19,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: } test("parse invalid piece type filters it out") { - val json = """{ + val json = """{ "metadata": {}, "gameState": { "turn": "White", @@ -36,7 +36,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: } test("parse invalid color in board filters piece") { - val json = """{ + val json = """{ "metadata": {}, "gameState": { "turn": "White", @@ -53,7 +53,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: } test("parse with missing turn uses default") { - val json = """{ + val json = """{ "metadata": {}, "gameState": {"board": []}, "moves": [] @@ -65,7 +65,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: } test("parse with missing board uses empty") { - val json = """{ + val json = """{ "metadata": {}, "gameState": {"turn": "White"}, "moves": [] @@ -77,7 +77,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: } test("parse with missing moves uses empty list") { - val json = """{ + val json = """{ "metadata": {}, "gameState": {"turn": "White", "board": []} }""" @@ -88,7 +88,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: } test("parse invalid square in board filters it") { - val json = """{ + val json = """{ "metadata": {}, "gameState": { "turn": "White", @@ -105,7 +105,7 @@ class JsonParserEdgeCasesSuite extends AnyFunSuite with Matchers: } test("parse all valid piece types") { - val json = """{ + val json = """{ "metadata": {}, "gameState": { "turn": "White", @@ -124,11 +124,16 @@ 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") { - val json = """{ + val json = """{ "metadata": {}, "gameState": { "turn": "White", diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserErrorHandlingSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserErrorHandlingSuite.scala index d62e32c..6e296b4 100644 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserErrorHandlingSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserErrorHandlingSuite.scala @@ -8,7 +8,7 @@ class JsonParserErrorHandlingSuite extends AnyFunSuite with Matchers: test("parse completely invalid JSON returns error") { val invalidJson = "{ this is not valid json at all }" - val result = JsonParser.importGameContext(invalidJson) + val result = JsonParser.importGameContext(invalidJson) assert(result.isLeft) assert(result.left.toOption.get.contains("JSON parsing error")) } @@ -26,26 +26,26 @@ class JsonParserErrorHandlingSuite extends AnyFunSuite with Matchers: test("parse malformed JSON object returns error") { val malformed = """{"metadata": {"unclosed": """ - val result = JsonParser.importGameContext(malformed) + val result = JsonParser.importGameContext(malformed) assert(result.isLeft) assert(result.left.toOption.get.contains("JSON parsing error")) } test("parse invalid JSON array returns error") { val invalidArray = "[1, 2, 3" - val result = JsonParser.importGameContext(invalidArray) + val result = JsonParser.importGameContext(invalidArray) assert(result.isLeft) } test("parse JSON with missing required fields") { - val json = """{"metadata": {}}""" + val json = """{"metadata": {}}""" val result = JsonParser.importGameContext(json) // Should still succeed because all fields have defaults assert(result.isRight) } test("parse valid JSON with invalid turn falls back to default") { - val json = """{ + val json = """{ "metadata": {}, "gameState": {"turn": "White", "board": []}, "moves": [] diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala index d36ca21..0e47d10 100644 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserMoveTypeSuite.scala @@ -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 @@ -9,7 +9,7 @@ import org.scalatest.matchers.should.Matchers class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers: test("parse all move type variations") { - val json = """{ + val json = """{ "metadata": {"event": "Game", "result": "*"}, "gameState": {"turn": "White", "board": []}, "moves": [ @@ -34,7 +34,7 @@ class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers: } test("parse invalid move type defaults to None") { - val json = """{ + val json = """{ "metadata": {"event": "Game"}, "gameState": {"turn": "White", "board": []}, "moves": [{"from": "e2", "to": "e4", "type": {"type": "unknown"}}] @@ -45,7 +45,7 @@ class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers: } test("parse promotion with default piece") { - val json = """{ + val json = """{ "metadata": {}, "gameState": {"turn": "White", "board": []}, "moves": [{"from": "a7", "to": "a8", "type": {"type": "promotion", "promotionPiece": "invalid"}}] @@ -56,7 +56,7 @@ class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers: } test("parse move with missing from/to skips it") { - val json = """{ + val json = """{ "metadata": {}, "gameState": {"turn": "White", "board": []}, "moves": [{"from": "e2", "to": "invalid", "type": {"type": "normal"}}] @@ -69,26 +69,26 @@ class JsonParserMoveTypeSuite extends AnyFunSuite with Matchers: } test("parse with invalid JSON returns error") { - val json = """{"invalid json""" + val json = """{"invalid json""" val result = JsonParser.importGameContext(json) assert(result.isLeft) } test("parse normal move with isCapture true") { - val json = """{ + val json = """{ "metadata": {}, "gameState": {"turn": "White", "board": []}, "moves": [{"from": "e4", "to": "d5", "type": {"type": "normal", "isCapture": true}}] }""" val result = JsonParser.importGameContext(json) assert(result.isRight) - val ctx = result.toOption.get + val ctx = result.toOption.get val move = ctx.moves.head assert(move.moveType == MoveType.Normal(true)) } test("parse board with invalid pieces filters them") { - val json = """{ + val json = """{ "metadata": {}, "gameState": { "turn": "White", diff --git a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserSuite.scala b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserSuite.scala index a499b34..d787efa 100644 --- a/modules/io/src/test/scala/de/nowchess/io/json/JsonParserSuite.scala +++ b/modules/io/src/test/scala/de/nowchess/io/json/JsonParserSuite.scala @@ -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 @@ -9,39 +9,39 @@ import org.scalatest.matchers.should.Matchers class JsonParserSuite extends AnyFunSuite with Matchers: test("importGameContext: parses valid JSON") { - val json = JsonExporter.exportGameContext(GameContext.initial) + val json = JsonExporter.exportGameContext(GameContext.initial) val result = JsonParser.importGameContext(json) - + assert(result.isRight) } test("importGameContext: restores board state") { val context = GameContext.initial - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result == Right(context)) } test("importGameContext: restores turn") { val context = GameContext.initial.withTurn(Color.Black) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.turn) == Right(Color.Black)) } test("importGameContext: restores moves") { - val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) val context = GameContext.initial.withMove(move) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.moves.length) == Right(1)) } test("importGameContext: handles empty board") { - val json = """{ + val json = """{ "metadata": {"event": "Game", "players": {"white": "A", "black": "B"}, "date": "2026-04-06", "result": "*"}, "gameState": { "board": [], @@ -56,30 +56,31 @@ class JsonParserSuite extends AnyFunSuite with Matchers: "timestamp": "2026-04-06T00:00:00Z" }""" val result = JsonParser.importGameContext(json) - + assert(result.isRight) assert(result.map(_.board.pieces.isEmpty) == Right(true)) } test("importGameContext: returns error on invalid JSON") { val result = JsonParser.importGameContext("not valid json {{{") - + assert(result.isLeft) } 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) } test("importGameContext: handles castling rights") { val newCastling = GameContext.initial.castlingRights.copy(whiteKingSide = false) - val context = GameContext.initial.withCastlingRights(newCastling) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val context = GameContext.initial.withCastlingRights(newCastling) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.castlingRights.whiteKingSide) == Right(false)) } @@ -91,7 +92,7 @@ class JsonParserSuite extends AnyFunSuite with Matchers: .withMove(move2) .withTurn(Color.White) - val json = JsonExporter.exportGameContext(context) + val json = JsonExporter.exportGameContext(context) val restored = JsonParser.importGameContext(json) assert(restored.map(_.moves.length) == Right(2)) @@ -100,55 +101,55 @@ class JsonParserSuite extends AnyFunSuite with Matchers: test("importGameContext: handles half-move clock") { val context = GameContext.initial.withHalfMoveClock(5) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.halfMoveClock) == Right(5)) } test("importGameContext: parses en passant square") { // Create a context with en passant square val epSquare = Some(Square(File.E, Rank.R3)) - val context = GameContext.initial.copy(enPassantSquare = epSquare) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val context = GameContext.initial.copy(enPassantSquare = epSquare) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.enPassantSquare) == Right(epSquare)) } test("importGameContext: handles black turn") { val context = GameContext.initial.withTurn(Color.Black) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.turn) == Right(Color.Black)) } test("importGameContext: preserves basic moves in JSON round-trip") { // Use simple move without explicit moveType to let system handle it - val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) + val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) val context = GameContext.initial.withMove(move) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.isRight) assert(result.map(_.moves.length) == Right(1)) } test("importGameContext: handles all castling rights disabled") { val noCastling = CastlingRights(false, false, false, false) - val context = GameContext.initial.withCastlingRights(noCastling) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val context = GameContext.initial.withCastlingRights(noCastling) + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.castlingRights) == Right(noCastling)) } test("importGameContext: handles mixed castling rights") { - val mixed = CastlingRights(true, false, false, true) + val mixed = CastlingRights(true, false, false, true) val context = GameContext.initial.withCastlingRights(mixed) - val json = JsonExporter.exportGameContext(context) - val result = JsonParser.importGameContext(json) - + val json = JsonExporter.exportGameContext(context) + val result = JsonParser.importGameContext(json) + assert(result.map(_.castlingRights) == Right(mixed)) } diff --git a/modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala b/modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala index e2626a0..139ec87 100644 --- a/modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala +++ b/modules/ui/src/main/scala/de/nowchess/ui/gui/ChessBoardView.scala @@ -17,37 +17,36 @@ 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: - - private val squareSize = 70.0 + + private val squareSize = 70.0 private val comicSansFontFamily = "Comic Sans MS" - private val boardGrid = new GridPane() + private val boardGrid = new GridPane() private val messageLabel = new Label { text = "Welcome!" font = Font.font(comicSansFontFamily, 16) padding = Insets(10) } - - private var currentBoard: Board = engine.board - private var currentTurn: Color = engine.turn + + private var currentBoard: Board = engine.board + private var currentTurn: Color = engine.turn private var selectedSquare: Option[Square] = None - private val squareViews = scala.collection.mutable.Map[(Int, Int), StackPane]() - + private val squareViews = scala.collection.mutable.Map[(Int, Int), StackPane]() + private var undoButton: Button = uninitialized private var redoButton: Button = uninitialized // Initialize UI initializeBoard() - + top = new VBox { padding = Insets(10) spacing = 5 @@ -58,17 +57,17 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B font = Font.font(comicSansFontFamily, 24) style = "-fx-font-weight: bold;" }, - messageLabel + messageLabel, ) } - + center = new VBox { padding = Insets(20) alignment = Pos.Center style = s"-fx-background-color: ${PieceSprites.SquareColors.Border};" children = boardGrid } - + bottom = new VBox { padding = Insets(10) spacing = 8 @@ -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,17 +140,17 @@ 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;" - } + }, ) - } + }, ) } - + private def initializeBoard(): Unit = boardGrid.padding = Insets(5) boardGrid.hgap = 0 boardGrid.vgap = 0 - + // Create 8x8 board with rank/file labels for rank <- 0 until 8 @@ -161,13 +159,13 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B val square = createSquare(rank, file) squareViews((rank, file)) = square boardGrid.add(square, file, 7 - rank) // Flip rank for proper display - + updateBoard(currentBoard, currentTurn) - + private def createSquare(rank: Int, file: Int): StackPane = - val isWhite = (rank + file) % 2 == 0 + val isWhite = (rank + file) % 2 == 0 val baseColor = if isWhite then PieceSprites.SquareColors.White else PieceSprites.SquareColors.Black - + val bgRect = new Rectangle { width = squareSize height = squareSize @@ -175,21 +173,20 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B arcWidth = 8 arcHeight = 8 } - + val square = new StackPane { children = Seq(bgRect) onMouseClicked = _ => handleSquareClick(rank, file) style = "-fx-cursor: hand;" } - + 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)) - + selectedSquare match case None => // First click - select piece if it belongs to current player @@ -198,13 +195,14 @@ 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) - .collect { case move if move.from == clickedSquare => move.to } + 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) + highlightSquare(sq.rank.ordinal, sq.file.ordinal, PieceSprites.SquareColors.ValidMove) } } - + case Some(fromSquare) => // Second click - attempt move if clickedSquare == fromSquare then @@ -216,21 +214,21 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B val moveStr = s"${fromSquare}$clickedSquare" engine.processUserInput(moveStr) selectedSquare = None - + def updateBoard(board: Board, turn: Color): Unit = currentBoard = board currentTurn = turn selectedSquare = None - + // Update all squares for rank <- 0 until 8 file <- 0 until 8 do squareViews.get((rank, file)).foreach { stackPane => - val isWhite = (rank + file) % 2 == 0 + val isWhite = (rank + file) % 2 == 0 val baseColor = if isWhite then PieceSprites.SquareColors.White else PieceSprites.SquareColors.Black - + val bgRect = new Rectangle { width = squareSize height = squareSize @@ -238,16 +236,16 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B arcWidth = 8 arcHeight = 8 } - - val square = Square(File.values(file), Rank.values(rank)) + + val square = Square(File.values(file), Rank.values(rank)) val pieceOption = board.pieceAt(square) - + val children = pieceOption match case Some(piece) => Seq(bgRect, PieceSprites.loadPieceImage(piece, squareSize * 0.8)) case None => Seq(bgRect) - + stackPane.children = children } @@ -266,20 +264,20 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B arcWidth = 8 arcHeight = 8 } - - val square = Square(File.values(file), Rank.values(rank)) + + val square = Square(File.values(file), Rank.values(rank)) val pieceOption = currentBoard.pieceAt(square) - + stackPane.children = pieceOption match case Some(piece) => Seq(bgRect, PieceSprites.loadPieceImage(piece, squareSize * 0.8)) case None => Seq(bgRect) } - + def showMessage(msg: String): Unit = messageLabel.text = msg - + def showPromotionDialog(from: Square, to: Square): Unit = val choices = Seq("Queen", "Rook", "Bishop", "Knight") val dialog = new ChoiceDialog(defaultChoice = "Queen", choices = choices) { @@ -288,14 +286,14 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B headerText = "Choose promotion piece" contentText = "Promote to:" } - + val result = dialog.showAndWait() result match - case Some("Queen") => engine.completePromotion(PromotionPiece.Queen) - case Some("Rook") => engine.completePromotion(PromotionPiece.Rook) + case Some("Queen") => engine.completePromotion(PromotionPiece.Queen) + case Some("Rook") => engine.completePromotion(PromotionPiece.Rook) case Some("Bishop") => engine.completePromotion(PromotionPiece.Bishop) case Some("Knight") => engine.completePromotion(PromotionPiece.Knight) - case _ => engine.completePromotion(PromotionPiece.Queen) // Default + case _ => engine.completePromotion(PromotionPiece.Queen) // Default private def doFenExport(): Unit = doExport(FenExporter, "FEN") @@ -316,16 +314,16 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B extensionFilters.add(new ExtensionFilter("JSON files (*.json)", "*.json")) extensionFilters.add(new ExtensionFilter("All files", "*.*")) } - + val selectedFile = fileChooser.showSaveDialog(stage) if selectedFile != null then val result = FileSystemGameService.saveGameToFile( engine.context, selectedFile.toPath, - JsonExporter + JsonExporter, ) result match - case Right(_) => showMessage(s"✓ Game saved to: ${selectedFile.getName}") + case Right(_) => showMessage(s"✓ Game saved to: ${selectedFile.getName}") case Left(err) => showMessage(s"⚠️ Error saving file: $err") private def doJsonImport(): Unit = @@ -334,12 +332,12 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B extensionFilters.add(new ExtensionFilter("JSON files (*.json)", "*.json")) extensionFilters.add(new ExtensionFilter("All files", "*.*")) } - + val selectedFile = fileChooser.showOpenDialog(stage) 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 -