refactor: NCS-4 curry MoveValidator public API and update all call sites

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
LQ63
2026-03-31 16:35:32 +02:00
parent 87806d516f
commit 38936b2e10
5 changed files with 57 additions and 58 deletions
@@ -43,10 +43,10 @@ object GameController:
case Some(piece) if piece.color != turn => case Some(piece) if piece.color != turn =>
MoveResult.WrongColor MoveResult.WrongColor
case Some(_) => case Some(_) =>
if !MoveValidator.isLegal(board, history, from, to) then if !MoveValidator.isLegal(board, history)(from, to) then
MoveResult.IllegalMove MoveResult.IllegalMove
else else
val castleOpt = if MoveValidator.isCastle(board, from, to) val castleOpt = if MoveValidator.isCastle(board)(from, to)
then Some(MoveValidator.castleSide(from, to)) then Some(MoveValidator.castleSide(from, to))
else None else None
val isEP = EnPassantCalculator.isEnPassant(board, history, from, to) val isEP = EnPassantCalculator.isEnPassant(board, history, from, to)
@@ -13,21 +13,23 @@ object GameRules:
board.pieces board.pieces
.collectFirst { case (sq, p) if p.color == color && p.pieceType == PieceType.King => sq } .collectFirst { case (sq, p) if p.color == color && p.pieceType == PieceType.King => sq }
.exists { kingSq => .exists { kingSq =>
val targets = MoveValidator.legalTargets(board)
board.pieces.exists { case (sq, piece) => board.pieces.exists { case (sq, piece) =>
piece.color != color && 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. */ /** 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 board.pieces
.collect { case (from, piece) if piece.color == color => from } .collect { case (from, piece) if piece.color == color => from }
.flatMap { from => .flatMap { from =>
MoveValidator.legalTargets(board, history, from) // context-aware: includes castling targets(from)
.filter { to => .filter { to =>
val newBoard = val newBoard =
if MoveValidator.isCastle(board, from, to) then if MoveValidator.isCastle(board)(from, to) then
board.withCastle(color, MoveValidator.castleSide(from, to)) board.withCastle(color, MoveValidator.castleSide(from, to))
else else
board.withMove(from, to)._1 board.withMove(from, to)._1
@@ -11,11 +11,11 @@ object MoveValidator:
* - cannot capture own pieces * - cannot capture own pieces
* - sliding pieces (bishop, rook, queen) are blocked by intervening pieces * - sliding pieces (bishop, rook, queen) are blocked by intervening pieces
*/ */
def isLegal(board: Board, from: Square, to: Square): Boolean = def isLegal(board: Board)(from: Square, to: Square): Boolean =
legalTargets(board, from).contains(to) legalTargets(board)(from).contains(to)
/** All squares a piece on `from` can legally move to (same rules as isLegal). */ /** 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 board.pieceAt(from) match
case None => Set.empty case None => Set.empty
case Some(piece) => case Some(piece) =>
@@ -70,9 +70,9 @@ object MoveValidator:
.toSet .toSet
private def pawnTargets(board: Board, from: Square, color: Color): Set[Square] = private def pawnTargets(board: Board, from: Square, color: Color): Set[Square] =
val fi = from.file.ordinal val fi = from.file.ordinal
val ri = from.rank.ordinal val ri = from.rank.ordinal
val dir = if color == Color.White then 1 else -1 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 startRank = if color == Color.White then 1 else 6 // R2 = ordinal 1, R7 = ordinal 6
val oneStep = squareAt(fi, ri + dir) val oneStep = squareAt(fi, ri + dir)
@@ -116,17 +116,17 @@ object MoveValidator:
private def isAttackedBy(board: Board, sq: Square, attackerColor: Color): Boolean = private def isAttackedBy(board: Board, sq: Square, attackerColor: Color): Boolean =
board.pieces.exists { case (from, piece) => 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) && board.pieceAt(from).exists(_.pieceType == PieceType.King) &&
math.abs(to.file.ordinal - from.file.ordinal) == 2 math.abs(to.file.ordinal - from.file.ordinal) == 2
def castleSide(from: Square, to: Square): CastleSide = def castleSide(from: Square, to: Square): CastleSide =
if to.file.ordinal > from.file.ordinal then CastleSide.Kingside else CastleSide.Queenside 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 rights = CastlingRightsCalculator.deriveCastlingRights(history, color)
val rank = if color == Color.White then Rank.R1 else Rank.R8 val rank = if color == Color.White then Rank.R1 else Rank.R8
val kingSq = Square(File.E, rank) val kingSq = Square(File.E, rank)
@@ -151,14 +151,14 @@ object MoveValidator:
kingsideSq.toSet ++ queensideSq.toSet 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 board.pieceAt(from) match
case Some(piece) if piece.pieceType == PieceType.King => 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 => case Some(piece) if piece.pieceType == PieceType.Pawn =>
pawnTargets(board, history, from, piece.color) pawnTargets(board, history, from, piece.color)
case _ => case _ =>
legalTargets(board, from) legalTargets(board)(from)
private def pawnTargets(board: Board, history: GameHistory, from: Square, color: Color): Set[Square] = private def pawnTargets(board: Board, history: GameHistory, from: Square, color: Color): Set[Square] =
val existing = pawnTargets(board, from, color) val existing = pawnTargets(board, from, color)
@@ -171,5 +171,5 @@ object MoveValidator:
.toSet .toSet
existing ++ epCapture existing ++ epCapture
def isLegal(board: Board, history: GameHistory, from: Square, to: Square): Boolean = def isLegal(board: Board, history: GameHistory)(from: Square, to: Square): Boolean =
legalTargets(board, history, from).contains(to) legalTargets(board, history)(from).contains(to)
@@ -106,7 +106,7 @@ object PgnParser:
val reachable: Set[Square] = val reachable: Set[Square] =
board.pieces.collect { board.pieces.collect {
case (from, piece) if piece.color == color && case (from, piece) if piece.color == color &&
MoveValidator.legalTargets(board, from).contains(toSquare) => from MoveValidator.legalTargets(board)(from).contains(toSquare) => from
}.toSet }.toSet
val candidates: Set[Square] = val candidates: Set[Square] =
@@ -14,36 +14,36 @@ class MoveValidatorTest extends AnyFunSuite with Matchers:
// ──── Empty square ─────────────────────────────────────────────────── // ──── Empty square ───────────────────────────────────────────────────
test("legalTargets returns empty set when no piece at from 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 ────────────────────────────── // ──── isLegal delegates to legalTargets ──────────────────────────────
test("isLegal returns true for a valid pawn move"): 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"): 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 ─────────────────────────────────────────────────── // ──── Pawn – White ───────────────────────────────────────────────────
test("white pawn on starting rank can move forward one square"): test("white pawn on starting rank can move forward one square"):
val b = board(sq(File.E, Rank.R2) -> Piece.WhitePawn) 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"): test("white pawn on starting rank can move forward two squares"):
val b = board(sq(File.E, Rank.R2) -> Piece.WhitePawn) 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"): test("white pawn not on starting rank cannot move two squares"):
val b = board(sq(File.E, Rank.R3) -> Piece.WhitePawn) 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"): test("white pawn is blocked by piece directly in front, and cannot jump over it"):
val b = board( val b = board(
sq(File.E, Rank.R2) -> Piece.WhitePawn, sq(File.E, Rank.R2) -> Piece.WhitePawn,
sq(File.E, Rank.R3) -> Piece.BlackPawn 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.R3)
targets should not contain sq(File.E, Rank.R4) 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.R2) -> Piece.WhitePawn,
sq(File.E, Rank.R4) -> Piece.BlackPawn 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 contain(sq(File.E, Rank.R3))
targets should not contain sq(File.E, Rank.R4) 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.E, Rank.R2) -> Piece.WhitePawn,
sq(File.D, Rank.R3) -> Piece.BlackPawn 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"): test("white pawn cannot capture diagonally when no enemy piece is present"):
val b = board(sq(File.E, Rank.R2) -> Piece.WhitePawn) 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"): 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 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.R3))
targets should contain(sq(File.A, Rank.R4)) targets should contain(sq(File.A, Rank.R4))
targets.size shouldBe 2 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"): test("black pawn on starting rank can move forward one and two squares"):
val b = board(sq(File.E, Rank.R7) -> Piece.BlackPawn) 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.R6))
targets should contain(sq(File.E, Rank.R5)) targets should contain(sq(File.E, Rank.R5))
test("black pawn not on starting rank cannot move two squares"): test("black pawn not on starting rank cannot move two squares"):
val b = board(sq(File.E, Rank.R6) -> Piece.BlackPawn) 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"): test("black pawn can capture diagonally when enemy piece is present"):
val b = board( val b = board(
sq(File.E, Rank.R7) -> Piece.BlackPawn, sq(File.E, Rank.R7) -> Piece.BlackPawn,
sq(File.F, Rank.R6) -> Piece.WhitePawn 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 ───────────────────────────────────────────────────────── // ──── Knight ─────────────────────────────────────────────────────────
test("knight in center has 8 possible moves"): test("knight in center has 8 possible moves"):
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteKnight) 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"): test("knight in corner has only 2 possible moves"):
val b = board(sq(File.A, Rank.R1) -> Piece.WhiteKnight) 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"): test("knight cannot land on own piece"):
val b = board( val b = board(
sq(File.D, Rank.R4) -> Piece.WhiteKnight, sq(File.D, Rank.R4) -> Piece.WhiteKnight,
sq(File.F, Rank.R5) -> Piece.WhiteRook 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"): test("knight can capture enemy piece"):
val b = board( val b = board(
sq(File.D, Rank.R4) -> Piece.WhiteKnight, sq(File.D, Rank.R4) -> Piece.WhiteKnight,
sq(File.F, Rank.R5) -> Piece.BlackRook 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 ───────────────────────────────────────────────────────── // ──── Bishop ─────────────────────────────────────────────────────────
test("bishop slides diagonally across an empty board"): test("bishop slides diagonally across an empty board"):
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteBishop) 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.E, Rank.R5))
targets should contain(sq(File.H, Rank.R8)) targets should contain(sq(File.H, Rank.R8))
targets should contain(sq(File.C, Rank.R3)) 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.D, Rank.R4) -> Piece.WhiteBishop,
sq(File.F, Rank.R6) -> Piece.WhiteRook 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 contain(sq(File.E, Rank.R5))
targets should not contain sq(File.F, Rank.R6) targets should not contain sq(File.F, Rank.R6)
targets should not contain sq(File.G, Rank.R7) 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.D, Rank.R4) -> Piece.WhiteBishop,
sq(File.F, Rank.R6) -> Piece.BlackRook 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.E, Rank.R5))
targets should contain(sq(File.F, Rank.R6)) targets should contain(sq(File.F, Rank.R6))
targets should not contain sq(File.G, Rank.R7) 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"): test("rook slides orthogonally across an empty board"):
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteRook) 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.R8))
targets should contain(sq(File.D, Rank.R1)) targets should contain(sq(File.D, Rank.R1))
targets should contain(sq(File.A, Rank.R4)) 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.A, Rank.R1) -> Piece.WhiteRook,
sq(File.C, Rank.R1) -> Piece.WhitePawn 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 contain(sq(File.B, Rank.R1))
targets should not contain sq(File.C, Rank.R1) targets should not contain sq(File.C, Rank.R1)
targets should not contain sq(File.D, 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.A, Rank.R1) -> Piece.WhiteRook,
sq(File.C, Rank.R1) -> Piece.BlackPawn 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.B, Rank.R1))
targets should contain(sq(File.C, Rank.R1)) targets should contain(sq(File.C, Rank.R1))
targets should not contain sq(File.D, 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"): test("queen combines rook and bishop movement for 27 squares from d4"):
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteQueen) 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.D, Rank.R8))
targets should contain(sq(File.H, Rank.R4)) targets should contain(sq(File.H, Rank.R4))
targets should contain(sq(File.H, Rank.R8)) 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"): test("king moves one step in all 8 directions from center"):
val b = board(sq(File.D, Rank.R4) -> Piece.WhiteKing) 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"): test("king at corner has only 3 reachable squares"):
val b = board(sq(File.A, Rank.R1) -> Piece.WhiteKing) 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"): test("king cannot capture own piece"):
val b = board( val b = board(
sq(File.D, Rank.R4) -> Piece.WhiteKing, sq(File.D, Rank.R4) -> Piece.WhiteKing,
sq(File.E, Rank.R4) -> Piece.WhiteRook 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"): test("king can capture enemy piece"):
val b = board( val b = board(
sq(File.D, Rank.R4) -> Piece.WhiteKing, sq(File.D, Rank.R4) -> Piece.WhiteKing,
sq(File.E, Rank.R4) -> Piece.BlackRook 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 ────────────────────────────────────── // ──── Pawn en passant targets ──────────────────────────────────────
test("white pawn includes ep target in legal moves after black double push"): 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( val b = board(
sq(File.E, Rank.R5) -> Piece.WhitePawn, sq(File.E, Rank.R5) -> Piece.WhitePawn,
sq(File.D, Rank.R5) -> Piece.BlackPawn sq(File.D, Rank.R5) -> Piece.BlackPawn
) )
val h = GameHistory.empty.addMove(sq(File.D, Rank.R7), sq(File.D, Rank.R5)) 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"): test("white pawn does not include ep target without a preceding double push"):
val b = board( val b = board(
sq(File.E, Rank.R5) -> Piece.WhitePawn, sq(File.E, Rank.R5) -> Piece.WhitePawn,
sq(File.D, Rank.R5) -> Piece.BlackPawn sq(File.D, Rank.R5) -> Piece.BlackPawn
) )
val h = GameHistory.empty.addMove(sq(File.D, Rank.R6), sq(File.D, Rank.R5)) // single push 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) 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"): 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( val b = board(
sq(File.D, Rank.R4) -> Piece.BlackPawn, sq(File.D, Rank.R4) -> Piece.BlackPawn,
sq(File.E, Rank.R4) -> Piece.WhitePawn sq(File.E, Rank.R4) -> Piece.WhitePawn
) )
val h = GameHistory.empty.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4)) 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"): 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( val b = board(
sq(File.A, Rank.R5) -> Piece.WhitePawn, sq(File.A, Rank.R5) -> Piece.WhitePawn,
sq(File.D, Rank.R5) -> Piece.BlackPawn sq(File.D, Rank.R5) -> Piece.BlackPawn
) )
val h = GameHistory.empty.addMove(sq(File.D, Rank.R7), sq(File.D, Rank.R5)) 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 ───── // ──── History-aware legalTargets fallback for non-pawn non-king pieces ─────
test("legalTargets with history delegates to geometry-only 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 b = board(sq(File.D, Rank.R4) -> Piece.WhiteRook)
val h = GameHistory.empty.addMove(sq(File.E, Rank.R2), sq(File.E, Rank.R4)) 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))