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 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,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`()
@@ -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`()
@@ -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`()
@@ -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`()
@@ -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"
@@ -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
}
}