fix: correct 50-move rule threshold to 100 half-moves (FIDE-compliant)

The halfMoveClock counts plies (half-moves). The FIDE 50-move rule requires
50 moves by each side = 100 plies, not 50. Changed both the processMove
and gameLoop checks from >= 50 to >= 100, and updated all tests accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LQ63
2026-03-30 12:58:34 +02:00
parent 0eaeb06e2b
commit c20b71e302
2 changed files with 12 additions and 12 deletions
@@ -36,7 +36,7 @@ object GameController:
case "quit" | "q" => case "quit" | "q" =>
MoveResult.Quit MoveResult.Quit
case "draw" => case "draw" =>
if halfMoveClock >= 50 then MoveResult.DrawClaimed if halfMoveClock >= 100 then MoveResult.DrawClaimed
else MoveResult.InvalidFormat("draw") else MoveResult.InvalidFormat("draw")
case trimmed => case trimmed =>
Parser.parseMove(trimmed) match Parser.parseMove(trimmed) match
@@ -80,7 +80,7 @@ object GameController:
println() println()
print(Renderer.render(board)) print(Renderer.render(board))
val input = val input =
if halfMoveClock >= 50 then if halfMoveClock >= 100 then
println(s"[50-move rule] ${turn.label} may claim a draw, or continue playing.") println(s"[50-move rule] ${turn.label} may claim a draw, or continue playing.")
println(" 1. Claim draw") println(" 1. Claim draw")
println(" 2. Continue") println(" 2. Continue")
@@ -404,19 +404,19 @@ class GameControllerTest extends AnyFunSuite with Matchers:
// ──── processMove: 50-move rule draw claim ─────────────────────────────── // ──── processMove: 50-move rule draw claim ───────────────────────────────
test("processMove: 'draw' with halfMoveClock = 50 returns DrawClaimed"): test("processMove: 'draw' with halfMoveClock = 100 returns DrawClaimed"):
val b = Board(Map( val b = Board(Map(
sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.E, Rank.R1) -> Piece.WhiteKing,
sq(File.E, Rank.R8) -> Piece.BlackKing sq(File.E, Rank.R8) -> Piece.BlackKing
)) ))
GameController.processMove(b, GameHistory.empty, Color.White, 50, "draw") shouldBe MoveResult.DrawClaimed GameController.processMove(b, GameHistory.empty, Color.White, 100, "draw") shouldBe MoveResult.DrawClaimed
test("processMove: 'draw' with halfMoveClock = 49 returns InvalidFormat"): test("processMove: 'draw' with halfMoveClock = 99 returns InvalidFormat"):
val b = Board(Map( val b = Board(Map(
sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.E, Rank.R1) -> Piece.WhiteKing,
sq(File.E, Rank.R8) -> Piece.BlackKing sq(File.E, Rank.R8) -> Piece.BlackKing
)) ))
GameController.processMove(b, GameHistory.empty, Color.White, 49, "draw") shouldBe MoveResult.InvalidFormat("draw") GameController.processMove(b, GameHistory.empty, Color.White, 99, "draw") shouldBe MoveResult.InvalidFormat("draw")
// ──── processMove: halfMoveClock update ────────────────────────────────── // ──── processMove: halfMoveClock update ──────────────────────────────────
@@ -470,30 +470,30 @@ class GameControllerTest extends AnyFunSuite with Matchers:
// ──── gameLoop: 50-move rule menu ──────────────────────────────────────── // ──── gameLoop: 50-move rule menu ────────────────────────────────────────
test("gameLoop: shows 50-move rule menu when halfMoveClock >= 50 and draw claimed"): test("gameLoop: shows 50-move rule menu when halfMoveClock >= 100 and draw claimed"):
val b = Board(Map( val b = Board(Map(
sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.E, Rank.R1) -> Piece.WhiteKing,
sq(File.E, Rank.R8) -> Piece.BlackKing sq(File.E, Rank.R8) -> Piece.BlackKing
)) ))
val output = captureOutput: val output = captureOutput:
withInput("1\nquit\n"): withInput("1\nquit\n"):
GameController.gameLoop(b, GameHistory.empty, Color.White, 50) GameController.gameLoop(b, GameHistory.empty, Color.White, 100)
output should include("50-move rule") output should include("50-move rule")
output should include("Draw claimed by 50-move rule.") output should include("Draw claimed by 50-move rule.")
test("gameLoop: shows 50-move rule menu when halfMoveClock >= 50 and player continues"): test("gameLoop: shows 50-move rule menu when halfMoveClock >= 100 and player continues"):
val b = Board(Map( val b = Board(Map(
sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.E, Rank.R1) -> Piece.WhiteKing,
sq(File.E, Rank.R8) -> Piece.BlackKing sq(File.E, Rank.R8) -> Piece.BlackKing
)) ))
val output = captureOutput: val output = captureOutput:
withInput("2\nquit\n"): withInput("2\nquit\n"):
GameController.gameLoop(b, GameHistory.empty, Color.White, 50) GameController.gameLoop(b, GameHistory.empty, Color.White, 100)
output should include("50-move rule") output should include("50-move rule")
output should include("White's turn") output should include("White's turn")
test("gameLoop: no 50-move rule menu when halfMoveClock < 50"): test("gameLoop: no 50-move rule menu when halfMoveClock < 100"):
val output = captureOutput: val output = captureOutput:
withInput("quit\n"): withInput("quit\n"):
GameController.gameLoop(Board.initial, GameHistory.empty, Color.White, 49) GameController.gameLoop(Board.initial, GameHistory.empty, Color.White, 99)
output should not include "50-move rule" output should not include "50-move rule"