diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/GameHistory.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/GameHistory.scala index 80011fe..fe52d55 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/GameHistory.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/logic/GameHistory.scala @@ -11,21 +11,36 @@ case class HistoryMove( promotionPiece: Option[PromotionPiece] = None ) -/** Complete game history: ordered list of moves. */ -case class GameHistory(moves: List[HistoryMove] = List.empty): +/** Complete game history: ordered list of moves plus the half-move clock for the 50-move rule. + * + * @param moves moves played so far, oldest first + * @param halfMoveClock plies since the last pawn move or capture (FIDE 50-move rule counter) + */ +case class GameHistory(moves: List[HistoryMove] = List.empty, halfMoveClock: Int = 0): + + /** Add a raw HistoryMove record. Clock increments by 1. + * Use the coordinate overload when you know whether the move is a pawn move or capture. + */ def addMove(move: HistoryMove): GameHistory = - GameHistory(moves :+ move) - - def addMove(from: Square, to: Square): GameHistory = - addMove(HistoryMove(from, to, None)) + GameHistory(moves :+ move, halfMoveClock + 1) + /** Add a move by coordinates. + * + * @param wasPawnMove true when the moving piece is a pawn — resets the clock to 0 + * @param wasCapture true when a piece was captured (including en passant) — resets the clock to 0 + * + * If neither flag is set the clock increments by 1. + */ def addMove( from: Square, to: Square, castleSide: Option[CastleSide] = None, - promotionPiece: Option[PromotionPiece] = None + promotionPiece: Option[PromotionPiece] = None, + wasPawnMove: Boolean = false, + wasCapture: Boolean = false ): GameHistory = - addMove(HistoryMove(from, to, castleSide, promotionPiece)) + val newClock = if wasPawnMove || wasCapture then 0 else halfMoveClock + 1 + GameHistory(moves :+ HistoryMove(from, to, castleSide, promotionPiece), newClock) object GameHistory: val empty: GameHistory = GameHistory() diff --git a/modules/core/src/test/scala/de/nowchess/chess/logic/GameHistoryTest.scala b/modules/core/src/test/scala/de/nowchess/chess/logic/GameHistoryTest.scala index 96e9af4..8a6069f 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/logic/GameHistoryTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/logic/GameHistoryTest.scala @@ -69,3 +69,36 @@ class GameHistoryTest extends AnyFunSuite with Matchers: newHistory.moves should have length 1 newHistory.moves.head.castleSide should be (None) newHistory.moves.head.promotionPiece should be (Some(PromotionPiece.Queen)) + + // ──── half-move clock ──────────────────────────────────────────────── + + test("halfMoveClock starts at 0"): + GameHistory.empty.halfMoveClock shouldBe 0 + + test("halfMoveClock increments on a non-pawn non-capture move"): + val h = GameHistory.empty.addMove(sq(File.G, Rank.R1), sq(File.F, Rank.R3)) + h.halfMoveClock shouldBe 1 + + test("halfMoveClock resets to 0 on a pawn move"): + val h = GameHistory.empty.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4), wasPawnMove = true) + h.halfMoveClock shouldBe 0 + + test("halfMoveClock resets to 0 on a capture"): + val h = GameHistory.empty.addMove(sq(File.E, Rank.R5), sq(File.D, Rank.R6), wasCapture = true) + h.halfMoveClock shouldBe 0 + + test("halfMoveClock resets to 0 when both wasPawnMove and wasCapture are true"): + val h = GameHistory.empty.addMove(sq(File.E, Rank.R5), sq(File.D, Rank.R6), wasPawnMove = true, wasCapture = true) + h.halfMoveClock shouldBe 0 + + test("halfMoveClock carries across multiple moves"): + val h = GameHistory.empty + .addMove(sq(File.G, Rank.R1), sq(File.F, Rank.R3)) // +1 → 1 + .addMove(sq(File.G, Rank.R8), sq(File.F, Rank.R6)) // +1 → 2 + .addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4), wasPawnMove = true) // reset → 0 + .addMove(sq(File.B, Rank.R1), sq(File.C, Rank.R3)) // +1 → 1 + h.halfMoveClock shouldBe 1 + + test("GameHistory can be initialised with a non-zero halfMoveClock"): + val h = GameHistory(halfMoveClock = 42) + h.halfMoveClock shouldBe 42