style: fix CRLF line endings introduced by rebase
Build & Test (NowChessSystems) TeamCity build failed

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