feat: Rework draw handling in GameEngine and GameResource
This commit is contained in:
@@ -38,9 +38,10 @@ class GameEngine(
|
|||||||
private implicit val ec: ExecutionContext = ExecutionContext.global
|
private implicit val ec: ExecutionContext = ExecutionContext.global
|
||||||
|
|
||||||
// Synchronized accessors for current state
|
// Synchronized accessors for current state
|
||||||
def board: Board = synchronized(currentContext.board)
|
def board: Board = synchronized(currentContext.board)
|
||||||
def turn: Color = synchronized(currentContext.turn)
|
def turn: Color = synchronized(currentContext.turn)
|
||||||
def context: GameContext = synchronized(currentContext)
|
def context: GameContext = synchronized(currentContext)
|
||||||
|
def pendingDrawOfferBy: Option[Color] = synchronized(pendingDrawOffer)
|
||||||
|
|
||||||
/** Check if undo is available. */
|
/** Check if undo is available. */
|
||||||
def canUndo: Boolean = synchronized(invoker.canUndo)
|
def canUndo: Boolean = synchronized(invoker.canUndo)
|
||||||
@@ -67,21 +68,7 @@ class GameEngine(
|
|||||||
performRedo()
|
performRedo()
|
||||||
|
|
||||||
case "draw" =>
|
case "draw" =>
|
||||||
if currentContext.halfMoveClock >= 100 then
|
claimDraw()
|
||||||
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.FiftyMoveRule)))
|
|
||||||
invoker.clear()
|
|
||||||
notifyObservers(DrawEvent(currentContext, DrawReason.FiftyMoveRule))
|
|
||||||
else if ruleSet.isThreefoldRepetition(currentContext) then
|
|
||||||
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.ThreefoldRepetition)))
|
|
||||||
invoker.clear()
|
|
||||||
notifyObservers(DrawEvent(currentContext, DrawReason.ThreefoldRepetition))
|
|
||||||
else
|
|
||||||
notifyObservers(
|
|
||||||
InvalidMoveEvent(
|
|
||||||
currentContext,
|
|
||||||
InvalidMoveReason.DrawCannotBeClaimed,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
case "" =>
|
case "" =>
|
||||||
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.EmptyInput))
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.EmptyInput))
|
||||||
@@ -195,6 +182,22 @@ class GameEngine(
|
|||||||
notifyObservers(DrawOfferDeclinedEvent(currentContext, color))
|
notifyObservers(DrawOfferDeclinedEvent(currentContext, color))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Claim a draw by fifty-move rule or threefold repetition. */
|
||||||
|
def claimDraw(): Unit = synchronized {
|
||||||
|
if currentContext.result.isDefined then
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.GameAlreadyOver))
|
||||||
|
else if currentContext.halfMoveClock >= 100 then
|
||||||
|
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.FiftyMoveRule)))
|
||||||
|
invoker.clear()
|
||||||
|
notifyObservers(DrawEvent(currentContext, DrawReason.FiftyMoveRule))
|
||||||
|
else if ruleSet.isThreefoldRepetition(currentContext) then
|
||||||
|
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.ThreefoldRepetition)))
|
||||||
|
invoker.clear()
|
||||||
|
notifyObservers(DrawEvent(currentContext, DrawReason.ThreefoldRepetition))
|
||||||
|
else
|
||||||
|
notifyObservers(InvalidMoveEvent(currentContext, InvalidMoveReason.DrawCannotBeClaimed))
|
||||||
|
}
|
||||||
|
|
||||||
/** Load a game using the provided importer. If the imported context has moves, they are replayed through the command
|
/** Load a game using the provided importer. If the imported context has moves, they are replayed through the command
|
||||||
* system. Otherwise, the position is set directly. Notifies observers with PgnLoadedEvent on success.
|
* system. Otherwise, the position is set directly. Notifies observers with PgnLoadedEvent on success.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -9,6 +9,5 @@ final case class GameEntry(
|
|||||||
engine: GameEngine,
|
engine: GameEngine,
|
||||||
white: PlayerInfo,
|
white: PlayerInfo,
|
||||||
black: PlayerInfo,
|
black: PlayerInfo,
|
||||||
drawOfferedBy: Option[Color] = None,
|
|
||||||
resigned: Boolean = false,
|
resigned: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class GameResource(@Inject val registry: GameRegistry, @Inject val objectMapper:
|
|||||||
// ── mapping ──────────────────────────────────────────────────────────────
|
// ── mapping ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private def statusOf(entry: GameEntry): String =
|
private def statusOf(entry: GameEntry): String =
|
||||||
if entry.drawOfferedBy.isDefined then "drawOffered"
|
if entry.engine.pendingDrawOfferBy.isDefined then "drawOffered"
|
||||||
else
|
else
|
||||||
val ctx = entry.engine.context
|
val ctx = entry.engine.context
|
||||||
ctx.result match
|
ctx.result match
|
||||||
@@ -104,7 +104,7 @@ class GameResource(@Inject val registry: GameRegistry, @Inject val objectMapper:
|
|||||||
val error = new AtomicReference[Option[String]](None)
|
val error = new AtomicReference[Option[String]](None)
|
||||||
val obs = new Observer:
|
val obs = new Observer:
|
||||||
def onGameEvent(e: GameEvent): Unit = e match
|
def onGameEvent(e: GameEvent): Unit = e match
|
||||||
case InvalidMoveEvent(_, reason) => error.set(Some(reason))
|
case InvalidMoveEvent(_, reason) => error.set(Some(reason.toString))
|
||||||
case _ => ()
|
case _ => ()
|
||||||
engine.subscribe(obs)
|
engine.subscribe(obs)
|
||||||
engine.processUserInput(uci)
|
engine.processUserInput(uci)
|
||||||
@@ -230,26 +230,16 @@ class GameResource(@Inject val registry: GameRegistry, @Inject val objectMapper:
|
|||||||
if entry.engine.context.result.isDefined then throw BadRequestException("GAME_OVER", "Game is already over")
|
if entry.engine.context.result.isDefined then throw BadRequestException("GAME_OVER", "Game is already over")
|
||||||
action match
|
action match
|
||||||
case "offer" =>
|
case "offer" =>
|
||||||
registry.update(entry.copy(drawOfferedBy = Some(entry.engine.context.turn)))
|
entry.engine.offerDraw(entry.engine.context.turn)
|
||||||
ok(OkResponseDto())
|
ok(OkResponseDto())
|
||||||
case "accept" =>
|
case "accept" =>
|
||||||
entry.drawOfferedBy match
|
entry.engine.acceptDraw(entry.engine.context.turn)
|
||||||
case None =>
|
ok(OkResponseDto())
|
||||||
throw BadRequestException("NO_DRAW_OFFER", "No draw offer to accept")
|
|
||||||
case Some(offerer) if offerer == entry.engine.context.turn =>
|
|
||||||
throw BadRequestException("CANNOT_ACCEPT_OWN_OFFER", "Cannot accept your own draw offer")
|
|
||||||
case _ =>
|
|
||||||
entry.engine.applyDraw(DrawReason.Agreement)
|
|
||||||
registry.update(entry.copy(drawOfferedBy = None))
|
|
||||||
ok(OkResponseDto())
|
|
||||||
case "decline" =>
|
case "decline" =>
|
||||||
if entry.drawOfferedBy.isEmpty then throw BadRequestException("NO_DRAW_OFFER", "No draw offer to decline")
|
entry.engine.declineDraw(entry.engine.context.turn)
|
||||||
registry.update(entry.copy(drawOfferedBy = None))
|
|
||||||
ok(OkResponseDto())
|
ok(OkResponseDto())
|
||||||
case "claim" =>
|
case "claim" =>
|
||||||
if entry.engine.context.halfMoveClock < 100 then
|
entry.engine.claimDraw()
|
||||||
throw BadRequestException("CLAIM_NOT_AVAILABLE", "Fifty-move rule draw is not available")
|
|
||||||
entry.engine.applyDraw(DrawReason.FiftyMoveRule)
|
|
||||||
ok(OkResponseDto())
|
ok(OkResponseDto())
|
||||||
case _ =>
|
case _ =>
|
||||||
throw BadRequestException("INVALID_ACTION", s"Unknown draw action: $action", Some("action"))
|
throw BadRequestException("INVALID_ACTION", s"Unknown draw action: $action", Some("action"))
|
||||||
|
|||||||
Reference in New Issue
Block a user