feat: add result tracking to GameContext and update GameEngine for game outcomes
Build & Test (NowChessSystems) TeamCity build failed

This commit is contained in:
2026-04-14 21:12:32 +02:00
parent 247724e57c
commit 7b24f02476
4 changed files with 29 additions and 2 deletions
@@ -12,6 +12,7 @@ case class GameContext(
enPassantSquare: Option[Square], enPassantSquare: Option[Square],
halfMoveClock: Int, halfMoveClock: Int,
moves: List[Move], moves: List[Move],
result: Option[GameResult] = None,
): ):
/** Create new context with updated board. */ /** Create new context with updated board. */
def withBoard(newBoard: Board): GameContext = copy(board = newBoard) def withBoard(newBoard: Board): GameContext = copy(board = newBoard)
@@ -31,6 +32,9 @@ case class GameContext(
/** Create new context with move appended to history. */ /** Create new context with move appended to history. */
def withMove(move: Move): GameContext = copy(moves = moves :+ move) 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: object GameContext:
/** Initial position: white to move, all castling rights, no en passant. */ /** Initial position: white to move, all castling rights, no en passant. */
def initial: GameContext = GameContext( def initial: GameContext = GameContext(
@@ -2,6 +2,7 @@ package de.nowchess.api.game
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Rank, Square} import de.nowchess.api.board.{Board, CastlingRights, Color, File, Rank, Square}
import de.nowchess.api.move.Move import de.nowchess.api.move.Move
import de.nowchess.api.game.{DrawReason, GameResult}
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
@@ -16,6 +17,7 @@ class GameContextTest extends AnyFunSuite with Matchers:
initial.enPassantSquare shouldBe None initial.enPassantSquare shouldBe None
initial.halfMoveClock shouldBe 0 initial.halfMoveClock shouldBe 0
initial.moves shouldBe List.empty initial.moves shouldBe List.empty
initial.result shouldBe None
test("withBoard updates only board"): test("withBoard updates only board"):
val square = Square(File.E, Rank.R4) val square = Square(File.E, Rank.R4)
@@ -57,3 +59,15 @@ class GameContextTest extends AnyFunSuite with Matchers:
test("withMove appends move to history"): test("withMove appends move to history"):
val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4)) val move = Move(Square(File.E, Rank.R2), Square(File.E, Rank.R4))
GameContext.initial.withMove(move).moves shouldBe List(move) 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
@@ -2,7 +2,7 @@ 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.{DrawReason, GameContext} import de.nowchess.api.game.{DrawReason, GameContext, GameResult}
import de.nowchess.chess.controller.Parser import de.nowchess.chess.controller.Parser
import de.nowchess.chess.observer.* import de.nowchess.chess.observer.*
import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult} import de.nowchess.chess.command.{CommandInvoker, MoveCommand, MoveResult}
@@ -60,6 +60,7 @@ class GameEngine(
case "draw" => case "draw" =>
if currentContext.halfMoveClock >= 100 then if currentContext.halfMoveClock >= 100 then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.FiftyMoveRule)))
invoker.clear() invoker.clear()
notifyObservers(DrawEvent(currentContext, DrawReason.FiftyMoveRule)) notifyObservers(DrawEvent(currentContext, DrawReason.FiftyMoveRule))
else else
@@ -222,12 +223,15 @@ class GameEngine(
if ruleSet.isCheckmate(currentContext) then if ruleSet.isCheckmate(currentContext) then
val winner = currentContext.turn.opposite val winner = currentContext.turn.opposite
currentContext = currentContext.withResult(Some(GameResult.Win(winner)))
notifyObservers(CheckmateEvent(currentContext, winner)) notifyObservers(CheckmateEvent(currentContext, winner))
invoker.clear() invoker.clear()
else if ruleSet.isStalemate(currentContext) then else if ruleSet.isStalemate(currentContext) then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.Stalemate)))
notifyObservers(DrawEvent(currentContext, DrawReason.Stalemate)) notifyObservers(DrawEvent(currentContext, DrawReason.Stalemate))
invoker.clear() invoker.clear()
else if ruleSet.isInsufficientMaterial(currentContext) then else if ruleSet.isInsufficientMaterial(currentContext) then
currentContext = currentContext.withResult(Some(GameResult.Draw(DrawReason.InsufficientMaterial)))
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 ruleSet.isCheck(currentContext) then notifyObservers(CheckDetectedEvent(currentContext))
@@ -1,7 +1,7 @@
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.DrawReason import de.nowchess.api.game.{DrawReason, GameResult}
import de.nowchess.chess.observer.* import de.nowchess.chess.observer.*
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers import org.scalatest.matchers.should.Matchers
@@ -23,6 +23,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
engine.processUserInput("d8h4") engine.processUserInput("d8h4")
observer.hasEvent[CheckmateEvent] shouldBe true observer.hasEvent[CheckmateEvent] shouldBe true
engine.context.result shouldBe Some(GameResult.Win(Color.Black))
test("checkmate with white winner"): test("checkmate with white winner"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
@@ -42,6 +43,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val evt = observer.getEvent[CheckmateEvent] val evt = observer.getEvent[CheckmateEvent]
evt.isDefined shouldBe true evt.isDefined shouldBe true
evt.get.winner shouldBe Color.White evt.get.winner shouldBe Color.White
engine.context.result shouldBe Some(GameResult.Win(Color.White))
// ── Stalemate ─────────────────────────────────────────────────── // ── Stalemate ───────────────────────────────────────────────────
@@ -78,6 +80,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val evt = observer.getEvent[DrawEvent] val evt = observer.getEvent[DrawEvent]
evt.isDefined shouldBe true evt.isDefined shouldBe true
evt.get.reason shouldBe DrawReason.Stalemate evt.get.reason shouldBe DrawReason.Stalemate
engine.context.result shouldBe Some(GameResult.Draw(DrawReason.Stalemate))
test("stalemate board is not reset after draw"): test("stalemate board is not reset after draw"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
@@ -189,6 +192,7 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val evt = observer.getEvent[DrawEvent] val evt = observer.getEvent[DrawEvent]
evt.isDefined shouldBe true evt.isDefined shouldBe true
evt.get.reason shouldBe DrawReason.FiftyMoveRule evt.get.reason shouldBe DrawReason.FiftyMoveRule
engine.context.result shouldBe Some(GameResult.Draw(DrawReason.FiftyMoveRule))
test("draw cannot be claimed when not available"): test("draw cannot be claimed when not available"):
val engine = EngineTestHelpers.makeEngine() val engine = EngineTestHelpers.makeEngine()
@@ -216,3 +220,4 @@ class GameEngineOutcomesTest extends AnyFunSuite with Matchers:
val evt = observer.getEvent[DrawEvent] val evt = observer.getEvent[DrawEvent]
evt.isDefined shouldBe true evt.isDefined shouldBe true
evt.get.reason shouldBe DrawReason.InsufficientMaterial evt.get.reason shouldBe DrawReason.InsufficientMaterial
engine.context.result shouldBe Some(GameResult.Draw(DrawReason.InsufficientMaterial))