From 538705d43834651eabbf35ecc17bc1ba57da84f8 Mon Sep 17 00:00:00 2001 From: LQ63 Date: Tue, 31 Mar 2026 18:09:25 +0200 Subject: [PATCH] refactor: NCS-4 curry GameRules public API and update all call sites Co-Authored-By: Claude Sonnet 4.6 --- .../chess/controller/GameController.scala | 2 +- .../de/nowchess/chess/logic/GameRules.scala | 12 +++---- .../nowchess/chess/logic/MoveValidator.scala | 2 +- .../nowchess/chess/logic/GameRulesTest.scala | 34 +++++-------------- 4 files changed, 17 insertions(+), 33 deletions(-) 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 d734200..c556cda 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 @@ -59,7 +59,7 @@ object GameController: (b.removed(capturedSq), board.pieceAt(capturedSq)) else (b, cap) val newHistory = history.addMove(from, to, castleOpt) - GameRules.gameStatus(newBoard, newHistory, turn.opposite) match + GameRules.gameStatus(newBoard, newHistory)(turn.opposite) match case PositionStatus.Normal => MoveResult.Moved(newBoard, newHistory, captured, turn.opposite) case PositionStatus.InCheck => MoveResult.MovedInCheck(newBoard, newHistory, captured, turn.opposite) case PositionStatus.Mated => MoveResult.Checkmate(turn) diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala index ff619c1..24c1504 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala @@ -9,7 +9,7 @@ enum PositionStatus: object GameRules: /** True if `color`'s king is under attack on this board. */ - def isInCheck(board: Board, color: Color): Boolean = + def isInCheck(board: Board)(color: Color): Boolean = board.pieces .collectFirst { case (sq, p) if p.color == color && p.pieceType == PieceType.King => sq } .exists { kingSq => @@ -21,7 +21,7 @@ object GameRules: } /** All (from, to) moves for `color` that do not leave their own king in check. */ - def legalMoves(board: Board, history: GameHistory, color: Color): Set[(Square, Square)] = + def legalMoves(board: Board, history: GameHistory)(color: Color): Set[(Square, Square)] = val targets = MoveValidator.legalTargets(board, history) board.pieces .collect { case (from, piece) if piece.color == color => from } @@ -33,16 +33,16 @@ object GameRules: board.withCastle(color, MoveValidator.castleSide(from, to)) else board.withMove(from, to)._1 - !isInCheck(newBoard, color) + !isInCheck(newBoard)(color) } .map(to => from -> to) } .toSet /** Position status for the side whose turn it is (`color`). */ - def gameStatus(board: Board, history: GameHistory, color: Color): PositionStatus = - val moves = legalMoves(board, history, color) - val inCheck = isInCheck(board, color) + def gameStatus(board: Board, history: GameHistory)(color: Color): PositionStatus = + val moves = legalMoves(board, history)(color) + val inCheck = isInCheck(board)(color) if moves.isEmpty && inCheck then PositionStatus.Mated else if moves.isEmpty then PositionStatus.Drawn else if inCheck then PositionStatus.InCheck diff --git a/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala b/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala index 9c33ae0..c40904c 100644 --- a/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala +++ b/modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala @@ -133,7 +133,7 @@ object MoveValidator: val enemy = color.opposite if !board.pieceAt(kingSq).contains(Piece(color, PieceType.King)) || - GameRules.isInCheck(board, color) then Set.empty + GameRules.isInCheck(board)(color) then Set.empty else val kingsideSq = Option.when( rights.kingSide && diff --git a/modules/core/src/test/scala/de/nowchess/chess/logic/GameRulesTest.scala b/modules/core/src/test/scala/de/nowchess/chess/logic/GameRulesTest.scala index 5f02f19..83a5657 100644 --- a/modules/core/src/test/scala/de/nowchess/chess/logic/GameRulesTest.scala +++ b/modules/core/src/test/scala/de/nowchess/chess/logic/GameRulesTest.scala @@ -13,38 +13,34 @@ class GameRulesTest extends AnyFunSuite with Matchers: /** Wrap a board in a GameContext with no castling rights — for non-castling tests. */ private def testLegalMoves(entries: (Square, Piece)*)(color: Color): Set[(Square, Square)] = - GameRules.legalMoves(Board(entries.toMap), GameHistory.empty, color) + GameRules.legalMoves(Board(entries.toMap), GameHistory.empty)(color) private def testGameStatus(entries: (Square, Piece)*)(color: Color): PositionStatus = - GameRules.gameStatus(Board(entries.toMap), GameHistory.empty, color) + GameRules.gameStatus(Board(entries.toMap), GameHistory.empty)(color) // ──── isInCheck ────────────────────────────────────────────────────── test("isInCheck: king attacked by enemy rook on same rank"): - // White King E1, Black Rook A1 — rook slides along rank 1 to E1 val b = board( sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.A, Rank.R1) -> Piece.BlackRook ) - GameRules.isInCheck(b, Color.White) shouldBe true + GameRules.isInCheck(b)(Color.White) shouldBe true test("isInCheck: king not attacked"): - // Black Rook A3 does not cover E1 val b = board( sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.A, Rank.R3) -> Piece.BlackRook ) - GameRules.isInCheck(b, Color.White) shouldBe false + GameRules.isInCheck(b)(Color.White) shouldBe false test("isInCheck: no king on board returns false"): val b = board(sq(File.A, Rank.R1) -> Piece.BlackRook) - GameRules.isInCheck(b, Color.White) shouldBe false + GameRules.isInCheck(b)(Color.White) shouldBe false // ──── legalMoves ───────────────────────────────────────────────────── test("legalMoves: move that exposes own king to rook is excluded"): - // White King E1, White Rook E4 (pinned on E-file), Black Rook E8 - // Moving the White Rook off the E-file would expose the king val moves = testLegalMoves( sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.E, Rank.R4) -> Piece.WhiteRook, @@ -53,7 +49,6 @@ class GameRulesTest extends AnyFunSuite with Matchers: moves should not contain (sq(File.E, Rank.R4) -> sq(File.D, Rank.R4)) test("legalMoves: move that blocks check is included"): - // White King E1 in check from Black Rook E8; White Rook A5 can interpose on E5 val moves = testLegalMoves( sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.A, Rank.R5) -> Piece.WhiteRook, @@ -64,8 +59,6 @@ class GameRulesTest extends AnyFunSuite with Matchers: // ──── gameStatus ────────────────────────────────────────────────────── test("gameStatus: checkmate returns Mated"): - // White Qh8, Ka6; Black Ka8 - // Qh8 attacks Ka8 along rank 8; all escape squares covered (spec-verified position) testGameStatus( sq(File.H, Rank.R8) -> Piece.WhiteQueen, sq(File.A, Rank.R6) -> Piece.WhiteKing, @@ -73,8 +66,6 @@ class GameRulesTest extends AnyFunSuite with Matchers: )(Color.Black) shouldBe PositionStatus.Mated test("gameStatus: stalemate returns Drawn"): - // White Qb6, Kc6; Black Ka8 - // Black king has no legal moves and is not in check (spec-verified position) testGameStatus( sq(File.B, Rank.R6) -> Piece.WhiteQueen, sq(File.C, Rank.R6) -> Piece.WhiteKing, @@ -82,14 +73,13 @@ class GameRulesTest extends AnyFunSuite with Matchers: )(Color.Black) shouldBe PositionStatus.Drawn test("gameStatus: king in check with legal escape returns InCheck"): - // White Ra8 attacks Black Ke8 along rank 8; king can escape to d7, e7, f7 testGameStatus( sq(File.A, Rank.R8) -> Piece.WhiteRook, sq(File.E, Rank.R8) -> Piece.BlackKing )(Color.Black) shouldBe PositionStatus.InCheck test("gameStatus: normal starting position returns Normal"): - GameRules.gameStatus(Board.initial, GameHistory.empty, Color.White) shouldBe PositionStatus.Normal + GameRules.gameStatus(Board.initial, GameHistory.empty)(Color.White) shouldBe PositionStatus.Normal test("legalMoves: includes castling destination when available"): val b = board( @@ -97,7 +87,7 @@ class GameRulesTest extends AnyFunSuite with Matchers: sq(File.H, Rank.R1) -> Piece.WhiteRook, sq(File.H, Rank.R8) -> Piece.BlackKing ) - GameRules.legalMoves(b, GameHistory.empty, Color.White) should contain(sq(File.E, Rank.R1) -> sq(File.G, Rank.R1)) + GameRules.legalMoves(b, GameHistory.empty)(Color.White) should contain(sq(File.E, Rank.R1) -> sq(File.G, Rank.R1)) test("legalMoves: excludes castling when king is in check"): val b = board( @@ -106,13 +96,9 @@ class GameRulesTest extends AnyFunSuite with Matchers: sq(File.E, Rank.R8) -> Piece.BlackRook, sq(File.A, Rank.R8) -> Piece.BlackKing ) - GameRules.legalMoves(b, GameHistory.empty, Color.White) should not contain (sq(File.E, Rank.R1) -> sq(File.G, Rank.R1)) + GameRules.legalMoves(b, GameHistory.empty)(Color.White) should not contain (sq(File.E, Rank.R1) -> sq(File.G, Rank.R1)) test("gameStatus: returns Normal (not Drawn) when castling is the only legal move"): - // White King e1, Rook h1 (kingside castling available). - // Black Rooks d2 and f2 box the king: d1 attacked by d2, e2 attacked by both, - // f1 attacked by f2. King cannot move to any adjacent square without entering - // an attacked square or an enemy piece. Only legal move: castle to g1. val b = board( sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.H, Rank.R1) -> Piece.WhiteRook, @@ -120,11 +106,9 @@ class GameRulesTest extends AnyFunSuite with Matchers: sq(File.F, Rank.R2) -> Piece.BlackRook, sq(File.A, Rank.R8) -> Piece.BlackKing ) - // No history means castling rights are intact - GameRules.gameStatus(b, GameHistory.empty, Color.White) shouldBe PositionStatus.Normal + GameRules.gameStatus(b, GameHistory.empty)(Color.White) shouldBe PositionStatus.Normal test("CastleSide.withCastle correctly positions pieces for Queenside castling"): - // Directly test the withCastle extension for Queenside (coverage gap on line 10) val b = board( sq(File.E, Rank.R1) -> Piece.WhiteKing, sq(File.A, Rank.R1) -> Piece.WhiteRook,