docs: add 50-move rule design spec for NCS-11
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
# 50-Move Rule — Design Spec
|
||||
**Branch:** feat/NCS-11
|
||||
**Date:** 2026-03-30
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Implement the 50-move rule: a player may claim a draw if no pawn move or capture has occurred in the last 50 half-moves (plies). The rule is **not** enforced automatically — the eligible player must actively claim it via a TUI menu option.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
### Approach
|
||||
Thread `halfMoveClock: Int` explicitly through `processMove` and `gameLoop` (Approach A). This mirrors the existing `halfMoveClock` field in `GameState` (already parsed from FEN) and requires no changes to `GameHistory` or `HistoryMove`.
|
||||
|
||||
---
|
||||
|
||||
## Section 1: Data & Signatures
|
||||
|
||||
### `MoveResult` changes (`GameController.scala`)
|
||||
|
||||
```scala
|
||||
case class Moved(newBoard: Board, newHistory: GameHistory, captured: Option[Piece],
|
||||
newHalfMoveClock: Int, newTurn: Color) extends MoveResult
|
||||
case class MovedInCheck(newBoard: Board, newHistory: GameHistory, captured: Option[Piece],
|
||||
newHalfMoveClock: Int, newTurn: Color) extends MoveResult
|
||||
case object DrawClaimed extends MoveResult
|
||||
```
|
||||
|
||||
`Stalemate` remains unchanged (automatic draw, no clock involved).
|
||||
|
||||
### `processMove` signature
|
||||
|
||||
```scala
|
||||
def processMove(board: Board, history: GameHistory, turn: Color,
|
||||
halfMoveClock: Int, raw: String): MoveResult
|
||||
```
|
||||
|
||||
### `gameLoop` signature
|
||||
|
||||
```scala
|
||||
def gameLoop(board: Board, history: GameHistory, turn: Color, halfMoveClock: Int): Unit
|
||||
```
|
||||
|
||||
### Clock update logic (inside `processMove`, after a legal move)
|
||||
|
||||
```scala
|
||||
val movedPiece = board.pieceAt(from).get
|
||||
val isReset = movedPiece.pieceType == PieceType.Pawn || captured.isDefined || isEP
|
||||
val newClock = if isReset then 0 else halfMoveClock + 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Section 2: TUI Menu / `gameLoop` Behaviour
|
||||
|
||||
When `halfMoveClock >= 50` at the start of a player's turn, `gameLoop` shows a special prompt **before** asking for a move:
|
||||
|
||||
```
|
||||
[50-move rule] You may claim a draw, or continue playing.
|
||||
1. Claim draw
|
||||
2. Continue
|
||||
```
|
||||
|
||||
- **Option 1:** `processMove` is called with `raw = "draw"` → returns `DrawClaimed` → prints `"Draw claimed by 50-move rule."` then restarts with the initial board.
|
||||
- **Option 2:** Falls through to the normal move prompt for that same turn. The clock is **not** reset by choosing to continue — it only resets on a pawn move or capture.
|
||||
- If `halfMoveClock < 50`: no menu shown, normal move prompt only.
|
||||
- If `raw = "draw"` arrives in `processMove` with `halfMoveClock < 50`: treated as `InvalidFormat("draw")`.
|
||||
|
||||
---
|
||||
|
||||
## Section 3: Testing
|
||||
|
||||
All tests in `GameControllerTest` (`AnyFunSuite with Matchers`, pure unit tests on `processMove`):
|
||||
|
||||
| Test | Expected result |
|
||||
|------|----------------|
|
||||
| `raw = "draw"`, `halfMoveClock = 50` | `DrawClaimed` |
|
||||
| `raw = "draw"`, `halfMoveClock = 49` | `InvalidFormat("draw")` |
|
||||
| After a pawn move | `newHalfMoveClock == 0` |
|
||||
| After a capture | `newHalfMoveClock == 0` |
|
||||
| After an en-passant capture | `newHalfMoveClock == 0` |
|
||||
| After a quiet piece move, clock = 10 | `newHalfMoveClock == 11` |
|
||||
| `Moved` result carries updated clock | clock present in result |
|
||||
| `MovedInCheck` result carries updated clock | clock present in result |
|
||||
|
||||
No changes needed to `GameRulesTest`, `FenParserTest`, or `FenExporterTest`.
|
||||
|
||||
---
|
||||
|
||||
## Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala` | Add `halfMoveClock` param to `processMove` and `gameLoop`; add clock update logic; add draw claim handling; update `MoveResult` cases |
|
||||
| `modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala` | Add tests for all clock and draw claim scenarios |
|
||||
Reference in New Issue
Block a user