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
@@ -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))
@@ -224,7 +224,9 @@ class GameStore:
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 =>
moves
.find(m => !m.moveType.isInstanceOf[MoveType.Promotion])
.orElse(moves.headOption) .orElse(moves.headOption)
private def detectGameOver(ctx: GameContext): Option[GameResult] = private def detectGameOver(ctx: GameContext): Option[GameResult] =
@@ -242,7 +244,8 @@ class GameStore:
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) =>
@@ -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()
@@ -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()
@@ -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,7 +23,8 @@ 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`()
@@ -33,7 +35,8 @@ class MoveResourceTest:
@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,7 +45,8 @@ 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`()
@@ -51,7 +55,8 @@ class MoveResourceTest:
@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`()
@@ -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
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w K - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.whiteKingSide shouldBe true ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe false ctx.castlingRights.whiteQueenSide shouldBe false
ctx.castlingRights.blackKingSide shouldBe false ctx.castlingRights.blackKingSide shouldBe false
ctx.castlingRights.blackQueenSide 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
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Q - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.whiteQueenSide shouldBe true ctx.castlingRights.whiteQueenSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false ctx.castlingRights.whiteKingSide shouldBe false,
) )
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w k - 0 1").fold(_ => fail(), ctx => FenParserFastParse
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w k - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.blackKingSide shouldBe true ctx.castlingRights.blackKingSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false ctx.castlingRights.whiteKingSide shouldBe false,
) )
FenParserFastParse.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w q - 0 1").fold(_ => fail(), ctx => FenParserFastParse
.parseFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w q - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.blackQueenSide shouldBe true ctx.castlingRights.blackQueenSide shouldBe true
ctx.castlingRights.whiteKingSide shouldBe false 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
@@ -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
.parseFen("8/8/8/8/8/8/8/8 w KQkq - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.whiteKingSide shouldBe true ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe true ctx.castlingRights.whiteQueenSide shouldBe true
ctx.castlingRights.blackKingSide shouldBe true ctx.castlingRights.blackKingSide shouldBe true
ctx.castlingRights.blackQueenSide 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
.parseFen("8/8/8/8/8/8/8/8 w Kq - 0 1")
.fold(
_ => fail(),
ctx =>
ctx.castlingRights.whiteKingSide shouldBe true ctx.castlingRights.whiteKingSide shouldBe true
ctx.castlingRights.whiteQueenSide shouldBe false ctx.castlingRights.whiteQueenSide shouldBe false
ctx.castlingRights.blackKingSide shouldBe false ctx.castlingRights.blackKingSide shouldBe false
ctx.castlingRights.blackQueenSide shouldBe true 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
@@ -32,7 +32,9 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
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
} }
} }