refactor: clean up code formatting and improve readability in ChessBoardView and related files
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user