refactor: NCS-4 curry GameRules public API and update all call sites
Build & Test (NowChessSystems) TeamCity build failed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LQ63
2026-03-31 18:09:25 +02:00
parent 38936b2e10
commit 538705d438
4 changed files with 17 additions and 33 deletions
@@ -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)
@@ -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
@@ -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 &&
@@ -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,