style: fix CRLF line endings introduced by rebase
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,52 +6,52 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include
|
|||||||
case class PlayerInfoDto(id: String, displayName: String)
|
case class 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
-5
@@ -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`()
|
||||||
|
|||||||
+28
-14
@@ -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`()
|
||||||
|
|||||||
+16
-9
@@ -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`()
|
||||||
|
|||||||
+32
-16
@@ -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`()
|
||||||
|
|||||||
+2
-2
@@ -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"
|
||||||
|
|||||||
+3
-1
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user