style: fix CRLF line endings introduced by rebase
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,52 +6,52 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include
|
||||
case class PlayerInfoDto(id: String, displayName: String)
|
||||
|
||||
case class GameStateResponse(
|
||||
fen: String,
|
||||
pgn: String,
|
||||
turn: String,
|
||||
status: String,
|
||||
@JsonInclude(Include.NON_ABSENT) winner: Option[String],
|
||||
moves: List[String],
|
||||
undoAvailable: Boolean,
|
||||
redoAvailable: Boolean,
|
||||
fen: String,
|
||||
pgn: String,
|
||||
turn: String,
|
||||
status: String,
|
||||
@JsonInclude(Include.NON_ABSENT) winner: Option[String],
|
||||
moves: List[String],
|
||||
undoAvailable: Boolean,
|
||||
redoAvailable: Boolean,
|
||||
)
|
||||
|
||||
case class GameFullResponse(
|
||||
gameId: String,
|
||||
white: PlayerInfoDto,
|
||||
black: PlayerInfoDto,
|
||||
state: GameStateResponse,
|
||||
gameId: String,
|
||||
white: PlayerInfoDto,
|
||||
black: PlayerInfoDto,
|
||||
state: GameStateResponse,
|
||||
)
|
||||
|
||||
case class OkResponse(ok: Boolean = true)
|
||||
|
||||
@JsonInclude(Include.NON_ABSENT)
|
||||
case class ApiErrorResponse(
|
||||
code: String,
|
||||
message: String,
|
||||
field: Option[String] = None,
|
||||
code: String,
|
||||
message: String,
|
||||
field: Option[String] = None,
|
||||
)
|
||||
|
||||
// Requests
|
||||
case class CreateGameRequest(
|
||||
white: Option[PlayerInfoDto] = None,
|
||||
black: Option[PlayerInfoDto] = None,
|
||||
white: Option[PlayerInfoDto] = None,
|
||||
black: Option[PlayerInfoDto] = None,
|
||||
)
|
||||
|
||||
case class ImportFenRequest(
|
||||
fen: String = "",
|
||||
white: Option[PlayerInfoDto] = None,
|
||||
black: Option[PlayerInfoDto] = None,
|
||||
fen: String = "",
|
||||
white: Option[PlayerInfoDto] = None,
|
||||
black: Option[PlayerInfoDto] = None,
|
||||
)
|
||||
|
||||
case class ImportPgnRequest(pgn: String = "")
|
||||
|
||||
case class LegalMoveDto(
|
||||
from: String,
|
||||
to: String,
|
||||
uci: String,
|
||||
moveType: String,
|
||||
@JsonInclude(Include.NON_ABSENT) promotion: Option[String] = None,
|
||||
from: String,
|
||||
to: String,
|
||||
uci: String,
|
||||
moveType: String,
|
||||
@JsonInclude(Include.NON_ABSENT) promotion: Option[String] = None,
|
||||
)
|
||||
|
||||
case class LegalMovesResponse(moves: List[LegalMoveDto])
|
||||
|
||||
@@ -3,7 +3,7 @@ package de.nowchess.backcore.game
|
||||
import java.security.SecureRandom
|
||||
|
||||
object GameId:
|
||||
private val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
private val chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
private val random = SecureRandom()
|
||||
|
||||
def generate(): String =
|
||||
|
||||
@@ -12,7 +12,6 @@ import de.nowchess.rules.sets.DefaultRules
|
||||
object GameMapper:
|
||||
private val mapper = new ObjectMapper().registerModule(DefaultScalaModule)
|
||||
|
||||
|
||||
def toGameFullJson(session: GameSession): String =
|
||||
mapper.writeValueAsString(toGameFull(session))
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import de.nowchess.api.board.Color
|
||||
sealed trait GameResult
|
||||
object GameResult:
|
||||
case class Checkmate(winner: Color) extends GameResult
|
||||
case object Stalemate extends GameResult
|
||||
case class Resign(winner: Color) extends GameResult
|
||||
case object AgreedDraw extends GameResult
|
||||
case object FiftyMoveDraw extends GameResult
|
||||
case object InsufficientMaterial extends GameResult
|
||||
case object Stalemate extends GameResult
|
||||
case class Resign(winner: Color) extends GameResult
|
||||
case object AgreedDraw extends GameResult
|
||||
case object FiftyMoveDraw extends GameResult
|
||||
case object InsufficientMaterial extends GameResult
|
||||
|
||||
@@ -6,11 +6,11 @@ import de.nowchess.api.player.PlayerInfo
|
||||
import de.nowchess.chess.command.CommandInvoker
|
||||
|
||||
case class GameSession(
|
||||
gameId: String,
|
||||
white: PlayerInfo,
|
||||
black: PlayerInfo,
|
||||
context: GameContext,
|
||||
invoker: CommandInvoker,
|
||||
drawOfferedBy: Option[Color] = None,
|
||||
result: Option[GameResult] = None,
|
||||
gameId: String,
|
||||
white: PlayerInfo,
|
||||
black: PlayerInfo,
|
||||
context: GameContext,
|
||||
invoker: CommandInvoker,
|
||||
drawOfferedBy: Option[Color] = None,
|
||||
result: Option[GameResult] = None,
|
||||
)
|
||||
|
||||
@@ -41,11 +41,11 @@ class GameStore:
|
||||
findMatchingMove(legalCandidates, to, promotion) match
|
||||
case None => Left(s"$uci is not a legal move")
|
||||
case Some(move) =>
|
||||
val nextCtx = DefaultRules.applyMove(session.context)(move)
|
||||
val prevCtx = session.context
|
||||
val nextCtx = DefaultRules.applyMove(session.context)(move)
|
||||
val prevCtx = session.context
|
||||
val cmd = MoveCommand(
|
||||
from = move.from,
|
||||
to = move.to,
|
||||
to = move.to,
|
||||
moveResult = Some(MoveResult.Successful(nextCtx, prevCtx.board.pieceAt(move.to))),
|
||||
previousContext = Some(prevCtx),
|
||||
)
|
||||
@@ -163,7 +163,7 @@ class GameStore:
|
||||
val session = newSession(id, white, black, GameContext.initial)
|
||||
replayIntoSession(session, game.moves, GameContext.initial) match
|
||||
case Left(err) => Left(err)
|
||||
case Right(s) =>
|
||||
case Right(s) =>
|
||||
games(id) = s
|
||||
Right(s)
|
||||
|
||||
@@ -180,15 +180,15 @@ class GameStore:
|
||||
id
|
||||
|
||||
private def newSession(
|
||||
id: String,
|
||||
white: Option[PlayerInfoDto],
|
||||
black: Option[PlayerInfoDto],
|
||||
ctx: GameContext,
|
||||
id: String,
|
||||
white: Option[PlayerInfoDto],
|
||||
black: Option[PlayerInfoDto],
|
||||
ctx: GameContext,
|
||||
): GameSession =
|
||||
GameSession(
|
||||
gameId = id,
|
||||
white = toPlayerInfo(white, "white", "White"),
|
||||
black = toPlayerInfo(black, "black", "Black"),
|
||||
gameId = id,
|
||||
white = toPlayerInfo(white, "white", "White"),
|
||||
black = toPlayerInfo(black, "black", "Black"),
|
||||
context = ctx,
|
||||
invoker = new CommandInvoker(),
|
||||
)
|
||||
@@ -215,17 +215,19 @@ class GameStore:
|
||||
case _ => None
|
||||
|
||||
private def findMatchingMove(
|
||||
candidates: List[Move],
|
||||
to: Square,
|
||||
promotion: Option[PromotionPiece],
|
||||
candidates: List[Move],
|
||||
to: Square,
|
||||
promotion: Option[PromotionPiece],
|
||||
): Option[Move] =
|
||||
candidates.filter(_.to == to) match
|
||||
case Nil => None
|
||||
case moves =>
|
||||
promotion match
|
||||
case Some(pp) => moves.find(_.moveType == MoveType.Promotion(pp))
|
||||
case None => moves.find(m => !m.moveType.isInstanceOf[MoveType.Promotion])
|
||||
.orElse(moves.headOption)
|
||||
case None =>
|
||||
moves
|
||||
.find(m => !m.moveType.isInstanceOf[MoveType.Promotion])
|
||||
.orElse(moves.headOption)
|
||||
|
||||
private def detectGameOver(ctx: GameContext): Option[GameResult] =
|
||||
if DefaultRules.isCheckmate(ctx) then Some(GameResult.Checkmate(ctx.turn.opposite))
|
||||
@@ -234,22 +236,23 @@ class GameStore:
|
||||
else None
|
||||
|
||||
private def replayIntoSession(
|
||||
session: GameSession,
|
||||
moves: List[Move],
|
||||
startCtx: GameContext,
|
||||
session: GameSession,
|
||||
moves: List[Move],
|
||||
startCtx: GameContext,
|
||||
): Either[String, GameSession] =
|
||||
moves.foldLeft[Either[String, GameSession]](Right(session)):
|
||||
case (Left(err), _) => Left(err)
|
||||
case (Right(s), move) =>
|
||||
val legal = DefaultRules.legalMoves(s.context)(move.from)
|
||||
legal.find(m => m.from == move.from && m.to == move.to && m.moveType == move.moveType)
|
||||
legal
|
||||
.find(m => m.from == move.from && m.to == move.to && m.moveType == move.moveType)
|
||||
.orElse(legal.find(m => m.from == move.from && m.to == move.to)) match
|
||||
case None => Left(s"Illegal move in PGN: $move")
|
||||
case Some(legalMove) =>
|
||||
val nextCtx = DefaultRules.applyMove(s.context)(legalMove)
|
||||
val cmd = MoveCommand(
|
||||
from = legalMove.from,
|
||||
to = legalMove.to,
|
||||
to = legalMove.to,
|
||||
moveResult = Some(MoveResult.Successful(nextCtx, s.context.board.pieceAt(legalMove.to))),
|
||||
previousContext = Some(s.context),
|
||||
)
|
||||
|
||||
@@ -24,7 +24,8 @@ class GameResource @Inject() (store: GameStore):
|
||||
store.get(gameId) match
|
||||
case Some(session) => Response.ok(GameMapper.toGameFull(session)).build()
|
||||
case None =>
|
||||
Response.status(404)
|
||||
Response
|
||||
.status(404)
|
||||
.entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found"))
|
||||
.build()
|
||||
|
||||
@@ -34,7 +35,8 @@ class GameResource @Inject() (store: GameStore):
|
||||
def streamGame(@PathParam("gameId") gameId: String): Response =
|
||||
store.get(gameId) match
|
||||
case None =>
|
||||
Response.status(404)
|
||||
Response
|
||||
.status(404)
|
||||
.`type`(MediaType.APPLICATION_JSON)
|
||||
.entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found"))
|
||||
.build()
|
||||
@@ -56,8 +58,8 @@ class GameResource @Inject() (store: GameStore):
|
||||
@POST
|
||||
@Path("/{gameId}/draw/{action}")
|
||||
def drawAction(
|
||||
@PathParam("gameId") gameId: String,
|
||||
@PathParam("action") action: String,
|
||||
@PathParam("gameId") gameId: String,
|
||||
@PathParam("action") action: String,
|
||||
): Response =
|
||||
store.drawAction(gameId, action) match
|
||||
case Right(_) => Response.ok(OkResponse()).build()
|
||||
@@ -72,7 +74,8 @@ class GameResource @Inject() (store: GameStore):
|
||||
def exportFen(@PathParam("gameId") gameId: String): Response =
|
||||
store.get(gameId) match
|
||||
case None =>
|
||||
Response.status(404)
|
||||
Response
|
||||
.status(404)
|
||||
.`type`(MediaType.APPLICATION_JSON)
|
||||
.entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found"))
|
||||
.build()
|
||||
@@ -86,7 +89,8 @@ class GameResource @Inject() (store: GameStore):
|
||||
def exportPgn(@PathParam("gameId") gameId: String): Response =
|
||||
store.get(gameId) match
|
||||
case None =>
|
||||
Response.status(404)
|
||||
Response
|
||||
.status(404)
|
||||
.`type`(MediaType.APPLICATION_JSON)
|
||||
.entity(ApiErrorResponse("GAME_NOT_FOUND", s"Game $gameId not found"))
|
||||
.build()
|
||||
|
||||
@@ -17,8 +17,8 @@ class MoveResource @Inject() (store: GameStore):
|
||||
@POST
|
||||
@Path("/{gameId}/move/{uci}")
|
||||
def makeMove(
|
||||
@PathParam("gameId") gameId: String,
|
||||
@PathParam("uci") uci: String,
|
||||
@PathParam("gameId") gameId: String,
|
||||
@PathParam("uci") uci: String,
|
||||
): Response =
|
||||
store.applyMove(gameId, uci) match
|
||||
case Right(session) => Response.ok(GameMapper.toGameState(session)).build()
|
||||
@@ -30,8 +30,8 @@ class MoveResource @Inject() (store: GameStore):
|
||||
@GET
|
||||
@Path("/{gameId}/moves")
|
||||
def getLegalMoves(
|
||||
@PathParam("gameId") gameId: String,
|
||||
@QueryParam("square") squareParam: String,
|
||||
@PathParam("gameId") gameId: String,
|
||||
@QueryParam("square") squareParam: String,
|
||||
): Response =
|
||||
val square = Option(squareParam).flatMap(Square.fromAlgebraic)
|
||||
store.legalMoves(gameId, square) match
|
||||
@@ -66,12 +66,12 @@ class MoveResource @Inject() (store: GameStore):
|
||||
private def toLegalMoveDto(move: Move): LegalMoveDto =
|
||||
val uci = GameMapper.moveToUci(move)
|
||||
val (moveType, promotion) = move.moveType match
|
||||
case MoveType.Normal(true) => ("capture", None)
|
||||
case MoveType.Normal(false) => ("normal", None)
|
||||
case MoveType.CastleKingside => ("castleKingside", None)
|
||||
case MoveType.CastleQueenside => ("castleQueenside", None)
|
||||
case MoveType.EnPassant => ("enPassant", None)
|
||||
case MoveType.Promotion(pp) =>
|
||||
case MoveType.Normal(true) => ("capture", None)
|
||||
case MoveType.Normal(false) => ("normal", None)
|
||||
case MoveType.CastleKingside => ("castleKingside", None)
|
||||
case MoveType.CastleQueenside => ("castleQueenside", None)
|
||||
case MoveType.EnPassant => ("enPassant", None)
|
||||
case MoveType.Promotion(pp) =>
|
||||
val pName = pp match
|
||||
case PromotionPiece.Queen => "queen"
|
||||
case PromotionPiece.Rook => "rook"
|
||||
@@ -79,9 +79,9 @@ class MoveResource @Inject() (store: GameStore):
|
||||
case PromotionPiece.Knight => "knight"
|
||||
("promotion", Some(pName))
|
||||
LegalMoveDto(
|
||||
from = move.from.toString,
|
||||
to = move.to.toString,
|
||||
uci = uci,
|
||||
moveType = moveType,
|
||||
from = move.from.toString,
|
||||
to = move.to.toString,
|
||||
uci = uci,
|
||||
moveType = moveType,
|
||||
promotion = promotion,
|
||||
)
|
||||
|
||||
+10
-5
@@ -10,7 +10,8 @@ class GameResourceTest:
|
||||
|
||||
@Test
|
||||
def createGameReturns201WithGameId(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("{}")
|
||||
.when()
|
||||
@@ -24,7 +25,8 @@ class GameResourceTest:
|
||||
|
||||
@Test
|
||||
def createGameWithPlayersReturns201(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("""{"white":{"id":"p1","displayName":"Alice"},"black":{"id":"p2","displayName":"Bob"}}""")
|
||||
.when()
|
||||
@@ -36,7 +38,8 @@ class GameResourceTest:
|
||||
|
||||
@Test
|
||||
def getGameReturns200ForExistingGame(): Unit =
|
||||
val gameId = RestAssured.`given`()
|
||||
val gameId = RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("{}")
|
||||
.when()
|
||||
@@ -46,7 +49,8 @@ class GameResourceTest:
|
||||
.extract()
|
||||
.path[String]("gameId")
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId")
|
||||
.`then`()
|
||||
@@ -55,7 +59,8 @@ class GameResourceTest:
|
||||
|
||||
@Test
|
||||
def getGameReturns404ForUnknownId(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get("/api/board/game/XXXXXXXX")
|
||||
.`then`()
|
||||
|
||||
+28
-14
@@ -14,7 +14,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def importFenReturns201WithCorrectPosition(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body(s"""{"fen":"$startFen"}""")
|
||||
.when()
|
||||
@@ -28,7 +29,8 @@ class ImportExportTest:
|
||||
@Test
|
||||
def importFenWithCustomPositionWorks(): Unit =
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body(s"""{"fen":"$fen"}""")
|
||||
.when()
|
||||
@@ -40,7 +42,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def importFenWithInvalidFenReturns400(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("""{"fen":"not-a-fen"}""")
|
||||
.when()
|
||||
@@ -52,7 +55,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def importPgnReturns201(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("""{"pgn":"1. e4 e5 2. Nf3 Nc6 *"}""")
|
||||
.when()
|
||||
@@ -64,7 +68,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def importPgnWithInvalidPgnReturns400(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("""{"pgn":"1. z9 *"}""")
|
||||
.when()
|
||||
@@ -76,7 +81,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def exportFenReturnsStartingFen(): Unit =
|
||||
val gameId = RestAssured.`given`()
|
||||
val gameId = RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("{}")
|
||||
.when()
|
||||
@@ -86,7 +92,8 @@ class ImportExportTest:
|
||||
.extract()
|
||||
.path[String]("gameId")
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId/export/fen")
|
||||
.`then`()
|
||||
@@ -95,7 +102,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def exportFenOnUnknownGameReturns404(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get("/api/board/game/XXXXXXXX/export/fen")
|
||||
.`then`()
|
||||
@@ -105,7 +113,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def exportPgnReturnsText(): Unit =
|
||||
val gameId = RestAssured.`given`()
|
||||
val gameId = RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("{}")
|
||||
.when()
|
||||
@@ -115,13 +124,15 @@ class ImportExportTest:
|
||||
.extract()
|
||||
.path[String]("gameId")
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/move/e2e4")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId/export/pgn")
|
||||
.`then`()
|
||||
@@ -132,7 +143,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def streamReturnsNdjsonSnapshot(): Unit =
|
||||
val gameId = RestAssured.`given`()
|
||||
val gameId = RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("{}")
|
||||
.when()
|
||||
@@ -142,7 +154,8 @@ class ImportExportTest:
|
||||
.extract()
|
||||
.path[String]("gameId")
|
||||
|
||||
val body = RestAssured.`given`()
|
||||
val body = RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId/stream")
|
||||
.`then`()
|
||||
@@ -156,7 +169,8 @@ class ImportExportTest:
|
||||
|
||||
@Test
|
||||
def streamOnUnknownGameReturns404(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get("/api/board/game/XXXXXXXX/stream")
|
||||
.`then`()
|
||||
|
||||
+16
-9
@@ -9,7 +9,8 @@ import org.junit.jupiter.api.Test
|
||||
class MoveResourceTest:
|
||||
|
||||
private def createGame(): String =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("{}")
|
||||
.when()
|
||||
@@ -22,18 +23,20 @@ class MoveResourceTest:
|
||||
@Test
|
||||
def makeMoveReturns200WithUpdatedFen(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/move/e2e4")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("fen", containsString("4P3")) // e4 pawn present in FEN
|
||||
.body("fen", containsString("4P3")) // e4 pawn present in FEN
|
||||
.body("turn", equalTo("black"))
|
||||
.body("moves", hasItem("e2e4"))
|
||||
|
||||
@Test
|
||||
def makeMoveOnUnknownGameReturns404(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post("/api/board/game/XXXXXXXX/move/e2e4")
|
||||
.`then`()
|
||||
@@ -42,16 +45,18 @@ class MoveResourceTest:
|
||||
@Test
|
||||
def illegalMoveReturns400(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/move/e2e5") // illegal — pawns can't jump 3 squares
|
||||
.post(s"/api/board/game/$gameId/move/e2e5") // illegal — pawns can't jump 3 squares
|
||||
.`then`()
|
||||
.statusCode(400)
|
||||
|
||||
@Test
|
||||
def getLegalMovesReturnsNonEmptyList(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId/moves")
|
||||
.`then`()
|
||||
@@ -61,7 +66,8 @@ class MoveResourceTest:
|
||||
@Test
|
||||
def getLegalMovesFilteredBySquareReturnsCorrectMoves(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId/moves?square=e2")
|
||||
.`then`()
|
||||
@@ -70,7 +76,8 @@ class MoveResourceTest:
|
||||
|
||||
@Test
|
||||
def getLegalMovesOnUnknownGameReturns404(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get("/api/board/game/XXXXXXXX/moves")
|
||||
.`then`()
|
||||
|
||||
+32
-16
@@ -9,7 +9,8 @@ import org.junit.jupiter.api.Test
|
||||
class ResignDrawTest:
|
||||
|
||||
private def createGame(): String =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("{}")
|
||||
.when()
|
||||
@@ -24,7 +25,8 @@ class ResignDrawTest:
|
||||
@Test
|
||||
def resignReturns200(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/resign")
|
||||
.`then`()
|
||||
@@ -34,13 +36,15 @@ class ResignDrawTest:
|
||||
@Test
|
||||
def afterResignGameShowsResignStatusAndWinner(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/resign")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId")
|
||||
.`then`()
|
||||
@@ -50,7 +54,8 @@ class ResignDrawTest:
|
||||
|
||||
@Test
|
||||
def resignOnUnknownGameReturns404(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post("/api/board/game/XXXXXXXX/resign")
|
||||
.`then`()
|
||||
@@ -61,14 +66,16 @@ class ResignDrawTest:
|
||||
@Test
|
||||
def offerDrawSetsDrawOfferedStatus(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/draw/offer")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("ok", equalTo(true))
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId")
|
||||
.`then`()
|
||||
@@ -79,7 +86,8 @@ class ResignDrawTest:
|
||||
def acceptDrawAfterOfferSetsDrawStatus(): Unit =
|
||||
val gameId = createGame()
|
||||
// White offers
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/draw/offer")
|
||||
.`then`()
|
||||
@@ -90,21 +98,24 @@ class ResignDrawTest:
|
||||
// The GameStore checks drawOfferedBy != turn to allow accept.
|
||||
// White offered on white's turn, so black needs to accept — but current turn is still white.
|
||||
// We need to make a move first to switch turns.
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/move/e2e4")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
|
||||
// Now it's black's turn and white offered the draw — black accepts
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/draw/accept")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("ok", equalTo(true))
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId")
|
||||
.`then`()
|
||||
@@ -114,20 +125,23 @@ class ResignDrawTest:
|
||||
@Test
|
||||
def declineDrawClearsOffer(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/draw/offer")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/draw/decline")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
.body("ok", equalTo(true))
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.get(s"/api/board/game/$gameId")
|
||||
.`then`()
|
||||
@@ -137,7 +151,8 @@ class ResignDrawTest:
|
||||
@Test
|
||||
def acceptWithoutOfferReturns400(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/draw/accept")
|
||||
.`then`()
|
||||
@@ -145,7 +160,8 @@ class ResignDrawTest:
|
||||
|
||||
@Test
|
||||
def drawOnUnknownGameReturns404(): Unit =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post("/api/board/game/XXXXXXXX/draw/offer")
|
||||
.`then`()
|
||||
|
||||
@@ -11,7 +11,8 @@ class UndoRedoTest:
|
||||
private val initialFen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||
|
||||
private def createGame(): String =
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.contentType("application/json")
|
||||
.body("{}")
|
||||
.when()
|
||||
@@ -24,13 +25,15 @@ class UndoRedoTest:
|
||||
@Test
|
||||
def undoAfterMoveRestoresOriginalPosition(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/move/e2e4")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/undo")
|
||||
.`then`()
|
||||
@@ -41,19 +44,22 @@ class UndoRedoTest:
|
||||
@Test
|
||||
def redoAfterUndoRestoresMovedPosition(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/move/e2e4")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/undo")
|
||||
.`then`()
|
||||
.statusCode(200)
|
||||
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/redo")
|
||||
.`then`()
|
||||
@@ -64,7 +70,8 @@ class UndoRedoTest:
|
||||
@Test
|
||||
def undoWithNoHistoryReturns400(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/undo")
|
||||
.`then`()
|
||||
@@ -73,7 +80,8 @@ class UndoRedoTest:
|
||||
@Test
|
||||
def redoWithNoRedoStackReturns400(): Unit =
|
||||
val gameId = createGame()
|
||||
RestAssured.`given`()
|
||||
RestAssured
|
||||
.`given`()
|
||||
.when()
|
||||
.post(s"/api/board/game/$gameId/redo")
|
||||
.`then`()
|
||||
|
||||
+2
-2
@@ -18,8 +18,8 @@ class CommandInvokerBranchTest extends AnyFunSuite with Matchers:
|
||||
initialShouldFailOnUndo: Boolean = false,
|
||||
initialShouldFailOnExecute: Boolean = false,
|
||||
) extends Command:
|
||||
val shouldFailOnUndo = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnUndo)
|
||||
val shouldFailOnExecute = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnExecute)
|
||||
val shouldFailOnUndo = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnUndo)
|
||||
val shouldFailOnExecute = new java.util.concurrent.atomic.AtomicBoolean(initialShouldFailOnExecute)
|
||||
override def execute(): Boolean = !shouldFailOnExecute.get()
|
||||
override def undo(): Boolean = !shouldFailOnUndo.get()
|
||||
override def description: String = "Conditional fail"
|
||||
|
||||
+3
-1
@@ -69,7 +69,9 @@ class GameEngineIntegrationTest extends AnyFunSuite with Matchers:
|
||||
|
||||
engine.context shouldBe target
|
||||
engine.commandHistory shouldBe empty
|
||||
events.lastOption.exists { case _: de.nowchess.chess.observer.BoardResetEvent => true; case _ => false } shouldBe true
|
||||
events.lastOption.exists {
|
||||
case _: de.nowchess.chess.observer.BoardResetEvent => true; case _ => false
|
||||
} shouldBe true
|
||||
|
||||
test("redo event includes captured piece description when replaying a capture"):
|
||||
val engine = new GameEngine()
|
||||
|
||||
@@ -70,58 +70,66 @@ class FenParserFastParseTest extends AnyFunSuite with Matchers:
|
||||
FenParserFastParse.parseBoard("8pp/8/8/8/8/8/8/8") shouldBe None
|
||||
|
||||
test("parseFen handles all individual castling rights"):
|
||||
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w K - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.castlingRights.whiteKingSide shouldBe true
|
||||
ctx.castlingRights.whiteQueenSide shouldBe false
|
||||
ctx.castlingRights.blackKingSide shouldBe false
|
||||
ctx.castlingRights.blackQueenSide shouldBe false
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w K - 0 1")
|
||||
.fold(
|
||||
_ => fail(),
|
||||
ctx =>
|
||||
ctx.castlingRights.whiteKingSide shouldBe true
|
||||
ctx.castlingRights.whiteQueenSide shouldBe false
|
||||
ctx.castlingRights.blackKingSide shouldBe false
|
||||
ctx.castlingRights.blackQueenSide shouldBe false,
|
||||
)
|
||||
|
||||
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Q - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.castlingRights.whiteQueenSide shouldBe true
|
||||
ctx.castlingRights.whiteKingSide shouldBe false
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Q - 0 1")
|
||||
.fold(
|
||||
_ => fail(),
|
||||
ctx =>
|
||||
ctx.castlingRights.whiteQueenSide shouldBe true
|
||||
ctx.castlingRights.whiteKingSide shouldBe false,
|
||||
)
|
||||
|
||||
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w k - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.castlingRights.blackKingSide shouldBe true
|
||||
ctx.castlingRights.whiteKingSide shouldBe false
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w k - 0 1")
|
||||
.fold(
|
||||
_ => fail(),
|
||||
ctx =>
|
||||
ctx.castlingRights.blackKingSide shouldBe true
|
||||
ctx.castlingRights.whiteKingSide shouldBe false,
|
||||
)
|
||||
|
||||
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w q - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.castlingRights.blackQueenSide shouldBe true
|
||||
ctx.castlingRights.whiteKingSide shouldBe false
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w q - 0 1")
|
||||
.fold(
|
||||
_ => fail(),
|
||||
ctx =>
|
||||
ctx.castlingRights.blackQueenSide shouldBe true
|
||||
ctx.castlingRights.whiteKingSide shouldBe false,
|
||||
)
|
||||
|
||||
test("parseFen parses all en passant squares"):
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - a3 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.enPassantSquare shouldBe Some(Square(File.A, Rank.R3))
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen("8/8/8/8/8/8/8/8 w - a3 0 1")
|
||||
.fold(_ => fail(), ctx => ctx.enPassantSquare shouldBe Some(Square(File.A, Rank.R3)))
|
||||
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - h6 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.enPassantSquare shouldBe Some(Square(File.H, Rank.R6))
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen("8/8/8/8/8/8/8/8 w - h6 0 1")
|
||||
.fold(_ => fail(), ctx => ctx.enPassantSquare shouldBe Some(Square(File.H, Rank.R6)))
|
||||
|
||||
test("parseFen parses different halfMove and fullMove clocks"):
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 5 10").fold(_ => fail(), ctx =>
|
||||
ctx.halfMoveClock shouldBe 5
|
||||
)
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 5 10").fold(_ => fail(), ctx => ctx.halfMoveClock shouldBe 5)
|
||||
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 0 100").fold(_ => fail(), ctx =>
|
||||
ctx.halfMoveClock shouldBe 0
|
||||
)
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 0 100").fold(_ => fail(), ctx => ctx.halfMoveClock shouldBe 0)
|
||||
|
||||
test("parseBoard parses boards with mixed empty and piece tokens"):
|
||||
val mixed = "8/1p1p1p1p/8/1P1P1P1P/8/8/8/8"
|
||||
FenParserFastParse.parseBoard(mixed) should not be empty
|
||||
|
||||
test("parseFen handles turn transitions"):
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.turn shouldBe Color.White
|
||||
)
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w - - 0 1").fold(_ => fail(), ctx => ctx.turn shouldBe Color.White)
|
||||
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 b - - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.turn shouldBe Color.Black
|
||||
)
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 b - - 0 1").fold(_ => fail(), ctx => ctx.turn shouldBe Color.Black)
|
||||
|
||||
test("parseFen rejects invalid piece characters"):
|
||||
FenParserFastParse.parseFen("8x/8/8/8/8/8/8/8 w - - 0 1").isLeft shouldBe true
|
||||
@@ -133,7 +141,7 @@ class FenParserFastParseTest extends AnyFunSuite with Matchers:
|
||||
test("parseBoard tests all piece types in various positions"):
|
||||
// Test each piece type: pawn, rook, knight, bishop, queen, king (both colors)
|
||||
val allPieces = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
|
||||
val parsed = FenParserFastParse.parseBoard(allPieces)
|
||||
val parsed = FenParserFastParse.parseBoard(allPieces)
|
||||
parsed.map(_.pieces.size) shouldBe Some(32)
|
||||
parsed.map(_.pieceAt(Square(File.A, Rank.R8))) shouldBe Some(Some(Piece.BlackRook))
|
||||
parsed.map(_.pieceAt(Square(File.B, Rank.R8))) shouldBe Some(Some(Piece.BlackKnight))
|
||||
@@ -150,25 +158,33 @@ class FenParserFastParseTest extends AnyFunSuite with Matchers:
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 b - - 0 1").fold(_ => fail(), _.turn shouldBe Color.Black)
|
||||
|
||||
test("parseFen tests all castling combinations"):
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w KQkq - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.castlingRights.whiteKingSide shouldBe true
|
||||
ctx.castlingRights.whiteQueenSide shouldBe true
|
||||
ctx.castlingRights.blackKingSide shouldBe true
|
||||
ctx.castlingRights.blackQueenSide shouldBe true
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen("8/8/8/8/8/8/8/8 w KQkq - 0 1")
|
||||
.fold(
|
||||
_ => fail(),
|
||||
ctx =>
|
||||
ctx.castlingRights.whiteKingSide shouldBe true
|
||||
ctx.castlingRights.whiteQueenSide shouldBe true
|
||||
ctx.castlingRights.blackKingSide shouldBe true
|
||||
ctx.castlingRights.blackQueenSide shouldBe true,
|
||||
)
|
||||
|
||||
FenParserFastParse.parseFen("8/8/8/8/8/8/8/8 w Kq - 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.castlingRights.whiteKingSide shouldBe true
|
||||
ctx.castlingRights.whiteQueenSide shouldBe false
|
||||
ctx.castlingRights.blackKingSide shouldBe false
|
||||
ctx.castlingRights.blackQueenSide shouldBe true
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen("8/8/8/8/8/8/8/8 w Kq - 0 1")
|
||||
.fold(
|
||||
_ => fail(),
|
||||
ctx =>
|
||||
ctx.castlingRights.whiteKingSide shouldBe true
|
||||
ctx.castlingRights.whiteQueenSide shouldBe false
|
||||
ctx.castlingRights.blackKingSide shouldBe false
|
||||
ctx.castlingRights.blackQueenSide shouldBe true,
|
||||
)
|
||||
|
||||
test("parseFen tests all en passant files"):
|
||||
for file <- Seq("a", "b", "c", "d", "e", "f", "g", "h") do
|
||||
FenParserFastParse.parseFen(s"8/8/8/8/8/8/8/8 w - ${file}3 0 1").fold(_ => fail(), ctx =>
|
||||
ctx.enPassantSquare should not be empty
|
||||
)
|
||||
FenParserFastParse
|
||||
.parseFen(s"8/8/8/8/8/8/8/8 w - ${file}3 0 1")
|
||||
.fold(_ => fail(), ctx => ctx.enPassantSquare should not be empty)
|
||||
|
||||
test("parseBoard with mixed pieces and empty squares"):
|
||||
FenParserFastParse.parseBoard("r1bqkb1r/pppppppp/2n2n2/8/8/2N2N2/PPPPPPPP/R1BQKB1R") should not be empty
|
||||
|
||||
@@ -29,10 +29,12 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("pawn can capture diagonally"):
|
||||
// FEN: white pawn e4, black pawn d5
|
||||
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val captures = moves.filter(m => m.from == Square(File.E, Rank.R4) && (m.moveType match { case _: MoveType.Normal => true; case _ => false }))
|
||||
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val captures = moves.filter(m =>
|
||||
m.from == Square(File.E, Rank.R4) && (m.moveType match { case _: MoveType.Normal => true; case _ => false }),
|
||||
)
|
||||
captures.exists(m => m.to == Square(File.D, Rank.R5)) shouldBe true
|
||||
|
||||
test("pawn cannot move backward"):
|
||||
|
||||
@@ -266,7 +266,8 @@ class ChessBoardView(val stage: Stage, private val engine: GameEngine) extends B
|
||||
case Some(piece) =>
|
||||
Seq(bgRect) ++ PieceSprites.loadPieceImage(piece, squareSize * 0.8).toSeq
|
||||
case None =>
|
||||
Seq(bgRect)): Seq[scalafx.scene.Node]
|
||||
Seq(bgRect)
|
||||
): Seq[scalafx.scene.Node]
|
||||
}
|
||||
|
||||
def showMessage(msg: String): Unit =
|
||||
|
||||
@@ -30,9 +30,9 @@ class ChessGUIApp extends JFXApplication:
|
||||
stage.scene = new Scene {
|
||||
root = boardView
|
||||
// Load CSS if available
|
||||
try {
|
||||
try
|
||||
Option(getClass.getResource("/styles.css")).foreach(url => stylesheets.add(url.toExternalForm))
|
||||
} catch {
|
||||
catch {
|
||||
case _: Exception => // CSS is optional
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user