feat: NCS-11 implement 50-move rule draw claim and observer events
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,19 @@ class GameEngine(
|
|||||||
case "redo" =>
|
case "redo" =>
|
||||||
performRedo()
|
performRedo()
|
||||||
|
|
||||||
|
case "draw" =>
|
||||||
|
if currentHistory.halfMoveClock >= 100 then
|
||||||
|
currentBoard = Board.initial
|
||||||
|
currentHistory = GameHistory.empty
|
||||||
|
currentTurn = Color.White
|
||||||
|
invoker.clear()
|
||||||
|
notifyObservers(DrawClaimedEvent(currentBoard, currentHistory, currentTurn))
|
||||||
|
else
|
||||||
|
notifyObservers(InvalidMoveEvent(
|
||||||
|
currentBoard, currentHistory, currentTurn,
|
||||||
|
"Draw cannot be claimed: the 50-move rule has not been triggered."
|
||||||
|
))
|
||||||
|
|
||||||
case "" =>
|
case "" =>
|
||||||
val event = InvalidMoveEvent(
|
val event = InvalidMoveEvent(
|
||||||
currentBoard,
|
currentBoard,
|
||||||
@@ -109,6 +122,8 @@ class GameEngine(
|
|||||||
invoker.execute(updatedCmd)
|
invoker.execute(updatedCmd)
|
||||||
updateGameState(newBoard, newHistory, newTurn)
|
updateGameState(newBoard, newHistory, newTurn)
|
||||||
emitMoveEvent(from.toString, to.toString, captured, newTurn)
|
emitMoveEvent(from.toString, to.toString, captured, newTurn)
|
||||||
|
if currentHistory.halfMoveClock >= 100 then
|
||||||
|
notifyObservers(FiftyMoveRuleAvailableEvent(currentBoard, currentHistory, currentTurn))
|
||||||
|
|
||||||
case MoveResult.MovedInCheck(newBoard, newHistory, captured, newTurn) =>
|
case MoveResult.MovedInCheck(newBoard, newHistory, captured, newTurn) =>
|
||||||
// Move succeeded with check
|
// Move succeeded with check
|
||||||
@@ -117,6 +132,8 @@ class GameEngine(
|
|||||||
updateGameState(newBoard, newHistory, newTurn)
|
updateGameState(newBoard, newHistory, newTurn)
|
||||||
emitMoveEvent(from.toString, to.toString, captured, newTurn)
|
emitMoveEvent(from.toString, to.toString, captured, newTurn)
|
||||||
notifyObservers(CheckDetectedEvent(currentBoard, currentHistory, currentTurn))
|
notifyObservers(CheckDetectedEvent(currentBoard, currentHistory, currentTurn))
|
||||||
|
if currentHistory.halfMoveClock >= 100 then
|
||||||
|
notifyObservers(FiftyMoveRuleAvailableEvent(currentBoard, currentHistory, currentTurn))
|
||||||
|
|
||||||
case MoveResult.Checkmate(winner) =>
|
case MoveResult.Checkmate(winner) =>
|
||||||
// Move resulted in checkmate
|
// Move resulted in checkmate
|
||||||
|
|||||||
@@ -67,6 +67,20 @@ case class BoardResetEvent(
|
|||||||
turn: Color
|
turn: Color
|
||||||
) extends GameEvent
|
) extends GameEvent
|
||||||
|
|
||||||
|
/** Fired after any move where the half-move clock reaches 100 — the 50-move rule is now claimable. */
|
||||||
|
case class FiftyMoveRuleAvailableEvent(
|
||||||
|
board: Board,
|
||||||
|
history: GameHistory,
|
||||||
|
turn: Color
|
||||||
|
) extends GameEvent
|
||||||
|
|
||||||
|
/** Fired when a player successfully claims a draw under the 50-move rule. */
|
||||||
|
case class DrawClaimedEvent(
|
||||||
|
board: Board,
|
||||||
|
history: GameHistory,
|
||||||
|
turn: Color
|
||||||
|
) extends GameEvent
|
||||||
|
|
||||||
/** Observer trait: implement to receive game state updates. */
|
/** Observer trait: implement to receive game state updates. */
|
||||||
trait Observer:
|
trait Observer:
|
||||||
def onGameEvent(event: GameEvent): Unit
|
def onGameEvent(event: GameEvent): Unit
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package de.nowchess.chess.engine
|
|||||||
import scala.collection.mutable
|
import scala.collection.mutable
|
||||||
import de.nowchess.api.board.{Board, Color}
|
import de.nowchess.api.board.{Board, Color}
|
||||||
import de.nowchess.chess.logic.GameHistory
|
import de.nowchess.chess.logic.GameHistory
|
||||||
import de.nowchess.chess.observer.{Observer, GameEvent, MoveExecutedEvent, CheckDetectedEvent, BoardResetEvent, InvalidMoveEvent}
|
import de.nowchess.chess.observer.{Observer, GameEvent, MoveExecutedEvent, CheckDetectedEvent, BoardResetEvent, InvalidMoveEvent, FiftyMoveRuleAvailableEvent, DrawClaimedEvent}
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
@@ -302,6 +302,47 @@ class GameEngineTest extends AnyFunSuite with Matchers:
|
|||||||
engine.board shouldBe boardAfterSecondMove
|
engine.board shouldBe boardAfterSecondMove
|
||||||
engine.turn shouldBe Color.White
|
engine.turn shouldBe Color.White
|
||||||
|
|
||||||
|
// ──── 50-move rule ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
test("GameEngine: 'draw' rejected when halfMoveClock < 100"):
|
||||||
|
val engine = new GameEngine()
|
||||||
|
val observer = new MockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
engine.processUserInput("draw")
|
||||||
|
observer.events.size shouldBe 1
|
||||||
|
observer.events.head shouldBe a[InvalidMoveEvent]
|
||||||
|
|
||||||
|
test("GameEngine: 'draw' accepted and fires DrawClaimedEvent when halfMoveClock >= 100"):
|
||||||
|
val engine = new GameEngine(initialHistory = GameHistory(halfMoveClock = 100))
|
||||||
|
val observer = new MockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
engine.processUserInput("draw")
|
||||||
|
observer.events.size shouldBe 1
|
||||||
|
observer.events.head shouldBe a[DrawClaimedEvent]
|
||||||
|
|
||||||
|
test("GameEngine: state resets to initial after draw claimed"):
|
||||||
|
val engine = new GameEngine(initialHistory = GameHistory(halfMoveClock = 100))
|
||||||
|
engine.processUserInput("draw")
|
||||||
|
engine.board shouldBe Board.initial
|
||||||
|
engine.history shouldBe GameHistory.empty
|
||||||
|
engine.turn shouldBe Color.White
|
||||||
|
|
||||||
|
test("GameEngine: FiftyMoveRuleAvailableEvent fired when move brings clock to 100"):
|
||||||
|
// Start at clock 99; a knight move (non-pawn, non-capture) increments to 100
|
||||||
|
val engine = new GameEngine(initialHistory = GameHistory(halfMoveClock = 99))
|
||||||
|
val observer = new MockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
engine.processUserInput("g1f3") // knight move on initial board
|
||||||
|
// Should receive MoveExecutedEvent AND FiftyMoveRuleAvailableEvent
|
||||||
|
observer.events.exists(_.isInstanceOf[FiftyMoveRuleAvailableEvent]) shouldBe true
|
||||||
|
|
||||||
|
test("GameEngine: FiftyMoveRuleAvailableEvent not fired when clock is below 100 after move"):
|
||||||
|
val engine = new GameEngine(initialHistory = GameHistory(halfMoveClock = 5))
|
||||||
|
val observer = new MockObserver()
|
||||||
|
engine.subscribe(observer)
|
||||||
|
engine.processUserInput("g1f3")
|
||||||
|
observer.events.exists(_.isInstanceOf[FiftyMoveRuleAvailableEvent]) shouldBe false
|
||||||
|
|
||||||
// Mock Observer for testing
|
// Mock Observer for testing
|
||||||
private class MockObserver extends Observer:
|
private class MockObserver extends Observer:
|
||||||
val events = mutable.ListBuffer[GameEvent]()
|
val events = mutable.ListBuffer[GameEvent]()
|
||||||
|
|||||||
@@ -67,3 +67,21 @@ class FenExporterTest extends AnyFunSuite with Matchers:
|
|||||||
)
|
)
|
||||||
val fen = FenExporter.gameStateToFen(gameState)
|
val fen = FenExporter.gameStateToFen(gameState)
|
||||||
fen shouldBe "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR w KQkq c6 2 3"
|
fen shouldBe "rnbqkbnr/pp1ppppp/8/2pP4/8/8/PPPP1PPP/RNBQKBNR w KQkq c6 2 3"
|
||||||
|
|
||||||
|
test("halfMoveClock round-trips through FEN export and import"):
|
||||||
|
import de.nowchess.chess.logic.GameHistory
|
||||||
|
import de.nowchess.chess.notation.FenParser
|
||||||
|
val history = GameHistory(halfMoveClock = 42)
|
||||||
|
val gameState = GameState(
|
||||||
|
piecePlacement = FenExporter.boardToFen(de.nowchess.api.board.Board.initial),
|
||||||
|
activeColor = Color.White,
|
||||||
|
castlingWhite = CastlingRights.Both,
|
||||||
|
castlingBlack = CastlingRights.Both,
|
||||||
|
enPassantTarget = None,
|
||||||
|
halfMoveClock = history.halfMoveClock,
|
||||||
|
fullMoveNumber = 1,
|
||||||
|
status = GameStatus.InProgress
|
||||||
|
)
|
||||||
|
val fen = FenExporter.gameStateToFen(gameState)
|
||||||
|
val parsed = FenParser.parseFen(fen).get
|
||||||
|
parsed.halfMoveClock shouldBe 42
|
||||||
|
|||||||
Reference in New Issue
Block a user