This commit is contained in:
@@ -4,9 +4,9 @@ import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.board.Square
|
||||
import de.nowchess.api.move.Move
|
||||
|
||||
/** Extension point for chess rule variants (standard, Chess960, etc.).
|
||||
* All rule queries are stateless: given a GameContext, return the answer.
|
||||
*/
|
||||
/** Extension point for chess rule variants (standard, Chess960, etc.). All rule queries are stateless: given a
|
||||
* GameContext, return the answer.
|
||||
*/
|
||||
trait RuleSet:
|
||||
/** All pseudo-legal moves for the piece on `square` (ignores check). */
|
||||
def candidateMoves(context: GameContext)(square: Square): List[Move]
|
||||
@@ -32,8 +32,7 @@ trait RuleSet:
|
||||
/** True if halfMoveClock >= 100 (50-move rule). */
|
||||
def isFiftyMoveRule(context: GameContext): Boolean
|
||||
|
||||
/** Apply a legal move to produce the next game context.
|
||||
* Handles all special move types: castling, en passant, promotion.
|
||||
* Updates castling rights, en passant square, half-move clock, turn, and move history.
|
||||
*/
|
||||
/** Apply a legal move to produce the next game context. Handles all special move types: castling, en passant,
|
||||
* promotion. Updates castling rights, en passant square, half-move clock, turn, and move history.
|
||||
*/
|
||||
def applyMove(context: GameContext)(move: Move): GameContext
|
||||
|
||||
@@ -7,20 +7,19 @@ import de.nowchess.rules.RuleSet
|
||||
|
||||
import scala.annotation.tailrec
|
||||
|
||||
/** Standard chess rules implementation.
|
||||
* Handles move generation, validation, check/checkmate/stalemate detection.
|
||||
*/
|
||||
/** Standard chess rules implementation. Handles move generation, validation, check/checkmate/stalemate detection.
|
||||
*/
|
||||
object DefaultRules extends RuleSet:
|
||||
|
||||
// ── Direction vectors ──────────────────────────────────────────────
|
||||
private val RookDirs: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1))
|
||||
private val RookDirs: List[(Int, Int)] = List((1, 0), (-1, 0), (0, 1), (0, -1))
|
||||
private val BishopDirs: List[(Int, Int)] = List((1, 1), (1, -1), (-1, 1), (-1, -1))
|
||||
private val QueenDirs: List[(Int, Int)] = RookDirs ++ BishopDirs
|
||||
private val QueenDirs: List[(Int, Int)] = RookDirs ++ BishopDirs
|
||||
private val KnightJumps: List[(Int, Int)] =
|
||||
List((2, 1), (2, -1), (-2, 1), (-2, -1), (1, 2), (1, -2), (-1, 2), (-1, -2))
|
||||
|
||||
// ── Pawn configuration helpers ─────────────────────────────────────
|
||||
private def pawnForward(color: Color): Int = if color == Color.White then 1 else -1
|
||||
private def pawnForward(color: Color): Int = if color == Color.White then 1 else -1
|
||||
private def pawnStartRank(color: Color): Int = if color == Color.White then 1 else 6
|
||||
private def pawnPromoRank(color: Color): Int = if color == Color.White then 7 else 0
|
||||
|
||||
@@ -29,13 +28,14 @@ object DefaultRules extends RuleSet:
|
||||
override def candidateMoves(context: GameContext)(square: Square): List[Move] =
|
||||
context.board.pieceAt(square).fold(List.empty[Move]) { piece =>
|
||||
if piece.color != context.turn then List.empty[Move]
|
||||
else piece.pieceType match
|
||||
case PieceType.Pawn => pawnCandidates(context, square, piece.color)
|
||||
case PieceType.Knight => knightCandidates(context, square, piece.color)
|
||||
case PieceType.Bishop => slidingMoves(context, square, piece.color, BishopDirs)
|
||||
case PieceType.Rook => slidingMoves(context, square, piece.color, RookDirs)
|
||||
case PieceType.Queen => slidingMoves(context, square, piece.color, QueenDirs)
|
||||
case PieceType.King => kingCandidates(context, square, piece.color)
|
||||
else
|
||||
piece.pieceType match
|
||||
case PieceType.Pawn => pawnCandidates(context, square, piece.color)
|
||||
case PieceType.Knight => knightCandidates(context, square, piece.color)
|
||||
case PieceType.Bishop => slidingMoves(context, square, piece.color, BishopDirs)
|
||||
case PieceType.Rook => slidingMoves(context, square, piece.color, RookDirs)
|
||||
case PieceType.Queen => slidingMoves(context, square, piece.color, QueenDirs)
|
||||
case PieceType.King => kingCandidates(context, square, piece.color)
|
||||
}
|
||||
|
||||
override def legalMoves(context: GameContext)(square: Square): List[Move] =
|
||||
@@ -65,18 +65,18 @@ object DefaultRules extends RuleSet:
|
||||
// ── Sliding pieces (Bishop, Rook, Queen) ───────────────────────────
|
||||
|
||||
private def slidingMoves(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
dirs: List[(Int, Int)]
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
dirs: List[(Int, Int)],
|
||||
): List[Move] =
|
||||
dirs.flatMap(dir => castRay(context.board, from, color, dir))
|
||||
|
||||
private def castRay(
|
||||
board: Board,
|
||||
from: Square,
|
||||
color: Color,
|
||||
dir: (Int, Int)
|
||||
board: Board,
|
||||
from: Square,
|
||||
color: Color,
|
||||
dir: (Int, Int),
|
||||
): List[Move] =
|
||||
@tailrec
|
||||
def loop(sq: Square, acc: List[Move]): List[Move] =
|
||||
@@ -84,40 +84,40 @@ object DefaultRules extends RuleSet:
|
||||
case None => acc
|
||||
case Some(next) =>
|
||||
board.pieceAt(next) match
|
||||
case None => loop(next, Move(from, next) :: acc)
|
||||
case None => loop(next, Move(from, next) :: acc)
|
||||
case Some(p) if p.color != color => Move(from, next, MoveType.Normal(isCapture = true)) :: acc
|
||||
case Some(_) => acc
|
||||
case Some(_) => acc
|
||||
loop(from, Nil).reverse
|
||||
|
||||
// ── Knight ─────────────────────────────────────────────────────────
|
||||
|
||||
private def knightCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
): List[Move] =
|
||||
KnightJumps.flatMap { (df, dr) =>
|
||||
from.offset(df, dr).flatMap { to =>
|
||||
context.board.pieceAt(to) match
|
||||
case Some(p) if p.color == color => None
|
||||
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
||||
case None => Some(Move(from, to))
|
||||
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
||||
case None => Some(Move(from, to))
|
||||
}
|
||||
}
|
||||
|
||||
// ── King ───────────────────────────────────────────────────────────
|
||||
|
||||
private def kingCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
): List[Move] =
|
||||
val steps = QueenDirs.flatMap { (df, dr) =>
|
||||
from.offset(df, dr).flatMap { to =>
|
||||
context.board.pieceAt(to) match
|
||||
case Some(p) if p.color == color => None
|
||||
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
||||
case None => Some(Move(from, to))
|
||||
case Some(_) => Some(Move(from, to, MoveType.Normal(isCapture = true)))
|
||||
case None => Some(Move(from, to))
|
||||
}
|
||||
}
|
||||
steps ++ castlingCandidates(context, from, color)
|
||||
@@ -125,17 +125,17 @@ object DefaultRules extends RuleSet:
|
||||
// ── Castling ───────────────────────────────────────────────────────
|
||||
|
||||
private case class CastlingMove(
|
||||
kingFromAlg: String,
|
||||
kingToAlg: String,
|
||||
middleAlg: String,
|
||||
rookFromAlg: String,
|
||||
moveType: MoveType
|
||||
kingFromAlg: String,
|
||||
kingToAlg: String,
|
||||
middleAlg: String,
|
||||
rookFromAlg: String,
|
||||
moveType: MoveType,
|
||||
)
|
||||
|
||||
private def castlingCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
): List[Move] =
|
||||
color match
|
||||
case Color.White => whiteCastles(context, from)
|
||||
@@ -146,10 +146,18 @@ object DefaultRules extends RuleSet:
|
||||
if from != expected then List.empty
|
||||
else
|
||||
val moves = scala.collection.mutable.ListBuffer[Move]()
|
||||
addCastleMove(context, moves, context.castlingRights.whiteKingSide,
|
||||
CastlingMove("e1", "g1", "f1", "h1", MoveType.CastleKingside))
|
||||
addCastleMove(context, moves, context.castlingRights.whiteQueenSide,
|
||||
CastlingMove("e1", "c1", "d1", "a1", MoveType.CastleQueenside))
|
||||
addCastleMove(
|
||||
context,
|
||||
moves,
|
||||
context.castlingRights.whiteKingSide,
|
||||
CastlingMove("e1", "g1", "f1", "h1", MoveType.CastleKingside),
|
||||
)
|
||||
addCastleMove(
|
||||
context,
|
||||
moves,
|
||||
context.castlingRights.whiteQueenSide,
|
||||
CastlingMove("e1", "c1", "d1", "a1", MoveType.CastleQueenside),
|
||||
)
|
||||
moves.toList
|
||||
|
||||
private def blackCastles(context: GameContext, from: Square): List[Move] =
|
||||
@@ -157,10 +165,18 @@ object DefaultRules extends RuleSet:
|
||||
if from != expected then List.empty
|
||||
else
|
||||
val moves = scala.collection.mutable.ListBuffer[Move]()
|
||||
addCastleMove(context, moves, context.castlingRights.blackKingSide,
|
||||
CastlingMove("e8", "g8", "f8", "h8", MoveType.CastleKingside))
|
||||
addCastleMove(context, moves, context.castlingRights.blackQueenSide,
|
||||
CastlingMove("e8", "c8", "d8", "a8", MoveType.CastleQueenside))
|
||||
addCastleMove(
|
||||
context,
|
||||
moves,
|
||||
context.castlingRights.blackKingSide,
|
||||
CastlingMove("e8", "g8", "f8", "h8", MoveType.CastleKingside),
|
||||
)
|
||||
addCastleMove(
|
||||
context,
|
||||
moves,
|
||||
context.castlingRights.blackQueenSide,
|
||||
CastlingMove("e8", "c8", "d8", "a8", MoveType.CastleQueenside),
|
||||
)
|
||||
moves.toList
|
||||
|
||||
private def queensideBSquare(kingToAlg: String): List[String] =
|
||||
@@ -170,10 +186,10 @@ object DefaultRules extends RuleSet:
|
||||
case _ => List.empty
|
||||
|
||||
private def addCastleMove(
|
||||
context: GameContext,
|
||||
moves: scala.collection.mutable.ListBuffer[Move],
|
||||
castlingRight: Boolean,
|
||||
castlingMove: CastlingMove
|
||||
context: GameContext,
|
||||
moves: scala.collection.mutable.ListBuffer[Move],
|
||||
castlingRight: Boolean,
|
||||
castlingMove: CastlingMove,
|
||||
): Unit =
|
||||
if castlingRight then
|
||||
val clearSqs = (List(castlingMove.middleAlg, castlingMove.kingToAlg) ++ queensideBSquare(castlingMove.kingToAlg))
|
||||
@@ -185,16 +201,15 @@ object DefaultRules extends RuleSet:
|
||||
kt <- Square.fromAlgebraic(castlingMove.kingToAlg)
|
||||
rf <- Square.fromAlgebraic(castlingMove.rookFromAlg)
|
||||
do
|
||||
val color = context.turn
|
||||
val color = context.turn
|
||||
val kingPresent = context.board.pieceAt(kf).exists(p => p.color == color && p.pieceType == PieceType.King)
|
||||
val rookPresent = context.board.pieceAt(rf).exists(p => p.color == color && p.pieceType == PieceType.Rook)
|
||||
val squaresSafe =
|
||||
!isAttackedBy(context.board, kf, color.opposite) &&
|
||||
!isAttackedBy(context.board, km, color.opposite) &&
|
||||
!isAttackedBy(context.board, kt, color.opposite)
|
||||
!isAttackedBy(context.board, km, color.opposite) &&
|
||||
!isAttackedBy(context.board, kt, color.opposite)
|
||||
|
||||
if kingPresent && rookPresent && squaresSafe then
|
||||
moves += Move(kf, kt, castlingMove.moveType)
|
||||
if kingPresent && rookPresent && squaresSafe then moves += Move(kf, kt, castlingMove.moveType)
|
||||
|
||||
private def squaresEmpty(board: Board, squares: List[Square]): Boolean =
|
||||
squares.forall(sq => board.pieceAt(sq).isEmpty)
|
||||
@@ -202,22 +217,26 @@ object DefaultRules extends RuleSet:
|
||||
// ── Pawn ───────────────────────────────────────────────────────────
|
||||
|
||||
private def pawnCandidates(
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color
|
||||
context: GameContext,
|
||||
from: Square,
|
||||
color: Color,
|
||||
): List[Move] =
|
||||
val fwd = pawnForward(color)
|
||||
val fwd = pawnForward(color)
|
||||
val startRank = pawnStartRank(color)
|
||||
val promoRank = pawnPromoRank(color)
|
||||
|
||||
val single = from.offset(0, fwd).filter(to => context.board.pieceAt(to).isEmpty)
|
||||
val double = Option.when(from.rank.ordinal == startRank) {
|
||||
from.offset(0, fwd).flatMap { mid =>
|
||||
Option.when(context.board.pieceAt(mid).isEmpty) {
|
||||
from.offset(0, fwd * 2).filter(to => context.board.pieceAt(to).isEmpty)
|
||||
}.flatten
|
||||
val double = Option
|
||||
.when(from.rank.ordinal == startRank) {
|
||||
from.offset(0, fwd).flatMap { mid =>
|
||||
Option
|
||||
.when(context.board.pieceAt(mid).isEmpty) {
|
||||
from.offset(0, fwd * 2).filter(to => context.board.pieceAt(to).isEmpty)
|
||||
}
|
||||
.flatten
|
||||
}
|
||||
}
|
||||
}.flatten
|
||||
.flatten
|
||||
|
||||
val diagonalCaptures = List(-1, 1).flatMap { df =>
|
||||
from.offset(df, fwd).flatMap { to =>
|
||||
@@ -236,22 +255,22 @@ object DefaultRules extends RuleSet:
|
||||
def toMoves(dest: Square, isCapture: Boolean): List[Move] =
|
||||
if dest.rank.ordinal == promoRank then
|
||||
List(
|
||||
PromotionPiece.Queen, PromotionPiece.Rook,
|
||||
PromotionPiece.Bishop, PromotionPiece.Knight
|
||||
PromotionPiece.Queen,
|
||||
PromotionPiece.Rook,
|
||||
PromotionPiece.Bishop,
|
||||
PromotionPiece.Knight,
|
||||
).map(pt => Move(from, dest, MoveType.Promotion(pt)))
|
||||
else List(Move(from, dest, MoveType.Normal(isCapture = isCapture)))
|
||||
|
||||
val stepSquares = single.toList ++ double.toList
|
||||
val stepMoves = stepSquares.flatMap(dest => toMoves(dest, isCapture = false))
|
||||
val stepSquares = single.toList ++ double.toList
|
||||
val stepMoves = stepSquares.flatMap(dest => toMoves(dest, isCapture = false))
|
||||
val captureMoves = diagonalCaptures.flatMap(dest => toMoves(dest, isCapture = true))
|
||||
stepMoves ++ captureMoves ++ epCaptures
|
||||
|
||||
// ── Check detection ────────────────────────────────────────────────
|
||||
|
||||
private def kingSquare(board: Board, color: Color): Option[Square] =
|
||||
Square.all.find(sq =>
|
||||
board.pieceAt(sq).exists(p => p.color == color && p.pieceType == PieceType.King)
|
||||
)
|
||||
Square.all.find(sq => board.pieceAt(sq).exists(p => p.color == color && p.pieceType == PieceType.King))
|
||||
|
||||
private def isAttackedBy(board: Board, target: Square, attacker: Color): Boolean =
|
||||
Square.all.exists { sq =>
|
||||
@@ -266,26 +285,26 @@ object DefaultRules extends RuleSet:
|
||||
case PieceType.Pawn =>
|
||||
from.offset(-1, fwd).contains(target) || from.offset(1, fwd).contains(target)
|
||||
case PieceType.Knight =>
|
||||
KnightJumps.exists { (df, dr) => from.offset(df, dr).contains(target) }
|
||||
KnightJumps.exists((df, dr) => from.offset(df, dr).contains(target))
|
||||
case PieceType.Bishop => rayReaches(board, from, BishopDirs, target)
|
||||
case PieceType.Rook => rayReaches(board, from, RookDirs, target)
|
||||
case PieceType.Queen => rayReaches(board, from, QueenDirs, target)
|
||||
case PieceType.Rook => rayReaches(board, from, RookDirs, target)
|
||||
case PieceType.Queen => rayReaches(board, from, QueenDirs, target)
|
||||
case PieceType.King =>
|
||||
QueenDirs.exists { (df, dr) => from.offset(df, dr).contains(target) }
|
||||
QueenDirs.exists((df, dr) => from.offset(df, dr).contains(target))
|
||||
|
||||
private def rayReaches(board: Board, from: Square, dirs: List[(Int, Int)], target: Square): Boolean =
|
||||
dirs.exists { dir =>
|
||||
@tailrec
|
||||
def loop(sq: Square): Boolean = sq.offset(dir._1, dir._2) match
|
||||
case None => false
|
||||
case Some(next) if next == target => true
|
||||
case None => false
|
||||
case Some(next) if next == target => true
|
||||
case Some(next) if board.pieceAt(next).isEmpty => loop(next)
|
||||
case Some(_) => false
|
||||
case Some(_) => false
|
||||
loop(from)
|
||||
}
|
||||
|
||||
private def leavesKingInCheck(context: GameContext, move: Move): Boolean =
|
||||
val nextBoard = context.board.applyMove(move)
|
||||
val nextBoard = context.board.applyMove(move)
|
||||
val nextContext = context.withBoard(nextBoard)
|
||||
isCheck(nextContext)
|
||||
|
||||
@@ -293,7 +312,7 @@ object DefaultRules extends RuleSet:
|
||||
|
||||
override def applyMove(context: GameContext)(move: Move): GameContext =
|
||||
val color = context.turn
|
||||
val board = context.board
|
||||
val board = context.board
|
||||
|
||||
val newBoard = move.moveType match
|
||||
case MoveType.CastleKingside => applyCastle(board, color, kingside = true)
|
||||
@@ -302,14 +321,14 @@ object DefaultRules extends RuleSet:
|
||||
case MoveType.Promotion(pp) => applyPromotion(board, move, color, pp)
|
||||
case MoveType.Normal(_) => board.applyMove(move)
|
||||
|
||||
val newCastlingRights = updateCastlingRights(context.castlingRights, board, move, color)
|
||||
val newCastlingRights = updateCastlingRights(context.castlingRights, board, move, color)
|
||||
val newEnPassantSquare = computeEnPassantSquare(board, move)
|
||||
val isCapture = move.moveType match
|
||||
case MoveType.Normal(capture) => capture
|
||||
case MoveType.EnPassant => true
|
||||
case _ => board.pieceAt(move.to).isDefined
|
||||
val isPawnMove = board.pieceAt(move.from).exists(_.pieceType == PieceType.Pawn)
|
||||
val newClock = if isPawnMove || isCapture then 0 else context.halfMoveClock + 1
|
||||
val newClock = if isPawnMove || isCapture then 0 else context.halfMoveClock + 1
|
||||
|
||||
context
|
||||
.withBoard(newBoard)
|
||||
@@ -322,19 +341,18 @@ object DefaultRules extends RuleSet:
|
||||
private def applyCastle(board: Board, color: Color, kingside: Boolean): Board =
|
||||
val rank = if color == Color.White then Rank.R1 else Rank.R8
|
||||
val (kingFrom, kingTo, rookFrom, rookTo) =
|
||||
if kingside then
|
||||
(Square(File.E, rank), Square(File.G, rank), Square(File.H, rank), Square(File.F, rank))
|
||||
else
|
||||
(Square(File.E, rank), Square(File.C, rank), Square(File.A, rank), Square(File.D, rank))
|
||||
if kingside then (Square(File.E, rank), Square(File.G, rank), Square(File.H, rank), Square(File.F, rank))
|
||||
else (Square(File.E, rank), Square(File.C, rank), Square(File.A, rank), Square(File.D, rank))
|
||||
val king = board.pieceAt(kingFrom).getOrElse(Piece(color, PieceType.King))
|
||||
val rook = board.pieceAt(rookFrom).getOrElse(Piece(color, PieceType.Rook))
|
||||
board
|
||||
.removed(kingFrom).removed(rookFrom)
|
||||
.removed(kingFrom)
|
||||
.removed(rookFrom)
|
||||
.updated(kingTo, king)
|
||||
.updated(rookTo, rook)
|
||||
|
||||
private def applyEnPassant(board: Board, move: Move): Board =
|
||||
val capturedRank = move.from.rank // the captured pawn is on the same rank as the moving pawn
|
||||
val capturedRank = move.from.rank // the captured pawn is on the same rank as the moving pawn
|
||||
val capturedSquare = Square(move.to.file, capturedRank)
|
||||
board.applyMove(move).removed(capturedSquare)
|
||||
|
||||
@@ -347,7 +365,7 @@ object DefaultRules extends RuleSet:
|
||||
board.removed(move.from).updated(move.to, Piece(color, promotedType))
|
||||
|
||||
private def updateCastlingRights(rights: CastlingRights, board: Board, move: Move, color: Color): CastlingRights =
|
||||
val piece = board.pieceAt(move.from)
|
||||
val piece = board.pieceAt(move.from)
|
||||
val isKingMove = piece.exists(_.pieceType == PieceType.King)
|
||||
val isRookMove = piece.exists(_.pieceType == PieceType.Rook)
|
||||
|
||||
@@ -360,14 +378,14 @@ object DefaultRules extends RuleSet:
|
||||
var r = rights
|
||||
if isKingMove then r = r.revokeColor(color)
|
||||
else if isRookMove then
|
||||
if move.from == whiteKingsideRook then r = r.revokeKingSide(Color.White)
|
||||
if move.from == whiteKingsideRook then r = r.revokeKingSide(Color.White)
|
||||
if move.from == whiteQueensideRook then r = r.revokeQueenSide(Color.White)
|
||||
if move.from == blackKingsideRook then r = r.revokeKingSide(Color.Black)
|
||||
if move.from == blackKingsideRook then r = r.revokeKingSide(Color.Black)
|
||||
if move.from == blackQueensideRook then r = r.revokeQueenSide(Color.Black)
|
||||
// Also revoke if a rook is captured
|
||||
if move.to == whiteKingsideRook then r = r.revokeKingSide(Color.White)
|
||||
if move.to == whiteKingsideRook then r = r.revokeKingSide(Color.White)
|
||||
if move.to == whiteQueensideRook then r = r.revokeQueenSide(Color.White)
|
||||
if move.to == blackKingsideRook then r = r.revokeKingSide(Color.Black)
|
||||
if move.to == blackKingsideRook then r = r.revokeKingSide(Color.Black)
|
||||
if move.to == blackQueensideRook then r = r.revokeQueenSide(Color.Black)
|
||||
r
|
||||
|
||||
@@ -386,9 +404,10 @@ object DefaultRules extends RuleSet:
|
||||
private def insufficientMaterial(board: Board): Boolean =
|
||||
val pieces = board.pieces.values.toList.filter(_.pieceType != PieceType.King)
|
||||
pieces match
|
||||
case Nil => true
|
||||
case Nil => true
|
||||
case List(p) if p.pieceType == PieceType.Bishop || p.pieceType == PieceType.Knight => true
|
||||
case List(p1, p2)
|
||||
if p1.pieceType == PieceType.Bishop && p2.pieceType == PieceType.Bishop
|
||||
&& p1.color != p2.color => true
|
||||
if p1.pieceType == PieceType.Bishop && p2.pieceType == PieceType.Bishop
|
||||
&& p1.color != p2.color =>
|
||||
true
|
||||
case _ => false
|
||||
|
||||
+26
-30
@@ -65,7 +65,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove clears en passant square for non double pawn push"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/4P3/4K3 w - d6 3 1")
|
||||
val move = Move(sq("e2"), sq("e3"))
|
||||
val move = Move(sq("e2"), sq("e3"))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -73,7 +73,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove resets halfMoveClock on pawn move"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/4P3/4K3 w - - 12 1")
|
||||
val move = Move(sq("e2"), sq("e4"))
|
||||
val move = Move(sq("e2"), sq("e4"))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -81,7 +81,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove increments halfMoveClock on quiet non pawn move"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/8/4K1N1 w - - 7 1")
|
||||
val move = Move(sq("g1"), sq("f3"))
|
||||
val move = Move(sq("g1"), sq("f3"))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -89,7 +89,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove resets halfMoveClock on capture"):
|
||||
val context = contextFromFen("r3k3/8/8/8/8/8/8/R3K3 w Qq - 9 1")
|
||||
val move = Move(sq("a1"), sq("a8"), MoveType.Normal(isCapture = true))
|
||||
val move = Move(sq("a1"), sq("a8"), MoveType.Normal(isCapture = true))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -98,7 +98,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove updates castling rights after king move"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")
|
||||
val move = Move(sq("e1"), sq("e2"))
|
||||
val move = Move(sq("e1"), sq("e2"))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -109,7 +109,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove updates castling rights after rook move from h1"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K2R w KQkq - 0 1")
|
||||
val move = Move(sq("h1"), sq("h2"))
|
||||
val move = Move(sq("h1"), sq("h2"))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -118,7 +118,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove revokes opponent castling right when rook on starting square is captured"):
|
||||
val context = contextFromFen("r3k3/8/8/8/8/8/8/R3K3 w Qq - 2 1")
|
||||
val move = Move(sq("a1"), sq("a8"), MoveType.Normal(isCapture = true))
|
||||
val move = Move(sq("a1"), sq("a8"), MoveType.Normal(isCapture = true))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -126,7 +126,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove executes kingside castling and repositions king and rook"):
|
||||
val context = contextFromFen("4k2r/8/8/8/8/8/8/R3K2R w KQk - 0 1")
|
||||
val move = Move(sq("e1"), sq("g1"), MoveType.CastleKingside)
|
||||
val move = Move(sq("e1"), sq("g1"), MoveType.CastleKingside)
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -137,7 +137,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove executes queenside castling and repositions king and rook"):
|
||||
val context = contextFromFen("r3k3/8/8/8/8/8/8/R3K2R w KQq - 0 1")
|
||||
val move = Move(sq("e1"), sq("c1"), MoveType.CastleQueenside)
|
||||
val move = Move(sq("e1"), sq("c1"), MoveType.CastleQueenside)
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -148,7 +148,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove executes en passant and removes captured pawn"):
|
||||
val context = contextFromFen("k7/8/8/3pP3/8/8/8/7K w - d6 0 1")
|
||||
val move = Move(sq("e5"), sq("d6"), MoveType.EnPassant)
|
||||
val move = Move(sq("e5"), sq("d6"), MoveType.EnPassant)
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -158,7 +158,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove executes promotion with selected piece type"):
|
||||
val context = contextFromFen("4k3/P7/8/8/8/8/8/4K3 w - - 0 1")
|
||||
val move = Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Knight))
|
||||
val move = Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Knight))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -179,7 +179,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove preserves black castling rights after white kingside castling"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1")
|
||||
val move = Move(sq("e1"), sq("g1"), MoveType.CastleKingside)
|
||||
val move = Move(sq("e1"), sq("g1"), MoveType.CastleKingside)
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -190,16 +190,18 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove can revoke both white castling rights when both rooks are captured"):
|
||||
val context = GameContext(
|
||||
board = contextFromFen("4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1").board.updated(sq("a8"), Piece(Color.Black, PieceType.Queen)),
|
||||
board =
|
||||
contextFromFen("4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1").board.updated(sq("a8"), Piece(Color.Black, PieceType.Queen)),
|
||||
turn = Color.Black,
|
||||
castlingRights = CastlingRights(true, true, false, false),
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty
|
||||
moves = List.empty,
|
||||
)
|
||||
|
||||
val afterA1Capture = DefaultRules.applyMove(context)(Move(sq("a8"), sq("a1"), MoveType.Normal(isCapture = true)))
|
||||
val afterH1Capture = DefaultRules.applyMove(afterA1Capture)(Move(sq("a1"), sq("h1"), MoveType.Normal(isCapture = true)))
|
||||
val afterH1Capture =
|
||||
DefaultRules.applyMove(afterA1Capture)(Move(sq("a1"), sq("h1"), MoveType.Normal(isCapture = true)))
|
||||
|
||||
afterH1Capture.castlingRights.whiteKingSide shouldBe false
|
||||
afterH1Capture.castlingRights.whiteQueenSide shouldBe false
|
||||
@@ -233,7 +235,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove executes black kingside castling and repositions pieces on rank 8"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K3 b kq - 0 1")
|
||||
val move = Move(sq("e8"), sq("g8"), MoveType.CastleKingside)
|
||||
val move = Move(sq("e8"), sq("g8"), MoveType.CastleKingside)
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -244,7 +246,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove revokes black castling rights when black rook moves from h8"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K3 b kq - 0 1")
|
||||
val move = Move(sq("h8"), sq("h7"))
|
||||
val move = Move(sq("h8"), sq("h7"))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -253,7 +255,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove revokes black queenside castling right when black rook moves from a8"):
|
||||
val context = contextFromFen("r3k2r/8/8/8/8/8/8/4K3 b kq - 0 1")
|
||||
val move = Move(sq("a8"), sq("a7"))
|
||||
val move = Move(sq("a8"), sq("a7"))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -262,7 +264,7 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("applyMove revokes black kingside castling right when rook on h8 is captured"):
|
||||
val context = contextFromFen("4k2r/8/8/8/8/8/8/4K2R w Kk - 0 1")
|
||||
val move = Move(sq("h1"), sq("h8"), MoveType.Normal(isCapture = true))
|
||||
val move = Move(sq("h1"), sq("h8"), MoveType.Normal(isCapture = true))
|
||||
|
||||
val next = DefaultRules.applyMove(context)(move)
|
||||
|
||||
@@ -270,31 +272,25 @@ class DefaultRulesStateTransitionsTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("candidateMoves creates all promotion move variants for black pawn"):
|
||||
val context = contextFromFen("4k3/8/8/8/8/8/p7/4K3 b - - 0 1")
|
||||
val to = sq("a1")
|
||||
val to = sq("a1")
|
||||
|
||||
val pawnMoves = DefaultRules.candidateMoves(context)(sq("a2"))
|
||||
val pawnMoves = DefaultRules.candidateMoves(context)(sq("a2"))
|
||||
val promotions = pawnMoves.collect { case Move(_, `to`, MoveType.Promotion(piece)) => piece }
|
||||
|
||||
promotions.toSet shouldBe Set(
|
||||
PromotionPiece.Queen,
|
||||
PromotionPiece.Rook,
|
||||
PromotionPiece.Bishop,
|
||||
PromotionPiece.Knight
|
||||
PromotionPiece.Knight,
|
||||
)
|
||||
|
||||
test("applyMove promotion supports queen rook and bishop targets"):
|
||||
val base = contextFromFen("4k3/P7/8/8/8/8/8/4K3 w - - 0 1")
|
||||
|
||||
val queen = DefaultRules.applyMove(base)(Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Queen)))
|
||||
val rook = DefaultRules.applyMove(base)(Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Rook)))
|
||||
val queen = DefaultRules.applyMove(base)(Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Queen)))
|
||||
val rook = DefaultRules.applyMove(base)(Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Rook)))
|
||||
val bishop = DefaultRules.applyMove(base)(Move(sq("a7"), sq("a8"), MoveType.Promotion(PromotionPiece.Bishop)))
|
||||
|
||||
queen.board.pieceAt(sq("a8")) shouldBe Some(Piece(Color.White, PieceType.Queen))
|
||||
rook.board.pieceAt(sq("a8")) shouldBe Some(Piece(Color.White, PieceType.Rook))
|
||||
bishop.board.pieceAt(sq("a8")) shouldBe Some(Piece(Color.White, PieceType.Bishop))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package de.nowchess.rule
|
||||
|
||||
import de.nowchess.api.board.{Board, Color, File, Rank, Square, Piece, PieceType, CastlingRights}
|
||||
import de.nowchess.api.board.{Board, CastlingRights, Color, File, Piece, PieceType, Rank, Square}
|
||||
import de.nowchess.api.game.GameContext
|
||||
import de.nowchess.api.move.{Move, MoveType}
|
||||
import de.nowchess.io.fen.FenParser
|
||||
@@ -15,31 +15,31 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
|
||||
// ── Pawn moves ──────────────────────────────────────────────────
|
||||
|
||||
test("pawn can move forward one square"):
|
||||
val fen = "8/8/8/8/8/8/4P3/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val fen = "8/8/8/8/8/8/4P3/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val pawnMoves = moves.filter(m => m.from == Square(File.E, Rank.R2))
|
||||
pawnMoves.exists(m => m.to == Square(File.E, Rank.R3)) shouldBe true
|
||||
|
||||
test("pawn can move forward two squares from starting position"):
|
||||
val context = GameContext.initial
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val e2Moves = moves.filter(m => m.from == Square(File.E, Rank.R2))
|
||||
e2Moves.exists(m => m.to == Square(File.E, Rank.R4)) shouldBe true
|
||||
|
||||
test("pawn can capture diagonally"):
|
||||
// FEN: white pawn e4, black pawn d5
|
||||
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val fen = "8/8/8/3p4/4P3/8/8/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val captures = moves.filter(m => m.from == Square(File.E, Rank.R4) && m.moveType.isInstanceOf[MoveType.Normal])
|
||||
captures.exists(m => m.to == Square(File.D, Rank.R5)) shouldBe true
|
||||
|
||||
test("pawn cannot move backward"):
|
||||
// FEN: white pawn on e4
|
||||
val fen = "8/8/8/8/4P3/8/8/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val fen = "8/8/8/8/4P3/8/8/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val pawnMoves = moves.filter(m => m.from == Square(File.E, Rank.R4))
|
||||
pawnMoves.exists(m => m.to == Square(File.E, Rank.R3)) shouldBe false
|
||||
|
||||
@@ -47,18 +47,18 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("moving king out of check removes it from legal moves if king stays in check"):
|
||||
// FEN: white king e1, black rook e8, white tries to move away
|
||||
val fen = "4r3/8/8/8/8/8/8/4K3 w - - 0 1"
|
||||
val fen = "4r3/8/8/8/8/8/8/4K3 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
// King must move; e2 should be valid but d1 might be blocked by rook if still on same file
|
||||
moves.exists(m => m.from == Square(File.E, Rank.R1)) shouldBe true
|
||||
|
||||
test("king cannot move to square attacked by opponent"):
|
||||
// FEN: white king e1, black rook e2 defended by black king e3
|
||||
val fen = "8/8/8/8/8/4k3/4r3/4K3 w - - 0 1"
|
||||
val fen = "8/8/8/8/8/4k3/4r3/4K3 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
// King cannot move to e2 (occupied and attacked)
|
||||
val kingMovesToE2 = moves.filter(m => m.from == Square(File.E, Rank.R1) && m.to == Square(File.E, Rank.R2))
|
||||
@@ -67,64 +67,67 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
|
||||
// ── Castling legality ────────────────────────────────────────────
|
||||
|
||||
test("castling kingside is legal when king and rook unmoved and path clear"):
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w KQkq - 0 1"
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w KQkq - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
|
||||
castles.nonEmpty shouldBe true
|
||||
|
||||
test("castling queenside is legal when king and rook unmoved and path clear"):
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1"
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/R3K2R w KQkq - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
val castles = moves.filter(m => m.moveType == MoveType.CastleQueenside)
|
||||
castles.nonEmpty shouldBe true
|
||||
|
||||
test("castling is illegal when castling rights are false"):
|
||||
// FEN: king and rook in position, but castling rights disabled
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w - - 0 1"
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQK2R w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
|
||||
castles.isEmpty shouldBe true
|
||||
|
||||
test("castling is illegal when king is in check"):
|
||||
// FEN: white king e1 in check from black rook e8
|
||||
val fen = "4r3/8/8/8/8/8/8/R3K2R w KQ - 0 1"
|
||||
val fen = "4r3/8/8/8/8/8/8/R3K2R w KQ - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
val castles = moves.filter(m => m.moveType == MoveType.CastleKingside || m.moveType == MoveType.CastleQueenside)
|
||||
castles.isEmpty shouldBe true
|
||||
|
||||
test("castling is illegal when path has piece in the way"):
|
||||
// FEN: white king e1, white rook h1, white bishop f1 (blocks f-file)
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBR1 w KQkq - 0 1"
|
||||
val fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBR1 w KQkq - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
val castles = moves.filter(m => m.moveType == MoveType.CastleKingside)
|
||||
castles.isEmpty shouldBe true
|
||||
|
||||
test("castling queenside is illegal when knight blocks on b8"):
|
||||
// Black king e8, black rook a8, black knight b8 (blocks queenside path)
|
||||
val board = Board(Map(
|
||||
Square(File.A, Rank.R8) -> Piece(Color.Black, PieceType.Rook),
|
||||
Square(File.B, Rank.R8) -> Piece(Color.Black, PieceType.Knight),
|
||||
Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King),
|
||||
Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook),
|
||||
Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King)
|
||||
))
|
||||
val board = Board(
|
||||
Map(
|
||||
Square(File.A, Rank.R8) -> Piece(Color.Black, PieceType.Rook),
|
||||
Square(File.B, Rank.R8) -> Piece(Color.Black, PieceType.Knight),
|
||||
Square(File.E, Rank.R8) -> Piece(Color.Black, PieceType.King),
|
||||
Square(File.A, Rank.R1) -> Piece(Color.White, PieceType.Rook),
|
||||
Square(File.E, Rank.R1) -> Piece(Color.White, PieceType.King),
|
||||
),
|
||||
)
|
||||
val context = GameContext(
|
||||
board = board,
|
||||
turn = Color.Black,
|
||||
castlingRights = CastlingRights(whiteKingSide = true, whiteQueenSide = true, blackKingSide = true, blackQueenSide = true),
|
||||
castlingRights =
|
||||
CastlingRights(whiteKingSide = true, whiteQueenSide = true, blackKingSide = true, blackQueenSide = true),
|
||||
enPassantSquare = None,
|
||||
halfMoveClock = 0,
|
||||
moves = List.empty
|
||||
moves = List.empty,
|
||||
)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
@@ -135,18 +138,18 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("en passant is legal when en passant square is set"):
|
||||
// FEN: white pawn e5, black pawn d5 (just double-pushed), en passant square d6
|
||||
val fen = "k7/8/8/3pP3/8/8/8/7K w - d6 0 1"
|
||||
val fen = "k7/8/8/3pP3/8/8/8/7K w - d6 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
val epMoves = moves.filter(m => m.moveType == MoveType.EnPassant)
|
||||
epMoves.exists(m => m.to == Square(File.D, Rank.R6)) shouldBe true
|
||||
|
||||
test("en passant is illegal when en passant square is none"):
|
||||
// FEN: white pawn e5, black pawn d5, but no en passant square
|
||||
val fen = "k7/8/8/3pP3/8/8/8/7K w - - 0 1"
|
||||
val fen = "k7/8/8/3pP3/8/8/8/7K w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
val epMoves = moves.filter(m => m.moveType == MoveType.EnPassant)
|
||||
epMoves.isEmpty shouldBe true
|
||||
@@ -155,9 +158,9 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
|
||||
|
||||
test("pinned piece cannot move and expose king to check"):
|
||||
// FEN: white king e1, white bishop d2 (pinned), black rook a2
|
||||
val fen = "8/8/8/8/8/8/r1B1K3/8 w - - 0 1"
|
||||
val fen = "8/8/8/8/8/8/r1B1K3/8 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
// Bishop on d2 is pinned by rook on a2; it cannot move
|
||||
val bishopMoves = moves.filter(m => m.from == Square(File.C, Rank.R2))
|
||||
@@ -166,9 +169,9 @@ class DefaultRulesTest extends AnyFunSuite with Matchers:
|
||||
test("piece blocking a check is legal"):
|
||||
// FEN: white king e1, white rook d1, black bishop a4 attacking e1 via d2
|
||||
// Actually, this is complex. Let's use: white king e1, black rook e8, white pawn blocks on e2
|
||||
val fen = "4r3/8/8/8/8/8/4P3/4K3 w - - 0 1"
|
||||
val fen = "4r3/8/8/8/8/8/4P3/4K3 w - - 0 1"
|
||||
val context = FenParser.parseFen(fen).fold(_ => fail(), identity)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
val moves = rules.allLegalMoves(context)
|
||||
|
||||
// White is in check; only moves that block or move the king are legal
|
||||
moves.nonEmpty shouldBe true
|
||||
|
||||
Reference in New Issue
Block a user