From b8f5c8eb77d534821f09530ce57929338377ca7b Mon Sep 17 00:00:00 2001 From: LQ63 Date: Tue, 31 Mar 2026 23:18:36 +0200 Subject: [PATCH] feat: NCS-11 propagate half-move clock flags through GameController Co-Authored-By: Claude Sonnet 4.6 --- .../chess/controller/GameController.scala | 7 ++-- .../chess/controller/GameControllerTest.scala | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala b/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala index 0542df6..429f188 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala @@ -63,7 +63,8 @@ object GameController: case PromotionPiece.Bishop => PieceType.Bishop case PromotionPiece.Knight => PieceType.Knight val newBoard = boardAfterMove.updated(to, Piece(turn, promotedPieceType)) - val newHistory = history.addMove(from, to, None, Some(piece)) + // Promotion is always a pawn move → clock resets + val newHistory = history.addMove(from, to, None, Some(piece), wasPawnMove = true) toMoveResult(newBoard, newHistory, captured, turn) // --------------------------------------------------------------------------- @@ -91,7 +92,9 @@ object GameController: val capturedSq = EnPassantCalculator.capturedPawnSquare(to, turn) (b.removed(capturedSq), board.pieceAt(capturedSq)) else (b, cap) - val newHistory = history.addMove(from, to, castleOpt) + val wasPawnMove = board.pieceAt(from).exists(_.pieceType == PieceType.Pawn) + val wasCapture = captured.isDefined + val newHistory = history.addMove(from, to, castleOpt, wasPawnMove = wasPawnMove, wasCapture = wasCapture) toMoveResult(newBoard, newHistory, captured, turn) private def toMoveResult(newBoard: Board, newHistory: GameHistory, captured: Option[Piece], turn: Color): MoveResult = diff --git a/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala b/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala index 8ff35cf..bb48a75 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala @@ -464,3 +464,39 @@ class GameControllerTest extends AnyFunSuite with Matchers: PromotionPiece.Knight, Color.White ) result should be (MoveResult.Stalemate) + + // ──── half-move clock propagation ──────────────────────────────────── + + test("processMove: non-pawn non-capture increments halfMoveClock"): + // g1f3 is a knight move — not a pawn, not a capture + processMove(Board.initial, GameHistory.empty, Color.White, "g1f3") match + case MoveResult.Moved(_, newHistory, _, _) => + newHistory.halfMoveClock shouldBe 1 + case other => fail(s"Expected Moved, got $other") + + test("processMove: pawn move resets halfMoveClock to 0"): + processMove(Board.initial, GameHistory.empty, Color.White, "e2e4") match + case MoveResult.Moved(_, newHistory, _, _) => + newHistory.halfMoveClock shouldBe 0 + case other => fail(s"Expected Moved, got $other") + + test("processMove: capture resets halfMoveClock to 0"): + // White pawn on e5, Black pawn on d6 — exd6 is a capture + val board = Board(Map( + sq(File.E, Rank.R5) -> Piece.WhitePawn, + sq(File.D, Rank.R6) -> Piece.BlackPawn, + sq(File.E, Rank.R1) -> Piece.WhiteKing, + sq(File.E, Rank.R8) -> Piece.BlackKing + )) + val history = GameHistory(halfMoveClock = 10) + processMove(board, history, Color.White, "e5d6") match + case MoveResult.Moved(_, newHistory, _, _) => + newHistory.halfMoveClock shouldBe 0 + case other => fail(s"Expected Moved, got $other") + + test("processMove: clock carries from previous history on non-pawn non-capture"): + val history = GameHistory(halfMoveClock = 5) + processMove(Board.initial, history, Color.White, "g1f3") match + case MoveResult.Moved(_, newHistory, _, _) => + newHistory.halfMoveClock shouldBe 6 + case other => fail(s"Expected Moved, got $other")