WIP: feat: NCS-4 Currying #11
@@ -0,0 +1,82 @@
|
||||
# Currying Public API — Design Spec
|
||||
**Branch:** feat/NCS-11
|
||||
**Date:** 2026-03-31
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Refactor the public methods of `MoveValidator` and `GameRules` to use multiple parameter groups (currying), separating `(board[, history])` as the context group from the computation parameters. This is a pure style refactoring — no behaviour changes, no new tests required.
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
Currying clarifies intent: `(board, history)` is "the world being operated on"; the remaining parameters are "what varies." It also enables partial application at call sites where board/history are fixed across a loop (e.g. `isInCheck`, `legalMoves`).
|
||||
|
||||
---
|
||||
|
||||
## Section 1: Methods Being Curried
|
||||
|
||||
### `MoveValidator` (public API only)
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `legalTargets(board, from)` | `legalTargets(board)(from)` |
|
||||
| `legalTargets(board, history, from)` | `legalTargets(board, history)(from)` |
|
||||
| `isLegal(board, from, to)` | `isLegal(board)(from, to)` |
|
||||
| `isLegal(board, history, from, to)` | `isLegal(board, history)(from, to)` |
|
||||
| `isCastle(board, from, to)` | `isCastle(board)(from, to)` |
|
||||
| `castlingTargets(board, history, color)` | `castlingTargets(board, history)(color)` |
|
||||
| `castleSide(from, to)` | unchanged — no board parameter |
|
||||
|
||||
Private helpers (`isOwnPiece`, `isEnemyPiece`, `slide`, `pawnTargets`, `knightTargets`, `kingTargets`) are **not** changed.
|
||||
|
||||
### `GameRules` (all public methods)
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| `isInCheck(board, color)` | `isInCheck(board)(color)` |
|
||||
| `legalMoves(board, history, color)` | `legalMoves(board, history)(color)` |
|
||||
| `gameStatus(board, history, color)` | `gameStatus(board, history)(color)` |
|
||||
|
||||
---
|
||||
|
||||
## Section 2: Call Sites Updated
|
||||
|
||||
| File | Methods affected |
|
||||
|------|-----------------|
|
||||
| `modules/core/src/main/scala/de/nowchess/chess/logic/MoveValidator.scala` | Internal calls to `legalTargets`, `castlingTargets`, `isCastle` |
|
||||
| `modules/core/src/main/scala/de/nowchess/chess/logic/GameRules.scala` | `MoveValidator.legalTargets`, `MoveValidator.isCastle`, `isInCheck` |
|
||||
| `modules/core/src/main/scala/de/nowchess/chess/controller/GameController.scala` | `MoveValidator.isLegal`, `MoveValidator.isCastle`, `GameRules.gameStatus` |
|
||||
| `modules/core/src/test/scala/de/nowchess/chess/logic/MoveValidatorTest.scala` | `legalTargets`, `isLegal`, `castlingTargets` |
|
||||
| `modules/core/src/test/scala/de/nowchess/chess/logic/GameRulesTest.scala` | `isInCheck`, `legalMoves`, `gameStatus` |
|
||||
|
||||
`EnPassantCalculator`, `CastlingRightsCalculator`, and their tests are **not** touched.
|
||||
|
||||
---
|
||||
|
||||
## Section 3: Concrete Style Gain
|
||||
|
||||
In `GameRules.isInCheck` and `legalMoves`, `board` is passed to `legalTargets` on every loop iteration today. After currying, it is factored out as a single partial application:
|
||||
|
||||
```scala
|
||||
// Before
|
||||
board.pieces.exists { case (sq, piece) =>
|
||||
piece.color != color &&
|
||||
MoveValidator.legalTargets(board, sq).contains(kingSq)
|
||||
}
|
||||
|
||||
// After
|
||||
val targets = MoveValidator.legalTargets(board)
|
||||
board.pieces.exists { case (sq, piece) =>
|
||||
piece.color != color &&
|
||||
targets(sq).contains(kingSq)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Section 4: Testing
|
||||
|
||||
Pure refactoring — no new tests. All existing tests must pass after call sites are updated. The test suite is the regression guard.
|
||||
@@ -43,10 +43,10 @@ object GameController:
|
||||
case Some(piece) if piece.color != turn =>
|
||||
MoveResult.WrongColor
|
||||
case Some(_) =>
|
||||
if !MoveValidator.isLegal(board, history, from, to) then
|
||||
if !MoveValidator.isLegal(board, history)(from, to) then
|
||||
MoveResult.IllegalMove
|
||||
else
|
||||
val castleOpt = if MoveValidator.isCastle(board, from, to)
|
||||
val castleOpt = if MoveValidator.isCastle(board)(from, to)
|
||||
then Some(MoveValidator.castleSide(from, to))
|
||||
else None
|
||||
val isEP = EnPassantCalculator.isEnPassant(board, history, from, to)
|
||||
@@ -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,38 +9,40 @@ 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 =>
|
||||
val targets = MoveValidator.legalTargets(board)
|
||||
board.pieces.exists { case (sq, piece) =>
|
||||
piece.color != color &&
|
||||
MoveValidator.legalTargets(board, sq).contains(kingSq)
|
||||
targets(sq).contains(kingSq)
|
||||
}
|
||||
}
|
||||
|
||||
/** 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 }
|
||||
.flatMap { from =>
|
||||
MoveValidator.legalTargets(board, history, from) // context-aware: includes castling
|
||||
targets(from)
|
||||
.filter { to =>
|
||||
val newBoard =
|
||||
if MoveValidator.isCastle(board, from, to) then
|
||||
if MoveValidator.isCastle(board)(from, to) then
|
||||
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
|
||||
|
||||
@@ -11,11 +11,11 @@ object MoveValidator:
|
||||
* - cannot capture own pieces
|
||||
* - sliding pieces (bishop, rook, queen) are blocked by intervening pieces
|
||||
*/
|
||||
def isLegal(board: Board, from: Square, to: Square): Boolean =
|
||||
legalTargets(board, from).contains(to)
|
||||
def isLegal(board: Board)(from: Square, to: Square): Boolean =
|
||||
legalTargets(board)(from).contains(to)
|
||||
|
||||
/** All squares a piece on `from` can legally move to (same rules as isLegal). */
|
||||
def legalTargets(board: Board, from: Square): Set[Square] =
|
||||
def legalTargets(board: Board)(from: Square): Set[Square] =
|
||||
board.pieceAt(from) match
|
||||
case None => Set.empty
|
||||
case Some(piece) =>
|
||||
@@ -70,9 +70,9 @@ object MoveValidator:
|
||||
.toSet
|
||||
|
||||
private def pawnTargets(board: Board, from: Square, color: Color): Set[Square] =
|
||||
val fi = from.file.ordinal
|
||||
val ri = from.rank.ordinal
|
||||
val dir = if color == Color.White then 1 else -1
|
||||
val fi = from.file.ordinal
|
||||
val ri = from.rank.ordinal
|
||||
val dir = if color == Color.White then 1 else -1
|
||||
val startRank = if color == Color.White then 1 else 6 // R2 = ordinal 1, R7 = ordinal 6
|
||||
|
||||
val oneStep = squareAt(fi, ri + dir)
|
||||
@@ -116,24 +116,24 @@ object MoveValidator:
|
||||
|
||||
private def isAttackedBy(board: Board, sq: Square, attackerColor: Color): Boolean =
|
||||
board.pieces.exists { case (from, piece) =>
|
||||
piece.color == attackerColor && legalTargets(board, from).contains(sq)
|
||||
piece.color == attackerColor && legalTargets(board)(from).contains(sq)
|
||||
}
|
||||
|
||||
def isCastle(board: Board, from: Square, to: Square): Boolean =
|
||||
def isCastle(board: Board)(from: Square, to: Square): Boolean =
|
||||
board.pieceAt(from).exists(_.pieceType == PieceType.King) &&
|
||||
math.abs(to.file.ordinal - from.file.ordinal) == 2
|
||||
|
||||
def castleSide(from: Square, to: Square): CastleSide =
|
||||
if to.file.ordinal > from.file.ordinal then CastleSide.Kingside else CastleSide.Queenside
|
||||
|
||||
def castlingTargets(board: Board, history: GameHistory, color: Color): Set[Square] =
|
||||
def castlingTargets(board: Board, history: GameHistory)(color: Color): Set[Square] =
|
||||
val rights = CastlingRightsCalculator.deriveCastlingRights(history, color)
|
||||
val rank = if color == Color.White then Rank.R1 else Rank.R8
|
||||
val kingSq = Square(File.E, rank)
|
||||
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 &&
|
||||
@@ -151,14 +151,14 @@ object MoveValidator:
|
||||
|
||||
kingsideSq.toSet ++ queensideSq.toSet
|
||||
|
||||
def legalTargets(board: Board, history: GameHistory, from: Square): Set[Square] =
|
||||
def legalTargets(board: Board, history: GameHistory)(from: Square): Set[Square] =
|
||||
board.pieceAt(from) match
|
||||
case Some(piece) if piece.pieceType == PieceType.King =>
|
||||
legalTargets(board, from) ++ castlingTargets(board, history, piece.color)
|
||||
legalTargets(board)(from) ++ castlingTargets(board, history)(piece.color)
|
||||
case Some(piece) if piece.pieceType == PieceType.Pawn =>
|
||||
pawnTargets(board, history, from, piece.color)
|
||||
case _ =>
|
||||
legalTargets(board, from)
|
||||
legalTargets(board)(from)
|
||||
|
||||
private def pawnTargets(board: Board, history: GameHistory, from: Square, color: Color): Set[Square] =
|
||||
val existing = pawnTargets(board, from, color)
|
||||
@@ -171,5 +171,5 @@ object MoveValidator:
|
||||
.toSet
|
||||
existing ++ epCapture
|
||||
|
||||
def isLegal(board: Board, history: GameHistory, from: Square, to: Square): Boolean =
|
||||
legalTargets(board, history, from).contains(to)
|
||||
def isLegal(board: Board, history: GameHistory)(from: Square, to: Square): Boolean =
|
||||
legalTargets(board, history)(from).contains(to)
|
||||
|
||||
@@ -106,7 +106,7 @@ object PgnParser:
|
||||
val reachable: Set[Square] =
|
||||
board.pieces.collect {
|
||||
case (from, piece) if piece.color == color &&
|
||||
MoveValidator.legalTargets(board, from).contains(toSquare) => from
|
||||
MoveValidator.legalTargets(board)(from).contains(toSquare) => from
|
||||
}.toSet
|
||||
|
||||
val candidates: Set[Square] =
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -14,36 +14,36 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
// ──── Empty square ───────────────────────────────────────────────────
|
||||
|
||||
test("legalTargets returns empty set when no piece at from square"):
|
||||
MoveValidator.legalTargets(Board.initial, sq(File.E, Rank.R4)) shouldBe empty
|
||||
MoveValidator.legalTargets(Board.initial)(sq(File.E, Rank.R4)) shouldBe empty
|
||||
|
||||
// ──── isLegal delegates to legalTargets ──────────────────────────────
|
||||
|
||||
test("isLegal returns true for a valid pawn move"):
|
||||
MoveValidator.isLegal(Board.initial, sq(File.E, Rank.R2), sq(File.E, Rank.R4)) shouldBe true
|
||||
MoveValidator.isLegal(Board.initial)(sq(File.E, Rank.R2), sq(File.E, Rank.R4)) shouldBe true
|
||||
|
||||
test("isLegal returns false for an invalid move"):
|
||||
MoveValidator.isLegal(Board.initial, sq(File.E, Rank.R2), sq(File.E, Rank.R5)) shouldBe false
|
||||
MoveValidator.isLegal(Board.initial)(sq(File.E, Rank.R2), sq(File.E, Rank.R5)) shouldBe false
|
||||
|
||||
// ──── Pawn – White ───────────────────────────────────────────────────
|
||||
|
||||
test("white pawn on starting rank can move forward one square"):
|
||||
val b = board(sq(File.E, Rank.R2) -> Piece.WhitePawn)
|
||||
MoveValidator.legalTargets(b, sq(File.E, Rank.R2)) should contain(sq(File.E, Rank.R3))
|
||||
MoveValidator.legalTargets(b)(sq(File.E, Rank.R2)) should contain(sq(File.E, Rank.R3))
|
||||
|
||||
test("white pawn on starting rank can move forward two squares"):
|
||||
val b = board(sq(File.E, Rank.R2) -> Piece.WhitePawn)
|
||||
MoveValidator.legalTargets(b, sq(File.E, Rank.R2)) should contain(sq(File.E, Rank.R4))
|
||||
MoveValidator.legalTargets(b)(sq(File.E, Rank.R2)) should contain(sq(File.E, Rank.R4))
|
||||
|
||||
test("white pawn not on starting rank cannot move two squares"):
|
||||
val b = board(sq(File.E, Rank.R3) -> Piece.WhitePawn)
|
||||
MoveValidator.legalTargets(b, sq(File.E, Rank.R3)) should not contain sq(File.E, Rank.R5)
|
||||
MoveValidator.legalTargets(b)(sq(File.E, Rank.R3)) should not contain sq(File.E, Rank.R5)
|
||||
|
||||
test("white pawn is blocked by piece directly in front, and cannot jump over it"):
|
||||
val b = board(
|
||||
sq(File.E, Rank.R2) -> Piece.WhitePawn,
|
||||
sq(File.E, Rank.R3) -> Piece.BlackPawn
|
||||
)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.E, Rank.R2))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.E, Rank.R2))
|
||||
targets should not contain sq(File.E, Rank.R3)
|
||||
targets should not contain sq(File.E, Rank.R4)
|
||||
|
||||
@@ -52,7 +52,7 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
sq(File.E, Rank.R2) -> Piece.WhitePawn,
|
||||
sq(File.E, Rank.R4) -> Piece.BlackPawn
|
||||
)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.E, Rank.R2))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.E, Rank.R2))
|
||||
targets should contain(sq(File.E, Rank.R3))
|
||||
targets should not contain sq(File.E, Rank.R4)
|
||||
|
||||
@@ -61,15 +61,15 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
sq(File.E, Rank.R2) -> Piece.WhitePawn,
|
||||
sq(File.D, Rank.R3) -> Piece.BlackPawn
|
||||
)
|
||||
MoveValidator.legalTargets(b, sq(File.E, Rank.R2)) should contain(sq(File.D, Rank.R3))
|
||||
MoveValidator.legalTargets(b)(sq(File.E, Rank.R2)) should contain(sq(File.D, Rank.R3))
|
||||
|
||||
test("white pawn cannot capture diagonally when no enemy piece is present"):
|
||||
val b = board(sq(File.E, Rank.R2) -> Piece.WhitePawn)
|
||||
MoveValidator.legalTargets(b, sq(File.E, Rank.R2)) should not contain sq(File.D, Rank.R3)
|
||||
MoveValidator.legalTargets(b)(sq(File.E, Rank.R2)) should not contain sq(File.D, Rank.R3)
|
||||
|
||||
test("white pawn at A-file does not generate diagonal to the left off the board"):
|
||||
val b = board(sq(File.A, Rank.R2) -> Piece.WhitePawn)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.A, Rank.R2))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.A, Rank.R2))
|
||||
targets should contain(sq(File.A, Rank.R3))
|
||||
targets should contain(sq(File.A, Rank.R4))
|
||||
targets.size shouldBe 2
|
||||
@@ -78,50 +78,50 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("black pawn on starting rank can move forward one and two squares"):
|
||||
val b = board(sq(File.E, Rank.R7) -> Piece.BlackPawn)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.E, Rank.R7))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.E, Rank.R7))
|
||||
targets should contain(sq(File.E, Rank.R6))
|
||||
targets should contain(sq(File.E, Rank.R5))
|
||||
|
||||
test("black pawn not on starting rank cannot move two squares"):
|
||||
val b = board(sq(File.E, Rank.R6) -> Piece.BlackPawn)
|
||||
MoveValidator.legalTargets(b, sq(File.E, Rank.R6)) should not contain sq(File.E, Rank.R4)
|
||||
MoveValidator.legalTargets(b)(sq(File.E, Rank.R6)) should not contain sq(File.E, Rank.R4)
|
||||
|
||||
test("black pawn can capture diagonally when enemy piece is present"):
|
||||
val b = board(
|
||||
sq(File.E, Rank.R7) -> Piece.BlackPawn,
|
||||
sq(File.F, Rank.R6) -> Piece.WhitePawn
|
||||
)
|
||||
MoveValidator.legalTargets(b, sq(File.E, Rank.R7)) should contain(sq(File.F, Rank.R6))
|
||||
MoveValidator.legalTargets(b)(sq(File.E, Rank.R7)) should contain(sq(File.F, Rank.R6))
|
||||
|
||||
// ──── Knight ─────────────────────────────────────────────────────────
|
||||
|
||||
test("knight in center has 8 possible moves"):
|
||||
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteKnight)
|
||||
MoveValidator.legalTargets(b, sq(File.D, Rank.R4)).size shouldBe 8
|
||||
MoveValidator.legalTargets(b)(sq(File.D, Rank.R4)).size shouldBe 8
|
||||
|
||||
test("knight in corner has only 2 possible moves"):
|
||||
val b = board(sq(File.A, Rank.R1) -> Piece.WhiteKnight)
|
||||
MoveValidator.legalTargets(b, sq(File.A, Rank.R1)).size shouldBe 2
|
||||
MoveValidator.legalTargets(b)(sq(File.A, Rank.R1)).size shouldBe 2
|
||||
|
||||
test("knight cannot land on own piece"):
|
||||
val b = board(
|
||||
sq(File.D, Rank.R4) -> Piece.WhiteKnight,
|
||||
sq(File.F, Rank.R5) -> Piece.WhiteRook
|
||||
)
|
||||
MoveValidator.legalTargets(b, sq(File.D, Rank.R4)) should not contain sq(File.F, Rank.R5)
|
||||
MoveValidator.legalTargets(b)(sq(File.D, Rank.R4)) should not contain sq(File.F, Rank.R5)
|
||||
|
||||
test("knight can capture enemy piece"):
|
||||
val b = board(
|
||||
sq(File.D, Rank.R4) -> Piece.WhiteKnight,
|
||||
sq(File.F, Rank.R5) -> Piece.BlackRook
|
||||
)
|
||||
MoveValidator.legalTargets(b, sq(File.D, Rank.R4)) should contain(sq(File.F, Rank.R5))
|
||||
MoveValidator.legalTargets(b)(sq(File.D, Rank.R4)) should contain(sq(File.F, Rank.R5))
|
||||
|
||||
// ──── Bishop ─────────────────────────────────────────────────────────
|
||||
|
||||
test("bishop slides diagonally across an empty board"):
|
||||
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteBishop)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.D, Rank.R4))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.D, Rank.R4))
|
||||
targets should contain(sq(File.E, Rank.R5))
|
||||
targets should contain(sq(File.H, Rank.R8))
|
||||
targets should contain(sq(File.C, Rank.R3))
|
||||
@@ -132,7 +132,7 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
sq(File.D, Rank.R4) -> Piece.WhiteBishop,
|
||||
sq(File.F, Rank.R6) -> Piece.WhiteRook
|
||||
)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.D, Rank.R4))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.D, Rank.R4))
|
||||
targets should contain(sq(File.E, Rank.R5))
|
||||
targets should not contain sq(File.F, Rank.R6)
|
||||
targets should not contain sq(File.G, Rank.R7)
|
||||
@@ -142,7 +142,7 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
sq(File.D, Rank.R4) -> Piece.WhiteBishop,
|
||||
sq(File.F, Rank.R6) -> Piece.BlackRook
|
||||
)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.D, Rank.R4))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.D, Rank.R4))
|
||||
targets should contain(sq(File.E, Rank.R5))
|
||||
targets should contain(sq(File.F, Rank.R6))
|
||||
targets should not contain sq(File.G, Rank.R7)
|
||||
@@ -151,7 +151,7 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("rook slides orthogonally across an empty board"):
|
||||
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteRook)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.D, Rank.R4))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.D, Rank.R4))
|
||||
targets should contain(sq(File.D, Rank.R8))
|
||||
targets should contain(sq(File.D, Rank.R1))
|
||||
targets should contain(sq(File.A, Rank.R4))
|
||||
@@ -162,7 +162,7 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
sq(File.A, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.C, Rank.R1) -> Piece.WhitePawn
|
||||
)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.A, Rank.R1))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.A, Rank.R1))
|
||||
targets should contain(sq(File.B, Rank.R1))
|
||||
targets should not contain sq(File.C, Rank.R1)
|
||||
targets should not contain sq(File.D, Rank.R1)
|
||||
@@ -172,7 +172,7 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
sq(File.A, Rank.R1) -> Piece.WhiteRook,
|
||||
sq(File.C, Rank.R1) -> Piece.BlackPawn
|
||||
)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.A, Rank.R1))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.A, Rank.R1))
|
||||
targets should contain(sq(File.B, Rank.R1))
|
||||
targets should contain(sq(File.C, Rank.R1))
|
||||
targets should not contain sq(File.D, Rank.R1)
|
||||
@@ -181,7 +181,7 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("queen combines rook and bishop movement for 27 squares from d4"):
|
||||
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteQueen)
|
||||
val targets = MoveValidator.legalTargets(b, sq(File.D, Rank.R4))
|
||||
val targets = MoveValidator.legalTargets(b)(sq(File.D, Rank.R4))
|
||||
targets should contain(sq(File.D, Rank.R8))
|
||||
targets should contain(sq(File.H, Rank.R4))
|
||||
targets should contain(sq(File.H, Rank.R8))
|
||||
@@ -192,66 +192,63 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("king moves one step in all 8 directions from center"):
|
||||
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteKing)
|
||||
MoveValidator.legalTargets(b, sq(File.D, Rank.R4)).size shouldBe 8
|
||||
MoveValidator.legalTargets(b)(sq(File.D, Rank.R4)).size shouldBe 8
|
||||
|
||||
test("king at corner has only 3 reachable squares"):
|
||||
val b = board(sq(File.A, Rank.R1) -> Piece.WhiteKing)
|
||||
MoveValidator.legalTargets(b, sq(File.A, Rank.R1)).size shouldBe 3
|
||||
MoveValidator.legalTargets(b)(sq(File.A, Rank.R1)).size shouldBe 3
|
||||
|
||||
test("king cannot capture own piece"):
|
||||
val b = board(
|
||||
sq(File.D, Rank.R4) -> Piece.WhiteKing,
|
||||
sq(File.E, Rank.R4) -> Piece.WhiteRook
|
||||
)
|
||||
MoveValidator.legalTargets(b, sq(File.D, Rank.R4)) should not contain sq(File.E, Rank.R4)
|
||||
MoveValidator.legalTargets(b)(sq(File.D, Rank.R4)) should not contain sq(File.E, Rank.R4)
|
||||
|
||||
test("king can capture enemy piece"):
|
||||
val b = board(
|
||||
sq(File.D, Rank.R4) -> Piece.WhiteKing,
|
||||
sq(File.E, Rank.R4) -> Piece.BlackRook
|
||||
)
|
||||
MoveValidator.legalTargets(b, sq(File.D, Rank.R4)) should contain(sq(File.E, Rank.R4))
|
||||
MoveValidator.legalTargets(b)(sq(File.D, Rank.R4)) should contain(sq(File.E, Rank.R4))
|
||||
|
||||
// ──── Pawn – en passant targets ──────────────────────────────────────
|
||||
|
||||
test("white pawn includes ep target in legal moves after black double push"):
|
||||
// Black pawn just double-pushed to d5 (ep target = d6); white pawn on e5
|
||||
val b = board(
|
||||
sq(File.E, Rank.R5) -> Piece.WhitePawn,
|
||||
sq(File.D, Rank.R5) -> Piece.BlackPawn
|
||||
)
|
||||
val h = GameHistory.empty.addMove(sq(File.D, Rank.R7), sq(File.D, Rank.R5))
|
||||
MoveValidator.legalTargets(b, h, sq(File.E, Rank.R5)) should contain(sq(File.D, Rank.R6))
|
||||
MoveValidator.legalTargets(b, h)(sq(File.E, Rank.R5)) should contain(sq(File.D, Rank.R6))
|
||||
|
||||
test("white pawn does not include ep target without a preceding double push"):
|
||||
val b = board(
|
||||
sq(File.E, Rank.R5) -> Piece.WhitePawn,
|
||||
sq(File.D, Rank.R5) -> Piece.BlackPawn
|
||||
)
|
||||
val h = GameHistory.empty.addMove(sq(File.D, Rank.R6), sq(File.D, Rank.R5)) // single push
|
||||
MoveValidator.legalTargets(b, h, sq(File.E, Rank.R5)) should not contain sq(File.D, Rank.R6)
|
||||
val h = GameHistory.empty.addMove(sq(File.D, Rank.R6), sq(File.D, Rank.R5))
|
||||
MoveValidator.legalTargets(b, h)(sq(File.E, Rank.R5)) should not contain sq(File.D, Rank.R6)
|
||||
|
||||
test("black pawn includes ep target in legal moves after white double push"):
|
||||
// White pawn just double-pushed to e4 (ep target = e3); black pawn on d4
|
||||
val b = board(
|
||||
sq(File.D, Rank.R4) -> Piece.BlackPawn,
|
||||
sq(File.E, Rank.R4) -> Piece.WhitePawn
|
||||
)
|
||||
val h = GameHistory.empty.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
MoveValidator.legalTargets(b, h, sq(File.D, Rank.R4)) should contain(sq(File.E, Rank.R3))
|
||||
MoveValidator.legalTargets(b, h)(sq(File.D, Rank.R4)) should contain(sq(File.E, Rank.R3))
|
||||
|
||||
test("pawn on wrong file does not get ep target from adjacent double push"):
|
||||
// White pawn on a5, black pawn double-pushed to d5 — a5 is not adjacent to d5
|
||||
val b = board(
|
||||
sq(File.A, Rank.R5) -> Piece.WhitePawn,
|
||||
sq(File.D, Rank.R5) -> Piece.BlackPawn
|
||||
)
|
||||
val h = GameHistory.empty.addMove(sq(File.D, Rank.R7), sq(File.D, Rank.R5))
|
||||
MoveValidator.legalTargets(b, h, sq(File.A, Rank.R5)) should not contain sq(File.D, Rank.R6)
|
||||
MoveValidator.legalTargets(b, h)(sq(File.A, Rank.R5)) should not contain sq(File.D, Rank.R6)
|
||||
|
||||
// ──── History-aware legalTargets fallback for non-pawn non-king pieces ─────
|
||||
|
||||
test("legalTargets with history delegates to geometry-only for non-pawn non-king pieces"):
|
||||
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteRook)
|
||||
val h = GameHistory.empty.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4))
|
||||
MoveValidator.legalTargets(b, h, sq(File.D, Rank.R4)) shouldBe MoveValidator.legalTargets(b, sq(File.D, Rank.R4))
|
||||
MoveValidator.legalTargets(b, h)(sq(File.D, Rank.R4)) shouldBe MoveValidator.legalTargets(b)(sq(File.D, Rank.R4))
|
||||
|
||||
Reference in New Issue
Block a user