feat: Implement threefold repetition detection and update game rules
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
This commit is contained in:
@@ -19,20 +19,17 @@ class GameEngine(
|
||||
val ruleSet: RuleSet = DefaultRules,
|
||||
val participants: Map[Color, Participant] = Map(Color.White -> Human, Color.Black -> Human),
|
||||
) extends Observable:
|
||||
// Ensure that initialBoard is set correctly for threefold repetition detection
|
||||
private val contextWithInitialBoard = if initialContext.moves.isEmpty && initialContext.board != initialContext.initialBoard then
|
||||
initialContext.copy(initialBoard = initialContext.board)
|
||||
else
|
||||
initialContext
|
||||
@SuppressWarnings(Array("DisableSyntax.var"))
|
||||
private var currentContext: GameContext = initialContext
|
||||
private var currentContext: GameContext = contextWithInitialBoard
|
||||
private val invoker = new CommandInvoker()
|
||||
|
||||
/** Pending promotion: the Move that triggered it (from/to only, moveType filled in later). */
|
||||
private case class PendingPromotion(from: Square, to: Square, contextBefore: GameContext)
|
||||
@SuppressWarnings(Array("DisableSyntax.var"))
|
||||
private var pendingPromotion: Option[PendingPromotion] = None
|
||||
|
||||
private implicit val ec: ExecutionContext = ExecutionContext.global
|
||||
|
||||
/** True if a pawn promotion move is pending and needs a piece choice. */
|
||||
def isPendingPromotion: Boolean = synchronized(pendingPromotion.isDefined)
|
||||
|
||||
// Synchronized accessors for current state
|
||||
def board: Board = synchronized(currentContext.board)
|
||||
def turn: Color = synchronized(currentContext.turn)
|
||||
@@ -67,11 +64,15 @@ class GameEngine(
|
||||
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,
|
||||
"Draw cannot be claimed: the 50-move rule has not been triggered.",
|
||||
"Draw cannot be claimed: neither the 50-move rule nor threefold repetition has been triggered.",
|
||||
),
|
||||
)
|
||||
|
||||
@@ -87,11 +88,11 @@ class GameEngine(
|
||||
s"Invalid move format '$moveInput'. Use coordinate notation, e.g. e2e4.",
|
||||
),
|
||||
)
|
||||
case Some((from, to)) =>
|
||||
handleParsedMove(from, to)
|
||||
case Some((from, to, promotionPiece: Option[PromotionPiece])) =>
|
||||
handleParsedMove(from, to, promotionPiece)
|
||||
}
|
||||
|
||||
private def handleParsedMove(from: Square, to: Square): Unit =
|
||||
private def handleParsedMove(from: Square, to: Square, promotionPiece: Option[PromotionPiece]): Unit =
|
||||
currentContext.board.pieceAt(from) match
|
||||
case None =>
|
||||
notifyObservers(InvalidMoveEvent(currentContext, "No piece on that square."))
|
||||
@@ -104,11 +105,13 @@ class GameEngine(
|
||||
candidates match
|
||||
case Nil =>
|
||||
notifyObservers(InvalidMoveEvent(currentContext, "Illegal move."))
|
||||
case moves if isPromotionMove(piece, to) =>
|
||||
// Multiple moves (one per promotion piece) — ask user to choose
|
||||
val contextBefore = currentContext
|
||||
pendingPromotion = Some(PendingPromotion(from, to, contextBefore))
|
||||
notifyObservers(PromotionRequiredEvent(currentContext, from, to))
|
||||
case _ if isPromotionMove(piece, to) =>
|
||||
if promotionPiece.isEmpty then
|
||||
notifyObservers(InvalidMoveEvent(currentContext, "Promotion piece required: append q, r, b, or n to the move."))
|
||||
else
|
||||
candidates.find(_.moveType == MoveType.Promotion(promotionPiece.get)) match
|
||||
case None => notifyObservers(InvalidMoveEvent(currentContext, "Error completing promotion: no matching legal move."))
|
||||
case Some(move) => executeMove(move)
|
||||
case move :: _ =>
|
||||
executeMove(move)
|
||||
|
||||
@@ -118,21 +121,6 @@ class GameEngine(
|
||||
to.rank.ordinal == promoRank
|
||||
}
|
||||
|
||||
/** Apply a player's promotion piece choice. Must only be called when isPendingPromotion is true.
|
||||
*/
|
||||
def completePromotion(piece: PromotionPiece): Unit = synchronized {
|
||||
pendingPromotion match
|
||||
case None =>
|
||||
notifyObservers(InvalidMoveEvent(currentContext, "No promotion pending."))
|
||||
case Some(pending) =>
|
||||
pendingPromotion = None
|
||||
val move = Move(pending.from, pending.to, MoveType.Promotion(piece))
|
||||
// Verify it's actually legal
|
||||
val legal = ruleSet.legalMoves(currentContext)(pending.from)
|
||||
if legal.contains(move) then executeMove(move)
|
||||
else notifyObservers(InvalidMoveEvent(currentContext, "Error completing promotion."))
|
||||
}
|
||||
|
||||
/** Undo the last move. */
|
||||
def undo(): Unit = synchronized(performUndo())
|
||||
|
||||
@@ -154,11 +142,10 @@ class GameEngine(
|
||||
private def replayGame(ctx: GameContext): Either[String, Unit] =
|
||||
val savedContext = currentContext
|
||||
currentContext = GameContext.initial
|
||||
pendingPromotion = None
|
||||
invoker.clear()
|
||||
|
||||
if ctx.moves.isEmpty then
|
||||
currentContext = ctx
|
||||
currentContext = ctx.copy(initialBoard = ctx.board)
|
||||
Right(())
|
||||
else replayMoves(ctx.moves, savedContext)
|
||||
|
||||
@@ -170,14 +157,13 @@ class GameEngine(
|
||||
result
|
||||
|
||||
private def applyReplayMove(move: Move): Either[String, Unit] =
|
||||
handleParsedMove(move.from, move.to)
|
||||
move.moveType match
|
||||
case MoveType.Promotion(pp) if pendingPromotion.isDefined =>
|
||||
completePromotion(pp)
|
||||
Right(())
|
||||
case MoveType.Promotion(_) =>
|
||||
Left(s"Promotion required for move ${move.from}${move.to}")
|
||||
case _ => Right(())
|
||||
val legal = ruleSet.legalMoves(currentContext)(move.from)
|
||||
val candidate = move.moveType match
|
||||
case MoveType.Promotion(pp) => legal.find(m => m.to == move.to && m.moveType == MoveType.Promotion(pp))
|
||||
case _ => legal.find(_.to == move.to)
|
||||
candidate match
|
||||
case None => Left("Illegal move.")
|
||||
case Some(lm) => executeMove(lm); Right(())
|
||||
|
||||
/** Export the current game context using the provided exporter. */
|
||||
def exportGame(exporter: GameContextExport): String = synchronized {
|
||||
@@ -186,7 +172,11 @@ class GameEngine(
|
||||
|
||||
/** Load an arbitrary board position, clearing all history and undo/redo state. */
|
||||
def loadPosition(newContext: GameContext): Unit = synchronized {
|
||||
currentContext = newContext
|
||||
val contextWithInitialBoard = if newContext.moves.isEmpty then
|
||||
newContext.copy(initialBoard = newContext.board)
|
||||
else
|
||||
newContext
|
||||
currentContext = contextWithInitialBoard
|
||||
invoker.clear()
|
||||
notifyObservers(BoardResetEvent(currentContext))
|
||||
}
|
||||
@@ -243,10 +233,7 @@ class GameEngine(
|
||||
else if ruleSet.isCheck(currentContext) then notifyObservers(CheckDetectedEvent(currentContext))
|
||||
|
||||
if currentContext.halfMoveClock >= 100 then notifyObservers(FiftyMoveRuleAvailableEvent(currentContext))
|
||||
|
||||
// Request bot move if it's the opponent bot's turn
|
||||
if ruleSet.isCheckmate(currentContext) || ruleSet.isStalemate(currentContext) then
|
||||
() // Game is over, don't request bot move
|
||||
if ruleSet.isThreefoldRepetition(currentContext) then notifyObservers(ThreefoldRepetitionAvailableEvent(currentContext))
|
||||
else requestBotMoveIfNeeded()
|
||||
|
||||
private def translateMoveToNotation(move: Move, boardBefore: Board): String =
|
||||
@@ -336,7 +323,7 @@ class GameEngine(
|
||||
if ruleSet.isCheckmate(currentContext) then
|
||||
val winner = currentContext.turn.opposite
|
||||
notifyObservers(CheckmateEvent(currentContext, winner))
|
||||
else if ruleSet.isStalemate(currentContext) then notifyObservers(StalemateEvent(currentContext))
|
||||
else if ruleSet.isStalemate(currentContext) then notifyObservers(DrawEvent(currentContext, DrawReason.Stalemate))
|
||||
}
|
||||
|
||||
private def performUndo(): Unit =
|
||||
|
||||
Reference in New Issue
Block a user