Files
NowChessSystems/docs/superpowers/specs/2026-03-30-50-move-rule-design.md
T
2026-03-30 12:32:53 +02:00

3.6 KiB

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)

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

def processMove(board: Board, history: GameHistory, turn: Color,
                halfMoveClock: Int, raw: String): MoveResult

gameLoop signature

def gameLoop(board: Board, history: GameHistory, turn: Color, halfMoveClock: Int): Unit
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