feat: true-microservices #40
@@ -13,7 +13,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
|||||||
classOf[ChallengeStatus],
|
classOf[ChallengeStatus],
|
||||||
classOf[DeclineReason],
|
classOf[DeclineReason],
|
||||||
classOf[TimeControl],
|
classOf[TimeControl],
|
||||||
|
|
||||||
classOf[LoginRequest],
|
classOf[LoginRequest],
|
||||||
classOf[TokenResponse],
|
classOf[TokenResponse],
|
||||||
classOf[PlayerInfo],
|
classOf[PlayerInfo],
|
||||||
@@ -27,7 +26,6 @@ import io.quarkus.runtime.annotations.RegisterForReflection
|
|||||||
classOf[ChallengeStatus],
|
classOf[ChallengeStatus],
|
||||||
classOf[ChallengeColor],
|
classOf[ChallengeColor],
|
||||||
classOf[DeclineReason],
|
classOf[DeclineReason],
|
||||||
|
|
||||||
classOf[CorePlayerInfo],
|
classOf[CorePlayerInfo],
|
||||||
classOf[CoreTimeControl],
|
classOf[CoreTimeControl],
|
||||||
classOf[CoreCreateGameRequest],
|
classOf[CoreCreateGameRequest],
|
||||||
|
|||||||
+1
-4
@@ -40,7 +40,4 @@ class AccountRepository:
|
|||||||
.headOption
|
.headOption
|
||||||
|
|
||||||
def findAll(): List[Account] =
|
def findAll(): List[Account] =
|
||||||
em.createQuery("FROM Account", classOf[Account])
|
em.createQuery("FROM Account", classOf[Account]).getResultList.asScala.toList
|
||||||
.getResultList
|
|
||||||
.asScala
|
|
||||||
.toList
|
|
||||||
|
|||||||
@@ -24,10 +24,8 @@ class AccountService:
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
def register(req: RegisterRequest): Either[AccountError, Account] =
|
def register(req: RegisterRequest): Either[AccountError, Account] =
|
||||||
if accountRepository.findByUsername(req.username).isDefined then
|
if accountRepository.findByUsername(req.username).isDefined then Left(AccountError.UsernameTaken(req.username))
|
||||||
Left(AccountError.UsernameTaken(req.username))
|
else if accountRepository.findByEmail(req.email).isDefined then Left(AccountError.EmailAlreadyRegistered(req.email))
|
||||||
else if accountRepository.findByEmail(req.email).isDefined then
|
|
||||||
Left(AccountError.EmailAlreadyRegistered(req.email))
|
|
||||||
else
|
else
|
||||||
val account = new Account()
|
val account = new Account()
|
||||||
account.username = req.username
|
account.username = req.username
|
||||||
|
|||||||
@@ -1,8 +1,21 @@
|
|||||||
package de.nowchess.account.service
|
package de.nowchess.account.service
|
||||||
|
|
||||||
import de.nowchess.account.client.{CoreCreateGameRequest, CoreGameClient, CoreGameResponse, CorePlayerInfo, CoreTimeControl}
|
import de.nowchess.account.client.{
|
||||||
|
CoreCreateGameRequest,
|
||||||
|
CoreGameClient,
|
||||||
|
CoreGameResponse,
|
||||||
|
CorePlayerInfo,
|
||||||
|
CoreTimeControl,
|
||||||
|
}
|
||||||
import de.nowchess.account.domain.{Challenge, ChallengeColor, ChallengeStatus, DeclineReason}
|
import de.nowchess.account.domain.{Challenge, ChallengeColor, ChallengeStatus, DeclineReason}
|
||||||
import de.nowchess.account.dto.{ChallengeDto, ChallengeListDto, ChallengeRequest, DeclineRequest, PlayerInfo, TimeControlDto}
|
import de.nowchess.account.dto.{
|
||||||
|
ChallengeDto,
|
||||||
|
ChallengeListDto,
|
||||||
|
ChallengeRequest,
|
||||||
|
DeclineRequest,
|
||||||
|
PlayerInfo,
|
||||||
|
TimeControlDto,
|
||||||
|
}
|
||||||
import de.nowchess.account.error.ChallengeError
|
import de.nowchess.account.error.ChallengeError
|
||||||
import de.nowchess.account.repository.{AccountRepository, ChallengeRepository}
|
import de.nowchess.account.repository.{AccountRepository, ChallengeRepository}
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
@@ -49,10 +62,8 @@ class ChallengeService:
|
|||||||
challenge.color = color
|
challenge.color = color
|
||||||
challenge.status = ChallengeStatus.Created
|
challenge.status = ChallengeStatus.Created
|
||||||
challenge.timeControlType = req.timeControl.`type`
|
challenge.timeControlType = req.timeControl.`type`
|
||||||
challenge.timeControlLimit =
|
challenge.timeControlLimit = req.timeControl.limit.map(java.lang.Integer.valueOf).orNull
|
||||||
req.timeControl.limit.map(java.lang.Integer.valueOf).orNull
|
challenge.timeControlIncrement = req.timeControl.increment.map(java.lang.Integer.valueOf).orNull
|
||||||
challenge.timeControlIncrement =
|
|
||||||
req.timeControl.increment.map(java.lang.Integer.valueOf).orNull
|
|
||||||
challenge.createdAt = Instant.now()
|
challenge.createdAt = Instant.now()
|
||||||
challenge.expiresAt = Instant.now().plus(24, ChronoUnit.HOURS)
|
challenge.expiresAt = Instant.now().plus(24, ChronoUnit.HOURS)
|
||||||
challengeRepository.persist(challenge)
|
challengeRepository.persist(challenge)
|
||||||
|
|||||||
+14
-2
@@ -156,8 +156,20 @@ class ChallengeResourceTest:
|
|||||||
val t1 = registerAndLogin("listUser1")
|
val t1 = registerAndLogin("listUser1")
|
||||||
registerAndLogin("listUser2")
|
registerAndLogin("listUser2")
|
||||||
registerAndLogin("listUser3")
|
registerAndLogin("listUser3")
|
||||||
authed(t1).contentType(ContentType.JSON).body(clockBody).when().post("/api/challenge/listUser2").`then`().statusCode(201)
|
authed(t1)
|
||||||
authed(t1).contentType(ContentType.JSON).body(clockBody).when().post("/api/challenge/listUser3").`then`().statusCode(201)
|
.contentType(ContentType.JSON)
|
||||||
|
.body(clockBody)
|
||||||
|
.when()
|
||||||
|
.post("/api/challenge/listUser2")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(201)
|
||||||
|
authed(t1)
|
||||||
|
.contentType(ContentType.JSON)
|
||||||
|
.body(clockBody)
|
||||||
|
.when()
|
||||||
|
.post("/api/challenge/listUser3")
|
||||||
|
.`then`()
|
||||||
|
.statusCode(201)
|
||||||
authed(t1)
|
authed(t1)
|
||||||
.when()
|
.when()
|
||||||
.get("/api/challenge")
|
.get("/api/challenge")
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package de.nowchess.api.rules
|
||||||
|
|
||||||
|
final case class PostMoveStatus(
|
||||||
|
isCheckmate: Boolean,
|
||||||
|
isStalemate: Boolean,
|
||||||
|
isInsufficientMaterial: Boolean,
|
||||||
|
isCheck: Boolean,
|
||||||
|
isThreefoldRepetition: Boolean,
|
||||||
|
)
|
||||||
@@ -39,3 +39,15 @@ trait RuleSet:
|
|||||||
* promotion. Updates castling rights, en passant square, half-move clock, turn, and move history.
|
* promotion. Updates castling rights, en passant square, half-move clock, turn, and move history.
|
||||||
*/
|
*/
|
||||||
def applyMove(context: GameContext)(move: Move): GameContext
|
def applyMove(context: GameContext)(move: Move): GameContext
|
||||||
|
|
||||||
|
/** Batch status check after a move is applied. Replaces individual isCheckmate/isStalemate/isInsufficientMaterial/
|
||||||
|
* isCheck/isThreefoldRepetition calls with a single round-trip. Override for remote implementations.
|
||||||
|
*/
|
||||||
|
def postMoveStatus(context: GameContext): PostMoveStatus =
|
||||||
|
PostMoveStatus(
|
||||||
|
isCheckmate = isCheckmate(context),
|
||||||
|
isStalemate = isStalemate(context),
|
||||||
|
isInsufficientMaterial = isInsufficientMaterial(context),
|
||||||
|
isCheck = isCheck(context),
|
||||||
|
isThreefoldRepetition = isThreefoldRepetition(context),
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import de.nowchess.api.board.Square
|
|||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.Move
|
import de.nowchess.api.move.Move
|
||||||
import de.nowchess.chess.client.{RuleMoveRequest, RuleServiceClient, RuleSquareRequest}
|
import de.nowchess.chess.client.{RuleMoveRequest, RuleServiceClient, RuleSquareRequest}
|
||||||
import de.nowchess.api.rules.RuleSet
|
import de.nowchess.api.rules.{PostMoveStatus, RuleSet}
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.inject.Inject
|
import jakarta.inject.Inject
|
||||||
import org.eclipse.microprofile.rest.client.inject.RestClient
|
import org.eclipse.microprofile.rest.client.inject.RestClient
|
||||||
@@ -49,3 +49,6 @@ class RuleSetRestAdapter extends RuleSet:
|
|||||||
|
|
||||||
def applyMove(ctx: GameContext)(move: Move): GameContext =
|
def applyMove(ctx: GameContext)(move: Move): GameContext =
|
||||||
client.applyMove(RuleMoveRequest(ctx, move))
|
client.applyMove(RuleMoveRequest(ctx, move))
|
||||||
|
|
||||||
|
override def postMoveStatus(ctx: GameContext): PostMoveStatus =
|
||||||
|
client.postMoveStatus(ctx)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import jakarta.ws.rs.*
|
|||||||
import jakarta.ws.rs.core.MediaType
|
import jakarta.ws.rs.core.MediaType
|
||||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||||
|
|
||||||
|
case class CombinedExportResponse(fen: String, pgn: String)
|
||||||
|
|
||||||
@Path("/io")
|
@Path("/io")
|
||||||
@RegisterRestClient(configKey = "io-service")
|
@RegisterRestClient(configKey = "io-service")
|
||||||
trait IoServiceClient:
|
trait IoServiceClient:
|
||||||
@@ -33,3 +35,9 @@ trait IoServiceClient:
|
|||||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
@Produces(Array("application/x-chess-pgn"))
|
@Produces(Array("application/x-chess-pgn"))
|
||||||
def exportPgn(ctx: GameContext): String
|
def exportPgn(ctx: GameContext): String
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/export/combined")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def exportCombined(ctx: GameContext): CombinedExportResponse
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package de.nowchess.chess.client
|
|||||||
|
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.Move
|
import de.nowchess.api.move.Move
|
||||||
|
import de.nowchess.api.rules.PostMoveStatus
|
||||||
import jakarta.ws.rs.*
|
import jakarta.ws.rs.*
|
||||||
import jakarta.ws.rs.core.MediaType
|
import jakarta.ws.rs.core.MediaType
|
||||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient
|
||||||
@@ -72,3 +73,9 @@ trait RuleServiceClient:
|
|||||||
@Consumes(Array(MediaType.APPLICATION_JSON))
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
def applyMove(req: RuleMoveRequest): GameContext
|
def applyMove(req: RuleMoveRequest): GameContext
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/post-move-status")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def postMoveStatus(ctx: GameContext): PostMoveStatus
|
||||||
|
|||||||
@@ -2,7 +2,19 @@ package de.nowchess.chess.engine
|
|||||||
|
|
||||||
import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square}
|
import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square}
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.api.game.{BotParticipant, ClockState, CorrespondenceClockState, DrawReason, GameContext, GameResult, Human, LiveClockState, Participant, TimeControl, WinReason}
|
import de.nowchess.api.game.{
|
||||||
|
BotParticipant,
|
||||||
|
ClockState,
|
||||||
|
CorrespondenceClockState,
|
||||||
|
DrawReason,
|
||||||
|
GameContext,
|
||||||
|
GameResult,
|
||||||
|
Human,
|
||||||
|
LiveClockState,
|
||||||
|
Participant,
|
||||||
|
TimeControl,
|
||||||
|
WinReason,
|
||||||
|
}
|
||||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||||
import de.nowchess.chess.controller.Parser
|
import de.nowchess.chess.controller.Parser
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
@@ -378,28 +390,28 @@ class GameEngine(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val status = ruleSet.postMoveStatus(currentContext)
|
||||||
if currentContext.result.isEmpty then
|
if currentContext.result.isEmpty then
|
||||||
if ruleSet.isCheckmate(currentContext) then
|
if status.isCheckmate then
|
||||||
val winner = currentContext.turn.opposite
|
val winner = currentContext.turn.opposite
|
||||||
currentContext = currentContext.withResult(Some(GameResult.Win(winner, Checkmate)))
|
currentContext = currentContext.withResult(Some(GameResult.Win(winner, Checkmate)))
|
||||||
cancelScheduled()
|
cancelScheduled()
|
||||||
notifyObservers(CheckmateEvent(currentContext, winner))
|
notifyObservers(CheckmateEvent(currentContext, winner))
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
else if ruleSet.isStalemate(currentContext) then
|
else if status.isStalemate then
|
||||||
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.Stalemate)))
|
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.Stalemate)))
|
||||||
cancelScheduled()
|
cancelScheduled()
|
||||||
notifyObservers(DrawEvent(currentContext, DrawReason.Stalemate))
|
notifyObservers(DrawEvent(currentContext, DrawReason.Stalemate))
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
else if ruleSet.isInsufficientMaterial(currentContext) then
|
else if status.isInsufficientMaterial then
|
||||||
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.InsufficientMaterial)))
|
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.InsufficientMaterial)))
|
||||||
cancelScheduled()
|
cancelScheduled()
|
||||||
notifyObservers(DrawEvent(currentContext, DrawReason.InsufficientMaterial))
|
notifyObservers(DrawEvent(currentContext, DrawReason.InsufficientMaterial))
|
||||||
invoker.clear()
|
invoker.clear()
|
||||||
else if ruleSet.isCheck(currentContext) then notifyObservers(CheckDetectedEvent(currentContext))
|
else if status.isCheck then notifyObservers(CheckDetectedEvent(currentContext))
|
||||||
|
|
||||||
if currentContext.halfMoveClock >= 100 then notifyObservers(FiftyMoveRuleAvailableEvent(currentContext))
|
if currentContext.halfMoveClock >= 100 then notifyObservers(FiftyMoveRuleAvailableEvent(currentContext))
|
||||||
if ruleSet.isThreefoldRepetition(currentContext) then
|
if status.isThreefoldRepetition then notifyObservers(ThreefoldRepetitionAvailableEvent(currentContext))
|
||||||
notifyObservers(ThreefoldRepetitionAvailableEvent(currentContext))
|
|
||||||
else requestBotMoveIfNeeded()
|
else requestBotMoveIfNeeded()
|
||||||
|
|
||||||
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
|
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
|
||||||
|
|||||||
@@ -3,7 +3,16 @@ package de.nowchess.chess.resource
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
import de.nowchess.api.board.{Color, Square}
|
import de.nowchess.api.board.{Color, Square}
|
||||||
import de.nowchess.api.dto.*
|
import de.nowchess.api.dto.*
|
||||||
import de.nowchess.api.game.{CorrespondenceClockState, DrawReason, GameContext, GameMode, GameResult, LiveClockState, TimeControl}
|
import de.nowchess.api.game.{
|
||||||
|
CorrespondenceClockState,
|
||||||
|
DrawReason,
|
||||||
|
GameContext,
|
||||||
|
GameMode,
|
||||||
|
GameResult,
|
||||||
|
LiveClockState,
|
||||||
|
TimeControl,
|
||||||
|
WinReason,
|
||||||
|
}
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
import de.nowchess.api.player.{PlayerId, PlayerInfo}
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -56,7 +65,8 @@ class GameResource:
|
|||||||
entry.mode match
|
entry.mode match
|
||||||
case GameMode.Open => entry.engine.context.turn
|
case GameMode.Open => entry.engine.context.turn
|
||||||
case GameMode.Authenticated =>
|
case GameMode.Authenticated =>
|
||||||
val subject = Option(jwt).flatMap(j => Option(j.getSubject))
|
val subject = Option(jwt)
|
||||||
|
.flatMap(j => Option(j.getSubject))
|
||||||
.getOrElse(throw ForbiddenException("Authentication required"))
|
.getOrElse(throw ForbiddenException("Authentication required"))
|
||||||
if entry.white.id.value == subject then Color.White
|
if entry.white.id.value == subject then Color.White
|
||||||
else if entry.black.id.value == subject then Color.Black
|
else if entry.black.id.value == subject then Color.Black
|
||||||
@@ -65,8 +75,7 @@ class GameResource:
|
|||||||
private def assertIsCurrentPlayer(entry: GameEntry): Unit =
|
private def assertIsCurrentPlayer(entry: GameEntry): Unit =
|
||||||
if entry.mode == GameMode.Authenticated then
|
if entry.mode == GameMode.Authenticated then
|
||||||
val color = colorOf(entry)
|
val color = colorOf(entry)
|
||||||
if color != entry.engine.context.turn then
|
if color != entry.engine.context.turn then throw ForbiddenException("Not your turn")
|
||||||
throw ForbiddenException("Not your turn")
|
|
||||||
|
|
||||||
// scalafix:on DisableSyntax.throw
|
// scalafix:on DisableSyntax.throw
|
||||||
|
|
||||||
@@ -77,10 +86,9 @@ class GameResource:
|
|||||||
else
|
else
|
||||||
val ctx = entry.engine.context
|
val ctx = entry.engine.context
|
||||||
ctx.result match
|
ctx.result match
|
||||||
case Some(GameResult.Win(_, _)) =>
|
case Some(GameResult.Win(_, WinReason.Checkmate)) => "checkmate"
|
||||||
if entry.resigned then "resign"
|
case Some(GameResult.Win(_, WinReason.Resignation)) => "resign"
|
||||||
else if entry.engine.ruleSet.isCheckmate(ctx) then "checkmate"
|
case Some(GameResult.Win(_, WinReason.TimeControl)) => "timeout"
|
||||||
else "timeout"
|
|
||||||
case Some(GameResult.Draw(DrawReason.Stalemate)) => "stalemate"
|
case Some(GameResult.Draw(DrawReason.Stalemate)) => "stalemate"
|
||||||
case Some(GameResult.Draw(DrawReason.InsufficientMaterial)) => "insufficientMaterial"
|
case Some(GameResult.Draw(DrawReason.InsufficientMaterial)) => "insufficientMaterial"
|
||||||
case Some(GameResult.Draw(_)) => "draw"
|
case Some(GameResult.Draw(_)) => "draw"
|
||||||
@@ -129,9 +137,10 @@ class GameResource:
|
|||||||
|
|
||||||
private def toGameStateDto(entry: GameEntry): GameStateDto =
|
private def toGameStateDto(entry: GameEntry): GameStateDto =
|
||||||
val ctx = entry.engine.context
|
val ctx = entry.engine.context
|
||||||
|
val exported = ioClient.exportCombined(ctx)
|
||||||
GameStateDto(
|
GameStateDto(
|
||||||
fen = ioClient.exportFen(ctx),
|
fen = exported.fen,
|
||||||
pgn = ioClient.exportPgn(ctx),
|
pgn = exported.pgn,
|
||||||
turn = ctx.turn.label.toLowerCase,
|
turn = ctx.turn.label.toLowerCase,
|
||||||
status = statusOf(entry),
|
status = statusOf(entry),
|
||||||
winner = ctx.result.collect { case GameResult.Win(c, _) => c.label.toLowerCase },
|
winner = ctx.result.collect { case GameResult.Win(c, _) => c.label.toLowerCase },
|
||||||
@@ -234,13 +243,8 @@ class GameResource:
|
|||||||
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
val entry = registry.get(gameId).getOrElse(throw GameNotFoundException(gameId))
|
||||||
assertGameNotOver(entry)
|
assertGameNotOver(entry)
|
||||||
assertIsCurrentPlayer(entry)
|
assertIsCurrentPlayer(entry)
|
||||||
val (from, to, promoOpt) = Parser
|
if Parser.parseMove(uci).isEmpty then
|
||||||
.parseMove(uci)
|
throw BadRequestException("INVALID_UCI", s"Invalid UCI notation: $uci", Some("uci"))
|
||||||
.getOrElse(throw BadRequestException("INVALID_UCI", s"Invalid UCI notation: $uci", Some("uci")))
|
|
||||||
val candidates = entry.engine.ruleSet.legalMoves(entry.engine.context)(from).filter(_.to == to)
|
|
||||||
val isPromotion = candidates.exists { case Move(_, _, MoveType.Promotion(_)) => true; case _ => false }
|
|
||||||
if candidates.isEmpty || (isPromotion && promoOpt.isEmpty) then
|
|
||||||
throw BadRequestException("INVALID_MOVE", s"$uci is not a legal move", Some("uci"))
|
|
||||||
applyMoveInput(entry.engine, uci).foreach(err => throw BadRequestException("INVALID_MOVE", err, Some("uci")))
|
applyMoveInput(entry.engine, uci).foreach(err => throw BadRequestException("INVALID_MOVE", err, Some("uci")))
|
||||||
ok(toGameStateDto(entry))
|
ok(toGameStateDto(entry))
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
package de.nowchess.chess.engine
|
package de.nowchess.chess.engine
|
||||||
|
|
||||||
import de.nowchess.api.board.Color
|
import de.nowchess.api.board.Color
|
||||||
import de.nowchess.api.game.{ClockState, CorrespondenceClockState, DrawReason, GameResult, LiveClockState, TimeControl, WinReason}
|
import de.nowchess.api.game.{
|
||||||
|
ClockState,
|
||||||
|
CorrespondenceClockState,
|
||||||
|
DrawReason,
|
||||||
|
GameResult,
|
||||||
|
LiveClockState,
|
||||||
|
TimeControl,
|
||||||
|
WinReason,
|
||||||
|
}
|
||||||
import de.nowchess.chess.observer.*
|
import de.nowchess.chess.observer.*
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
|||||||
+9
-1
@@ -3,7 +3,8 @@ package de.nowchess.chess.resource
|
|||||||
import de.nowchess.api.board.Square
|
import de.nowchess.api.board.Square
|
||||||
import de.nowchess.api.dto.*
|
import de.nowchess.api.dto.*
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.chess.client.{IoServiceClient, RuleMoveRequest, RuleServiceClient, RuleSquareRequest}
|
import de.nowchess.api.rules.PostMoveStatus
|
||||||
|
import de.nowchess.chess.client.{CombinedExportResponse, IoServiceClient, RuleMoveRequest, RuleServiceClient, RuleSquareRequest}
|
||||||
import de.nowchess.chess.exception.BadRequestException
|
import de.nowchess.chess.exception.BadRequestException
|
||||||
import de.nowchess.io.fen.FenExporter
|
import de.nowchess.io.fen.FenExporter
|
||||||
import de.nowchess.io.pgn.PgnParser
|
import de.nowchess.io.pgn.PgnParser
|
||||||
@@ -44,6 +45,10 @@ class GameResourceIntegrationTest:
|
|||||||
)
|
)
|
||||||
when(ioClient.exportFen(any())).thenReturn(FenExporter.exportGameContext(GameContext.initial))
|
when(ioClient.exportFen(any())).thenReturn(FenExporter.exportGameContext(GameContext.initial))
|
||||||
when(ioClient.exportPgn(any())).thenReturn("1. e4 c5")
|
when(ioClient.exportPgn(any())).thenReturn("1. e4 c5")
|
||||||
|
when(ioClient.exportCombined(any())).thenAnswer((inv: InvocationOnMock) =>
|
||||||
|
val ctx = inv.getArgument[GameContext](0)
|
||||||
|
CombinedExportResponse(FenExporter.exportGameContext(ctx), ""),
|
||||||
|
)
|
||||||
when(ruleClient.legalMoves(any())).thenAnswer((inv: InvocationOnMock) =>
|
when(ruleClient.legalMoves(any())).thenAnswer((inv: InvocationOnMock) =>
|
||||||
val req = inv.getArgument[RuleSquareRequest](0)
|
val req = inv.getArgument[RuleSquareRequest](0)
|
||||||
DefaultRules.legalMoves(req.context)(Square.fromAlgebraic(req.square).get),
|
DefaultRules.legalMoves(req.context)(Square.fromAlgebraic(req.square).get),
|
||||||
@@ -70,6 +75,9 @@ class GameResourceIntegrationTest:
|
|||||||
when(ruleClient.isThreefoldRepetition(any())).thenAnswer((inv: InvocationOnMock) =>
|
when(ruleClient.isThreefoldRepetition(any())).thenAnswer((inv: InvocationOnMock) =>
|
||||||
DefaultRules.isThreefoldRepetition(inv.getArgument[GameContext](0)),
|
DefaultRules.isThreefoldRepetition(inv.getArgument[GameContext](0)),
|
||||||
)
|
)
|
||||||
|
when(ruleClient.postMoveStatus(any())).thenAnswer((inv: InvocationOnMock) =>
|
||||||
|
DefaultRules.postMoveStatus(inv.getArgument[GameContext](0)),
|
||||||
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("createGame returns 201")
|
@DisplayName("createGame returns 201")
|
||||||
|
|||||||
@@ -11,16 +11,27 @@ object FenParser extends GameContextImport:
|
|||||||
*/
|
*/
|
||||||
def parseFen(fen: String): Either[GameError, GameContext] =
|
def parseFen(fen: String): Either[GameError, GameContext] =
|
||||||
val parts = fen.trim.split("\\s+")
|
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
|
else
|
||||||
for
|
for
|
||||||
board <- parseBoard(parts(0)).toRight(GameError.ParseError("Invalid FEN: invalid board position"))
|
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')"))
|
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"))
|
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"))
|
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)"))
|
halfMoveClock <- parts(4).toIntOption.toRight(
|
||||||
fullMoveNumber <- parts(5).toIntOption.toRight(GameError.ParseError("Invalid FEN: invalid full move number (expected integer)"))
|
GameError.ParseError("Invalid FEN: invalid half-move clock (expected integer)"),
|
||||||
_ <- Either.cond(halfMoveClock >= 0 && fullMoveNumber >= 1, (), GameError.ParseError("Invalid FEN: invalid move counts"))
|
)
|
||||||
|
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(
|
yield GameContext(
|
||||||
board = board,
|
board = board,
|
||||||
turn = activeColor,
|
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.api.game.GameContext
|
||||||
import de.nowchess.io.fen.{FenExporter, FenParser}
|
import de.nowchess.io.fen.{FenExporter, FenParser}
|
||||||
import de.nowchess.io.pgn.{PgnExporter, PgnParser}
|
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 io.smallrye.mutiny.Uni
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.ws.rs.*
|
import jakarta.ws.rs.*
|
||||||
@@ -75,3 +75,18 @@ class IoResource:
|
|||||||
@APIResponse(responseCode = "200", description = "PGN text")
|
@APIResponse(responseCode = "200", description = "PGN text")
|
||||||
def exportPgn(ctx: GameContext): Uni[Response] =
|
def exportPgn(ctx: GameContext): Uni[Response] =
|
||||||
Uni.createFrom().item(Response.ok(PgnExporter.exportGameContext(ctx)).build())
|
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(),
|
||||||
|
)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import de.nowchess.api.board.Square
|
|||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.Move
|
import de.nowchess.api.move.Move
|
||||||
import de.nowchess.rules.dto.*
|
import de.nowchess.rules.dto.*
|
||||||
|
import de.nowchess.api.rules.PostMoveStatus
|
||||||
import de.nowchess.rules.sets.DefaultRules
|
import de.nowchess.rules.sets.DefaultRules
|
||||||
import jakarta.enterprise.context.ApplicationScoped
|
import jakarta.enterprise.context.ApplicationScoped
|
||||||
import jakarta.ws.rs.*
|
import jakarta.ws.rs.*
|
||||||
@@ -88,3 +89,10 @@ class RuleSetResource:
|
|||||||
@Produces(Array(MediaType.APPLICATION_JSON))
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
def applyMove(req: ContextMoveRequest): GameContext =
|
def applyMove(req: ContextMoveRequest): GameContext =
|
||||||
rules.applyMove(req.context)(req.move)
|
rules.applyMove(req.context)(req.move)
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/post-move-status")
|
||||||
|
@Consumes(Array(MediaType.APPLICATION_JSON))
|
||||||
|
@Produces(Array(MediaType.APPLICATION_JSON))
|
||||||
|
def postMoveStatus(ctx: GameContext): PostMoveStatus =
|
||||||
|
rules.postMoveStatus(ctx)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package de.nowchess.rules.sets
|
|||||||
import de.nowchess.api.board.*
|
import de.nowchess.api.board.*
|
||||||
import de.nowchess.api.game.GameContext
|
import de.nowchess.api.game.GameContext
|
||||||
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
import de.nowchess.api.move.{Move, MoveType, PromotionPiece}
|
||||||
import de.nowchess.api.rules.RuleSet
|
import de.nowchess.api.rules.{PostMoveStatus, RuleSet}
|
||||||
|
|
||||||
/** Standard chess rules — optimized hot path.
|
/** Standard chess rules — optimized hot path.
|
||||||
*
|
*
|
||||||
@@ -12,6 +12,8 @@ import de.nowchess.api.rules.RuleSet
|
|||||||
* avoid heap allocation in tight loops. Check detection uses make/unmake on the mutable array instead of copying the
|
* avoid heap allocation in tight loops. Check detection uses make/unmake on the mutable array instead of copying the
|
||||||
* immutable Board map.
|
* immutable Board map.
|
||||||
*/
|
*/
|
||||||
|
// scalafix:off DisableSyntax.var
|
||||||
|
// scalafix:off DisableSyntax.return
|
||||||
object DefaultRules extends RuleSet:
|
object DefaultRules extends RuleSet:
|
||||||
|
|
||||||
// ─── Piece constants ──────────────────────────────────────────────────────
|
// ─── Piece constants ──────────────────────────────────────────────────────
|
||||||
@@ -225,12 +227,19 @@ object DefaultRules extends RuleSet:
|
|||||||
var n = 0; var sq = 0
|
var n = 0; var sq = 0
|
||||||
while sq < 64 do
|
while sq < 64 do
|
||||||
val p = arr(sq)
|
val p = arr(sq)
|
||||||
if !isEmpty(p) && isWhitePiece(p) == isWhite then
|
if !isEmpty(p) && isWhitePiece(p) == isWhite then n = generatePiece(arr, sq, pieceType(p), isWhite, ctx, buf, n)
|
||||||
n = generatePiece(arr, sq, pieceType(p), isWhite, ctx, buf, n)
|
|
||||||
sq += 1
|
sq += 1
|
||||||
n
|
n
|
||||||
|
|
||||||
private def generatePiece(arr: Array[Int], sq: Int, pt: Int, isWhite: Boolean, ctx: GameContext, buf: Array[Int], n: Int): Int =
|
private def generatePiece(
|
||||||
|
arr: Array[Int],
|
||||||
|
sq: Int,
|
||||||
|
pt: Int,
|
||||||
|
isWhite: Boolean,
|
||||||
|
ctx: GameContext,
|
||||||
|
buf: Array[Int],
|
||||||
|
n: Int,
|
||||||
|
): Int =
|
||||||
if pt == PAWN then generatePawnMoves(arr, sq, isWhite, ctx, buf, n)
|
if pt == PAWN then generatePawnMoves(arr, sq, isWhite, ctx, buf, n)
|
||||||
else if pt == KNIGHT then generateJumps(arr, sq, isWhite, KNIGHT_TARGETS(sq), buf, n)
|
else if pt == KNIGHT then generateJumps(arr, sq, isWhite, KNIGHT_TARGETS(sq), buf, n)
|
||||||
else if pt == BISHOP then generateRays(arr, sq, isWhite, buf, n, rookRays = false)
|
else if pt == BISHOP then generateRays(arr, sq, isWhite, buf, n, rookRays = false)
|
||||||
@@ -240,7 +249,14 @@ object DefaultRules extends RuleSet:
|
|||||||
generateRays(arr, sq, isWhite, buf, n2, rookRays = false)
|
generateRays(arr, sq, isWhite, buf, n2, rookRays = false)
|
||||||
else generateKingMoves(arr, sq, isWhite, ctx, buf, n)
|
else generateKingMoves(arr, sq, isWhite, ctx, buf, n)
|
||||||
|
|
||||||
private def generateJumps(arr: Array[Int], from: Int, isWhite: Boolean, targets: Array[Int], buf: Array[Int], start: Int): Int =
|
private def generateJumps(
|
||||||
|
arr: Array[Int],
|
||||||
|
from: Int,
|
||||||
|
isWhite: Boolean,
|
||||||
|
targets: Array[Int],
|
||||||
|
buf: Array[Int],
|
||||||
|
start: Int,
|
||||||
|
): Int =
|
||||||
var n = start; var i = 0
|
var n = start; var i = 0
|
||||||
while i < targets.length do
|
while i < targets.length do
|
||||||
val to = targets(i); val tgt = arr(to)
|
val to = targets(i); val tgt = arr(to)
|
||||||
@@ -251,7 +267,14 @@ object DefaultRules extends RuleSet:
|
|||||||
i += 1
|
i += 1
|
||||||
n
|
n
|
||||||
|
|
||||||
private def generateRays(arr: Array[Int], from: Int, isWhite: Boolean, buf: Array[Int], start: Int, rookRays: Boolean): Int =
|
private def generateRays(
|
||||||
|
arr: Array[Int],
|
||||||
|
from: Int,
|
||||||
|
isWhite: Boolean,
|
||||||
|
buf: Array[Int],
|
||||||
|
start: Int,
|
||||||
|
rookRays: Boolean,
|
||||||
|
): Int =
|
||||||
var n = start
|
var n = start
|
||||||
val rays = RAY_TABLES(from)
|
val rays = RAY_TABLES(from)
|
||||||
val d0 = if rookRays then 0 else 4
|
val d0 = if rookRays then 0 else 4
|
||||||
@@ -271,11 +294,25 @@ object DefaultRules extends RuleSet:
|
|||||||
d += 1
|
d += 1
|
||||||
n
|
n
|
||||||
|
|
||||||
private def generateKingMoves(arr: Array[Int], from: Int, isWhite: Boolean, ctx: GameContext, buf: Array[Int], start: Int): Int =
|
private def generateKingMoves(
|
||||||
|
arr: Array[Int],
|
||||||
|
from: Int,
|
||||||
|
isWhite: Boolean,
|
||||||
|
ctx: GameContext,
|
||||||
|
buf: Array[Int],
|
||||||
|
start: Int,
|
||||||
|
): Int =
|
||||||
val n = generateJumps(arr, from, isWhite, KING_TARGETS(from), buf, start)
|
val n = generateJumps(arr, from, isWhite, KING_TARGETS(from), buf, start)
|
||||||
generateCastlingMoves(arr, from, isWhite, ctx, buf, n)
|
generateCastlingMoves(arr, from, isWhite, ctx, buf, n)
|
||||||
|
|
||||||
private def generateCastlingMoves(arr: Array[Int], from: Int, isWhite: Boolean, ctx: GameContext, buf: Array[Int], start: Int): Int =
|
private def generateCastlingMoves(
|
||||||
|
arr: Array[Int],
|
||||||
|
from: Int,
|
||||||
|
isWhite: Boolean,
|
||||||
|
ctx: GameContext,
|
||||||
|
buf: Array[Int],
|
||||||
|
start: Int,
|
||||||
|
): Int =
|
||||||
var n = start
|
var n = start
|
||||||
val cr = ctx.castlingRights
|
val cr = ctx.castlingRights
|
||||||
if isWhite && from == E1 then
|
if isWhite && from == E1 then
|
||||||
@@ -283,30 +320,41 @@ object DefaultRules extends RuleSet:
|
|||||||
arr(E1) == KING && arr(H1) == ROOK &&
|
arr(E1) == KING && arr(H1) == ROOK &&
|
||||||
!isAttackedByColor(arr, E1, false) &&
|
!isAttackedByColor(arr, E1, false) &&
|
||||||
!isAttackedByColor(arr, F1, false) &&
|
!isAttackedByColor(arr, F1, false) &&
|
||||||
!isAttackedByColor(arr, G1, false) then
|
!isAttackedByColor(arr, G1, false)
|
||||||
|
then
|
||||||
buf(n) = encMove(E1, G1, KIND_CASTLEK); n += 1
|
buf(n) = encMove(E1, G1, KIND_CASTLEK); n += 1
|
||||||
if cr.whiteQueenSide && isEmpty(arr(D1)) && isEmpty(arr(C1)) && isEmpty(arr(B1)) &&
|
if cr.whiteQueenSide && isEmpty(arr(D1)) && isEmpty(arr(C1)) && isEmpty(arr(B1)) &&
|
||||||
arr(E1) == KING && arr(A1) == ROOK &&
|
arr(E1) == KING && arr(A1) == ROOK &&
|
||||||
!isAttackedByColor(arr, E1, false) &&
|
!isAttackedByColor(arr, E1, false) &&
|
||||||
!isAttackedByColor(arr, D1, false) &&
|
!isAttackedByColor(arr, D1, false) &&
|
||||||
!isAttackedByColor(arr, C1, false) then
|
!isAttackedByColor(arr, C1, false)
|
||||||
|
then
|
||||||
buf(n) = encMove(E1, C1, KIND_CASTLEQ); n += 1
|
buf(n) = encMove(E1, C1, KIND_CASTLEQ); n += 1
|
||||||
else if !isWhite && from == E8 then
|
else if !isWhite && from == E8 then
|
||||||
if cr.blackKingSide && isEmpty(arr(F8)) && isEmpty(arr(G8)) &&
|
if cr.blackKingSide && isEmpty(arr(F8)) && isEmpty(arr(G8)) &&
|
||||||
arr(E8) == -KING && arr(H8) == -ROOK &&
|
arr(E8) == -KING && arr(H8) == -ROOK &&
|
||||||
!isAttackedByColor(arr, E8, true) &&
|
!isAttackedByColor(arr, E8, true) &&
|
||||||
!isAttackedByColor(arr, F8, true) &&
|
!isAttackedByColor(arr, F8, true) &&
|
||||||
!isAttackedByColor(arr, G8, true) then
|
!isAttackedByColor(arr, G8, true)
|
||||||
|
then
|
||||||
buf(n) = encMove(E8, G8, KIND_CASTLEK); n += 1
|
buf(n) = encMove(E8, G8, KIND_CASTLEK); n += 1
|
||||||
if cr.blackQueenSide && isEmpty(arr(D8)) && isEmpty(arr(C8)) && isEmpty(arr(B8)) &&
|
if cr.blackQueenSide && isEmpty(arr(D8)) && isEmpty(arr(C8)) && isEmpty(arr(B8)) &&
|
||||||
arr(E8) == -KING && arr(A8) == -ROOK &&
|
arr(E8) == -KING && arr(A8) == -ROOK &&
|
||||||
!isAttackedByColor(arr, E8, true) &&
|
!isAttackedByColor(arr, E8, true) &&
|
||||||
!isAttackedByColor(arr, D8, true) &&
|
!isAttackedByColor(arr, D8, true) &&
|
||||||
!isAttackedByColor(arr, C8, true) then
|
!isAttackedByColor(arr, C8, true)
|
||||||
|
then
|
||||||
buf(n) = encMove(E8, C8, KIND_CASTLEQ); n += 1
|
buf(n) = encMove(E8, C8, KIND_CASTLEQ); n += 1
|
||||||
n
|
n
|
||||||
|
|
||||||
private def generatePawnMoves(arr: Array[Int], from: Int, isWhite: Boolean, ctx: GameContext, buf: Array[Int], start: Int): Int =
|
private def generatePawnMoves(
|
||||||
|
arr: Array[Int],
|
||||||
|
from: Int,
|
||||||
|
isWhite: Boolean,
|
||||||
|
ctx: GameContext,
|
||||||
|
buf: Array[Int],
|
||||||
|
start: Int,
|
||||||
|
): Int =
|
||||||
var n = start
|
var n = start
|
||||||
val f = fileOf(from); val r = rankOf(from)
|
val f = fileOf(from); val r = rankOf(from)
|
||||||
val fwd = if isWhite then 1 else -1
|
val fwd = if isWhite then 1 else -1
|
||||||
@@ -531,7 +579,12 @@ object DefaultRules extends RuleSet:
|
|||||||
|
|
||||||
// ─── Threefold repetition ─────────────────────────────────────────────────
|
// ─── Threefold repetition ─────────────────────────────────────────────────
|
||||||
|
|
||||||
private case class Position(board: Board, turn: Color, castlingRights: CastlingRights, enPassantSquare: Option[Square])
|
private case class Position(
|
||||||
|
board: Board,
|
||||||
|
turn: Color,
|
||||||
|
castlingRights: CastlingRights,
|
||||||
|
enPassantSquare: Option[Square],
|
||||||
|
)
|
||||||
|
|
||||||
private def countPositionOccurrences(context: GameContext, target: Position): Int =
|
private def countPositionOccurrences(context: GameContext, target: Position): Int =
|
||||||
try
|
try
|
||||||
@@ -557,5 +610,13 @@ object DefaultRules extends RuleSet:
|
|||||||
(nextCtx, nextCount)
|
(nextCtx, nextCount)
|
||||||
}
|
}
|
||||||
._2
|
._2
|
||||||
catch
|
catch case _: Exception => 1
|
||||||
case _: Exception => 1
|
|
||||||
|
override def postMoveStatus(context: GameContext): PostMoveStatus =
|
||||||
|
PostMoveStatus(
|
||||||
|
isCheckmate = isCheckmate(context),
|
||||||
|
isStalemate = isStalemate(context),
|
||||||
|
isInsufficientMaterial = isInsufficientMaterial(context),
|
||||||
|
isCheck = isCheck(context),
|
||||||
|
isThreefoldRepetition = isThreefoldRepetition(context),
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user