From e5e20c566e368b12ca1dc59680c34e9112bf6762 Mon Sep 17 00:00:00 2001 From: Janis Date: Wed, 1 Apr 2026 09:07:06 +0200 Subject: [PATCH] fix: update move validation to check for king safety (#13) Reviewed-on: https://git.janis-eccarius.de/NowChess/NowChessSystems/pulls/13 --- .claude/agents/test-writer.md | 1 - .../chess/controller/GameController.scala | 2 +- .../chess/controller/GameControllerTest.scala | 24 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/.claude/agents/test-writer.md b/.claude/agents/test-writer.md index 93668f2..d31ee21 100644 --- a/.claude/agents/test-writer.md +++ b/.claude/agents/test-writer.md @@ -20,5 +20,4 @@ When invoked BEFORE scala-implementer (no implementation exists yet): When invoked AFTER scala-implementer (implementation exists): Run python3 jacoco-reporter/jacoco_coverage_gaps.py modules/{service-name}/build/reports/jacoco/test/jacocoTestReport.xml --output agent - Use the jacoco-coverage-gaps skill — close coverage gaps revealed by the report. To regenerate the report run the tests first. diff --git a/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala b/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala index 0542df6..64e8d3a 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala @@ -75,7 +75,7 @@ object GameController: case None => MoveResult.NoPiece case Some(piece) if piece.color != turn => MoveResult.WrongColor case Some(_) => - if !MoveValidator.isLegal(board, history, from, to) then MoveResult.IllegalMove + if !GameRules.legalMoves(board, history, turn).contains(from -> to) then MoveResult.IllegalMove else if MoveValidator.isPromotionMove(board, from, to) then MoveResult.PromotionRequired(from, to, board, history, board.pieceAt(to), turn) else applyNormalMove(board, history, turn, from, to) diff --git a/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala b/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala index 8ff35cf..c379d4a 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/controller/GameControllerTest.scala @@ -43,6 +43,30 @@ class GameControllerTest extends AnyFunSuite with Matchers: // White pawn at E2 cannot jump three squares to E5 processMove(Board.initial, GameHistory.empty, Color.White, "e2e5") shouldBe MoveResult.IllegalMove + test("processMove: move that leaves own king in check returns IllegalMove"): + // White King E1 is in check from Black Rook E8. Moving the D2 pawn is + // geometrically legal but does not resolve the check — must be rejected. + val b = Board(Map( + sq(File.E, Rank.R1) -> Piece.WhiteKing, + sq(File.D, Rank.R2) -> Piece.WhitePawn, + sq(File.E, Rank.R8) -> Piece.BlackRook, + sq(File.A, Rank.R8) -> Piece.BlackKing + )) + processMove(b, GameHistory.empty, Color.White, "d2d4") shouldBe MoveResult.IllegalMove + + test("processMove: move that resolves check is allowed"): + // White King E1 is in check from Black Rook E8 along the E-file. + // White Rook A5 interposes at E5 — resolves the check, no new check on Black King A8. + val b = Board(Map( + sq(File.E, Rank.R1) -> Piece.WhiteKing, + sq(File.A, Rank.R5) -> Piece.WhiteRook, + sq(File.E, Rank.R8) -> Piece.BlackRook, + sq(File.A, Rank.R8) -> Piece.BlackKing + )) + processMove(b, GameHistory.empty, Color.White, "a5e5") match + case _: MoveResult.Moved => succeed + case other => fail(s"Expected Moved, got $other") + test("processMove: legal pawn move returns Moved with updated board and flipped turn"): processMove(Board.initial, GameHistory.empty, Color.White, "e2e4") match case MoveResult.Moved(newBoard, newHistory, captured, newTurn) =>