refactor: NCS-4 curry GameRules public API and update all call sites
Build & Test (NowChessSystems) TeamCity build failed
Build & Test (NowChessSystems) TeamCity build failed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user