From 7b24f02476ec4caffffcf48beef3abd9e9d2ff6d Mon Sep 17 00:00:00 2001 From: Janis Date: Tue, 14 Apr 2026 21:12:32 +0200 Subject: [PATCH] feat: add result tracking to GameContext and update GameEngine for game outcomes --- .../scala/de/nowchess/api/game/GameContext.scala | 4 ++++ .../de/nowchess/api/game/GameContextTest.scala | 14 ++++++++++++++ .../de/nowchess/chess/engine/GameEngine.scala | 6 +++++- .../chess/engine/GameEngineOutcomesTest.scala | 7 ++++++- 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala b/modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala index 1ede764..cde9b01 100644 --- a/modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala +++ b/modules/api/src/main/scala/de/nowchess/api/game/GameContext.scala @@ -12,6 +12,7 @@ case class GameContext( enPassantSquare: Option[Square], halfMoveClock: Int, moves: List[Move], + result: Option[GameResult] = None, ): /** Create new context with updated board. */ def withBoard(newBoard: Board): GameContext = copy(board = newBoard) @@ -31,6 +32,9 @@ case class GameContext( /** Create new context with move appended to history. */ def withMove(move: Move): GameContext = copy(moves = moves :+ move) + /** Create new context with updated result. */ + def withResult(newResult: Option[GameResult]): GameContext = copy(result = newResult) + object GameContext: /** Initial position: white to move, all castling rights, no en passant. */ def initial: GameContext = GameContext( diff --git a/modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala b/modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala index d5d6759..0b57258 100644 --- a/modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala +++ b/modules/api/src/test/scala/de/nowchess/api/game/GameContextTest.scala @@ -2,6 +2,7 @@ package de.nowchess.api.game import de.nowchess.api.board.{Board, CastlingRights, Color, File, Rank, Square} import de.nowchess.api.move.Move +import de.nowchess.api.game.{DrawReason, GameResult} import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -16,6 +17,7 @@ class GameContextTest extends AnyFunSuite with Matchers: initial.enPassantSquare shouldBe None initial.halfMoveClock shouldBe 0 initial.moves shouldBe List.empty + initial.result shouldBe None test("withBoard updates only board"): val square = Square(File.E, Rank.R4) @@ -57,3 +59,15 @@ class GameContextTest extends AnyFunSuite with Matchers: test("withMove appends move to history"): val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) GameContext.initial.withMove(move).moves shouldBe List(move) + + test("withResult sets Win result"): + val win = Some(GameResult.Win(Color.White)) + GameContext.initial.withResult(win).result shouldBe win + + test("withResult sets Draw result"): + val draw = Some(GameResult.Draw(DrawReason.Stalemate)) + GameContext.initial.withResult(draw).result shouldBe draw + + test("withResult clears result"): + val ctx = GameContext.initial.withResult(Some(GameResult.Win(Color.Black))) + ctx.withResult(None).result shouldBe None diff --git a/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala b/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala index 0460c5b..635a575 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala @@ -2,7 +2,7 @@ package de.nowchess.chess.engine import de.nowchess.api.board.{Board, Color, Piece, PieceType, Square} import de.nowchess.api.move.{Move, MoveType, PromotionPiece} -import de.nowchess.api.game.{DrawReason, GameContext} +import de.nowchess.api.game.{DrawReason, GameContext, GameResult} import de.nowchess.chess.controller.Parser import de.nowchess.chess.observer.* import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult} @@ -60,6 +60,7 @@ class GameEngine( case "draw" => if currentContext.halfMoveClock >= 100 then + currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.FiftyMoveRule))) invoker.clear() notifyObservers(DrawEvent(currentContext, DrawReason.FiftyMoveRule)) else @@ -222,12 +223,15 @@ class GameEngine( if ruleSet.isCheckmate(currentContext) then val winner = currentContext.turn.opposite + currentContext = currentContext.withResult(Some(GameResult.Win(winner))) notifyObservers(CheckmateEvent(currentContext, winner)) invoker.clear() else if ruleSet.isStalemate(currentContext) then + currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.Stalemate))) notifyObservers(DrawEvent(currentContext, DrawReason.Stalemate)) invoker.clear() else if ruleSet.isInsufficientMaterial(currentContext) then + currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.InsufficientMaterial))) notifyObservers(DrawEvent(currentContext, DrawReason.InsufficientMaterial)) invoker.clear() else if ruleSet.isCheck(currentContext) then notifyObservers(CheckDetectedEvent(currentContext)) diff --git a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineOutcomesTest.scala b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineOutcomesTest.scala index 3ea402f..7d2881d 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineOutcomesTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineOutcomesTest.scala @@ -1,7 +1,7 @@ package de.nowchess.chess.engine import de.nowchess.api.board.Color -import de.nowchess.api.game.DrawReason +import de.nowchess.api.game.{DrawReason, GameResult} import de.nowchess.chess.observer.* import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -23,6 +23,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers: engine.processUserInput("d8h4") observer.hasEvent[CheckmateEvent] shouldBe true + engine.context.result shouldBe Some(GameResult.Win(Color.Black)) test("checkmate with white winner"): val engine = EngineTestHelpers.makeEngine() @@ -42,6 +43,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers: val evt = observer.getEvent[CheckmateEvent] evt.isDefined shouldBe true evt.get.winner shouldBe Color.White + engine.context.result shouldBe Some(GameResult.Win(Color.White)) // ── Stalemate ─────────────────────────────────────────────────── @@ -78,6 +80,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers: val evt = observer.getEvent[DrawEvent] evt.isDefined shouldBe true evt.get.reason shouldBe DrawReason.Stalemate + engine.context.result shouldBe Some(GameResult.Draw(DrawReason.Stalemate)) test("stalemate board is not reset after draw"): val engine = EngineTestHelpers.makeEngine() @@ -189,6 +192,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers: val evt = observer.getEvent[DrawEvent] evt.isDefined shouldBe true evt.get.reason shouldBe DrawReason.FiftyMoveRule + engine.context.result shouldBe Some(GameResult.Draw(DrawReason.FiftyMoveRule)) test("draw cannot be claimed when not available"): val engine = EngineTestHelpers.makeEngine() @@ -216,3 +220,4 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers: val evt = observer.getEvent[DrawEvent] evt.isDefined shouldBe true evt.get.reason shouldBe DrawReason.InsufficientMaterial + engine.context.result shouldBe Some(GameResult.Draw(DrawReason.InsufficientMaterial))