From f61ffce22ae656c343b12e74cb090aea6ededfba Mon Sep 17 00:00:00 2001 From: LQ63 Date: Tue, 31 Mar 2026 22:46:29 +0200 Subject: [PATCH] docs: extend 50-move rule spec with FEN and PGN integration --- .../specs/2026-03-31-50-move-rule-design.md | 74 ++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/docs/superpowers/specs/2026-03-31-50-move-rule-design.md b/docs/superpowers/specs/2026-03-31-50-move-rule-design.md index 5d972a2..86789fd 100644 --- a/docs/superpowers/specs/2026-03-31-50-move-rule-design.md +++ b/docs/superpowers/specs/2026-03-31-50-move-rule-design.md @@ -130,7 +130,67 @@ This fires immediately after the `MoveExecutedEvent` (or `CheckDetectedEvent`) f --- -## Section 4: Files Changed +## Section 4: FEN Integration + +`FenExporter.gameStateToFen` and `FenParser.parseFen` already handle `halfMoveClock` at the `GameState` level — no changes to those files are needed. + +The bridge between `GameHistory.halfMoveClock` and `GameState.halfMoveClock` is a caller responsibility: + +**FEN export (writing):** When constructing a `GameState` for FEN export, pass `halfMoveClock = history.halfMoveClock`. Since `GameEngine` already exposes `def history: GameHistory`, this works automatically once the field is populated: + +```scala +GameState( + piecePlacement = FenExporter.boardToFen(engine.board), + activeColor = engine.turn, + ..., + halfMoveClock = engine.history.halfMoveClock, + ... +) +``` + +**FEN import (reading):** When loading from a parsed `GameState`, initialise the engine with a `GameHistory` carrying the parsed clock: + +```scala +val gs = FenParser.parseFen(fenString).get +new GameEngine( + initialBoard = FenParser.parseBoard(gs.piecePlacement).get, + initialHistory = GameHistory(halfMoveClock = gs.halfMoveClock), + initialTurn = gs.activeColor +) +``` + +A round-trip test is added to `FenExporterTest` / `FenParserTest` verifying that a non-zero clock survives export → import. + +--- + +## Section 5: PGN Integration + +`PgnExporter.exportGame` currently hardcodes `" *"` as the game termination marker. PGN standard requires the marker to match the `Result` header (`1-0`, `0-1`, `1/2-1/2`, or `*`). + +### Change to `PgnExporter` + +Replace the hardcoded `" *"` with the value from the `Result` header: + +```scala +val termination = headers.getOrElse("Result", "*") +moveLines.mkString(" ") + s" $termination" +``` + +### Draw claim result + +When `DrawClaimedEvent` is handled by a caller that exports PGN, it should pass: + +```scala +Map("Result" -> "1/2-1/2", ...) +``` + +The move text will then end with `1/2-1/2`, which is correct per PGN standard for a drawn game. + +A test is added to `PgnExporterTest` verifying that `exportGame` with `"Result" -> "1/2-1/2"` produces a move text ending in `1/2-1/2`. + +--- + +## Section 6: Files Changed | File | Change | |------|--------| @@ -138,15 +198,18 @@ This fires immediately after the `MoveExecutedEvent` (or `CheckDetectedEvent`) f | `modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala` | Compute and pass flags in `applyNormalMove` and `completePromotion` | | `modules/core/src/main/scala/de/nowchess/chess/observer/Observer.scala` | Add `FiftyMoveRuleAvailableEvent` and `DrawClaimedEvent` | | `modules/core/src/main/scala/de/nowchess/chess/engine/GameEngine.scala` | Handle `"draw"` input; fire `FiftyMoveRuleAvailableEvent` after eligible moves | +| `modules/core/src/main/scala/de/nowchess/chess/notation/PgnExporter.scala` | Derive termination marker from `Result` header instead of hardcoding `*` | | `modules/core/src/test/scala/de/nowchess/chess/logic/GameHistoryTest.scala` | New test suite for clock update rules | | `modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala` | Tests for clock values in `applyNormalMove` and `completePromotion` | | `modules/core/src/test/scala/de/nowchess/chess/engine/GameEngineTest.scala` | Tests for `"draw"` command and `FiftyMoveRuleAvailableEvent` | +| `modules/core/src/test/scala/de/nowchess/chess/notation/PgnExporterTest.scala` | Test for `1/2-1/2` termination marker | +| `modules/core/src/test/scala/de/nowchess/chess/notation/FenExporterTest.scala` | Round-trip test: non-zero `halfMoveClock` survives FEN export → import | `EnPassantCalculator`, `CastlingRightsCalculator`, `MoveValidator`, `GameRules`, and their tests are **not** touched. --- -## Section 5: Testing +## Section 7: Testing ### `GameHistoryTest` - Clock starts at 0 @@ -167,3 +230,10 @@ This fires immediately after the `MoveExecutedEvent` (or `CheckDetectedEvent`) f - `processUserInput("draw")` fires `InvalidMoveEvent` when `halfMoveClock < 100` - A successful non-pawn, non-capture move that brings the clock to exactly 100 fires `FiftyMoveRuleAvailableEvent` - A successful move that does not reach 100 does not fire `FiftyMoveRuleAvailableEvent` + +### `PgnExporterTest` +- `exportGame` with `"Result" -> "1/2-1/2"` produces move text ending in `1/2-1/2` +- `exportGame` with no `Result` header still produces `*` as before (backward-compatible) + +### `FenExporterTest` +- Round-trip: a `GameHistory` with `halfMoveClock = 42` exported to FEN and re-parsed yields `halfMoveClock = 42`