perf(core): reduce inter-service HTTP calls from 11 to 4 per move
- Add `postMoveStatus` batch method to `RuleSet` trait (default impl composes individual calls; `RuleSetRestAdapter` overrides with single HTTP round-trip) - Collapse 5 sequential rule checks in `GameEngine.executeMove` into one `postMoveStatus` call - Add `POST /api/rules/post-move-status` endpoint to rule-service - Add `exportCombined` to `IoServiceClient` and `POST /io/export/combined` endpoint to io-service, replacing two separate FEN/PGN HTTP calls - Fix `statusOf` to pattern-match on `WinReason` from `ctx.result` instead of making a redundant `isCheckmate` HTTP call - Remove duplicate `legalMoves` pre-validation in `GameResource.makeMove`; engine already validates and fires `InvalidMoveEvent` - Add `scalafix:off` guards for pre-existing `var`/`return` usage in `DefaultRules` hot-path code - Apply spotless formatting to previously unformatted files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,16 +11,27 @@ object FenParser extends GameContextImport:
|
||||
*/
|
||||
def parseFen(fen: String): Either[GameError, GameContext] =
|
||||
val parts = fen.trim.split("\\s+")
|
||||
if parts.length != 6 then Left(GameError.ParseError(s"Invalid FEN: expected 6 space-separated fields, got ${parts.length}"))
|
||||
if parts.length != 6 then
|
||||
Left(GameError.ParseError(s"Invalid FEN: expected 6 space-separated fields, got ${parts.length}"))
|
||||
else
|
||||
for
|
||||
board <- parseBoard(parts(0)).toRight(GameError.ParseError("Invalid FEN: invalid board position"))
|
||||
activeColor <- parseColor(parts(1)).toRight(GameError.ParseError("Invalid FEN: invalid active color (expected 'w' or 'b')"))
|
||||
board <- parseBoard(parts(0)).toRight(GameError.ParseError("Invalid FEN: invalid board position"))
|
||||
activeColor <- parseColor(parts(1)).toRight(
|
||||
GameError.ParseError("Invalid FEN: invalid active color (expected 'w' or 'b')"),
|
||||
)
|
||||
castlingRights <- parseCastling(parts(2)).toRight(GameError.ParseError("Invalid FEN: invalid castling rights"))
|
||||
enPassant <- parseEnPassant(parts(3)).toRight(GameError.ParseError("Invalid FEN: invalid en passant square"))
|
||||
halfMoveClock <- parts(4).toIntOption.toRight(GameError.ParseError("Invalid FEN: invalid half-move clock (expected integer)"))
|
||||
fullMoveNumber <- parts(5).toIntOption.toRight(GameError.ParseError("Invalid FEN: invalid full move number (expected integer)"))
|
||||
_ <- Either.cond(halfMoveClock >= 0 && fullMoveNumber >= 1, (), GameError.ParseError("Invalid FEN: invalid move counts"))
|
||||
enPassant <- parseEnPassant(parts(3)).toRight(GameError.ParseError("Invalid FEN: invalid en passant square"))
|
||||
halfMoveClock <- parts(4).toIntOption.toRight(
|
||||
GameError.ParseError("Invalid FEN: invalid half-move clock (expected integer)"),
|
||||
)
|
||||
fullMoveNumber <- parts(5).toIntOption.toRight(
|
||||
GameError.ParseError("Invalid FEN: invalid full move number (expected integer)"),
|
||||
)
|
||||
_ <- Either.cond(
|
||||
halfMoveClock >= 0 && fullMoveNumber >= 1,
|
||||
(),
|
||||
GameError.ParseError("Invalid FEN: invalid move counts"),
|
||||
)
|
||||
yield GameContext(
|
||||
board = board,
|
||||
turn = activeColor,
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package de.nowchess.io.service.dto
|
||||
|
||||
final case class CombinedExportResponse(fen: String, pgn: String)
|
||||
@@ -3,7 +3,7 @@ package de.nowchess.io.service.resource
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
||||
import de.nowchess.io.service.dto.{ImportFenRequest, ImportPgnRequest, IoErrorDto}
|
||||
import de.nowchess.io.service.dto.{CombinedExportResponse, ImportFenRequest, ImportPgnRequest, IoErrorDto}
|
||||
import io.smallrye.mutiny.Uni
|
||||
import jakarta.enterprise.context.ApplicationScoped
|
||||
import jakarta.ws.rs.*
|
||||
@@ -75,3 +75,18 @@ class IoResource:
|
||||
@APIResponse(responseCode = "200", description = "PGN text")
|
||||
def exportPgn(ctx: GameContext): Uni[Response] =
|
||||
Uni.createFrom().item(Response.ok(PgnExporter.exportGameContext(ctx)).build())
|
||||
|
||||
@POST
|
||||
@Path("/export/combined")
|
||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||
@Operation(summary = "Export FEN and PGN", description = "Serialize a GameContext to both FEN and PGN in one call")
|
||||
@APIResponse(responseCode = "200", description = "FEN and PGN")
|
||||
def exportCombined(ctx: GameContext): Uni[Response] =
|
||||
Uni
|
||||
.createFrom()
|
||||
.item(
|
||||
Response
|
||||
.ok(CombinedExportResponse(FenExporter.exportGameContext(ctx), PgnExporter.exportGameContext(ctx)))
|
||||
.build(),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user