Fix bugs with en passant and king movement

This commit is contained in:
Mattia Giambirtone 2024-04-12 17:03:45 +02:00
parent f75f7533f5
commit 2ada052460
1 changed files with 50 additions and 17 deletions

View File

@ -693,6 +693,7 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king:
of None:
# Unreachable
discard
# Some of these checks may seem redundant, but we
# perform them because they're less expensive
@ -789,7 +790,11 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
## Returns the squares that need to be covered to
## resolve the current check (including capturing
## the checking piece). In case of double check, an
## empty list is returned (as the king must move)
## empty list is returned (as the king must move).
## Note that this function does not handle the special
## case of a friendly pawn being able to capture an enemy
## pawn that is checking our friendly king via en passant:
## that is handled internally by generatePawnMoves
var king: Location
case color:
of White:
@ -810,12 +815,12 @@ proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] =
var attack = self.getAttackFor(attacker, king)
# Capturing the piece resolves the check
result.add(attacker)
# Blocking the attack is also a viable strategy
# (unless the check is from a knight or a pawn,
# in which case either the king has to move or
# that piece has to be captured, but this is
# already implicitly handled by the loop below)
var location = attacker
while location != king:
location = location + attack.direction
@ -830,7 +835,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
var
piece = self.grid[location.row, location.col]
directions: seq[Location] = @[]
doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
assert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}"
# Pawns can move forward one square
let forward = location + piece.color.topSide()
# Only if the square is empty though
@ -842,24 +847,32 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
# Check that both squares are empty
if double.isValid() and self.grid[forward].color == None and self.grid[double].color == None:
directions.add(piece.color.doublePush())
let enPassantPawn = self.getEnPassantTarget() + piece.color.opposite().topSide()
let
enPassantTarget = self.getEnPassantTarget()
enPassantPawn = enPassantTarget + piece.color.opposite().topSide()
topLeft = piece.color.topLeftDiagonal()
topRight = piece.color.topRightDiagonal()
var enPassantLegal = false
# They can also move one square on either of their
# forward diagonals, but only for captures and en passant
for diagonal in [piece.color.topRightDiagonal(), piece.color.topLeftDiagonal()]:
for diagonal in [topRight, topLeft]:
let target = location + diagonal
if target.isValid():
let otherPiece = self.grid[target]
if target == self.position.enPassantSquare and self.grid[enPassantPawn].color == piece.color.opposite():
if target == enPassantTarget and self.grid[enPassantPawn].color == piece.color.opposite():
# En passant may be possible
let targetPawn = self.grid[enPassantPawn]
# Remove both pieces and see if the king ends up in check
# Simulate the move and see if the king ends up in check
self.removePiece(enPassantPawn, attack=false)
self.removePiece(location, attack=false)
self.spawnPiece(target, piece)
self.updateAttackedSquares()
if not self.inCheck(piece.color):
# King is not in check after en passant: move is legal
directions.add(diagonal)
enPassantLegal = true
# Reset what we just did and reupdate the attack metadata
self.removePiece(target, attack=false)
self.spawnPiece(location, piece)
self.spawnPiece(enPassantPawn, targetPawn)
self.updateAttackedSquares()
@ -875,7 +888,13 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
newDirections.add(direction)
directions = newDirections
let checked = self.inCheck()
let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
var resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color)
# If the check comes from a pawn and en passant is legal and would capture it,
# we add that to the list of possible check resolutions
if checked and enPassantLegal:
let attackingPawn = self.getAttackFor(enPassantPawn, self.getKing(piece.color))
if attackingPawn.source == enPassantPawn:
resolutions.add(enPassantTarget)
var targetPiece: Piece
for direction in directions:
let target = location + direction
@ -900,7 +919,7 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] =
proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] =
## Generates moves for the sliding piece in the given location
let piece = self.grid[location.row, location.col]
doAssert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}"
assert piece.kind in [Bishop, Rook, Queen], &"generateSlidingMoves called on a {piece.kind}"
var directions: seq[Location] = @[]
# Only check in the right directions for the chosen piece
@ -963,7 +982,7 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] =
## Generates moves for the king in the given location
var
piece = self.grid[location.row, location.col]
doAssert piece.kind == King, &"generateKingMoves called on a {piece.kind}"
assert piece.kind == King, &"generateKingMoves called on a {piece.kind}"
var directions: seq[Location] = @[piece.color.topLeftDiagonal(),
piece.color.topRightDiagonal(),
piece.color.bottomRightDiagonal(),
@ -1008,7 +1027,7 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] =
## Generates moves for the knight in the given location
var
piece = self.grid[location.row, location.col]
doAssert piece.kind == Knight, &"generateKnightMoves called on a {piece.kind}"
assert piece.kind == Knight, &"generateKnightMoves called on a {piece.kind}"
var directions: seq[Location] = @[piece.color.bottomLeftKnightMove(),
piece.color.bottomRightKnightMove(),
piece.color.topLeftKnightMove(),
@ -1124,8 +1143,9 @@ proc getAttacks*(self: ChessBoard, loc: Location): Attacked =
proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] =
## Returns the first attack of the piece in the given
## source location that also attacks the target location
## Returns the first attack from the given source to the
## given target square
result = (emptyLocation(), emptyLocation(), emptyLocation())
let piece = self.grid[source.row, source.col]
case piece.color:
of Black:
@ -1158,6 +1178,10 @@ func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Locati
proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] =
## Returns all the directions along which the piece in the given
## location is pinned. If the result is non-empty, the piece at
## the given location is only allowed to move along the directions
## returned by this function
let piece = self.grid[loc.row, loc.col]
case piece.color:
of None:
@ -1176,8 +1200,8 @@ proc updatePawnAttacks(self: ChessBoard) =
## Internal helper of updateAttackedSquares
for loc in self.position.pieces.white.pawns:
# Pawns are special in how they capture (i.e. the
# squares they can move to do not match the squares
# they can capture on. Sneaky fucks)
# squares they can regularly move to do not match
# the squares they can capture on. Sneaky fucks)
self.addAttack((loc, loc + White.topRightDiagonal(), White.topRightDiagonal()), White)
self.addAttack((loc, loc + White.topLeftDiagonal(), White.topLeftDiagonal()), White)
# We do the same thing for black
@ -1193,11 +1217,20 @@ proc updateKingAttacks(self: ChessBoard) =
self.addAttack((king, king + White.topLeftDiagonal(), White.topLeftDiagonal()), White)
self.addAttack((king, king + White.bottomLeftDiagonal(), White.bottomLeftDiagonal()), White)
self.addAttack((king, king + White.bottomRightDiagonal(), White.bottomRightDiagonal()), White)
self.addAttack((king, king + White.leftSide(), White.leftSide()), White)
self.addAttack((king, king + White.rightSide(), White.rightSide()), White)
self.addAttack((king, king + White.bottomSide(), White.bottomSide()), White)
self.addAttack((king, king + White.topSide(), White.topSide()), White)
king = self.position.pieces.black.king
self.addAttack((king, king + Black.topRightDiagonal(), Black.topRightDiagonal()), Black)
self.addAttack((king, king + Black.topLeftDiagonal(), Black.topLeftDiagonal()), Black)
self.addAttack((king, king + Black.bottomLeftDiagonal(), Black.bottomLeftDiagonal()), Black)
self.addAttack((king, king + Black.bottomRightDiagonal(), Black.bottomRightDiagonal()), Black)
self.addAttack((king, king + Black.leftSide(), Black.leftSide()), Black)
self.addAttack((king, king + Black.rightSide(), Black.rightSide()), Black)
self.addAttack((king, king + Black.bottomSide(), Black.bottomSide()), Black)
self.addAttack((king, king + Black.topSide(), Black.topSide()), Black)
proc updateKnightAttacks(self: ChessBoard) =
@ -1284,7 +1317,7 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked
# we allow it to move two squares as well
if square.col == loc.col:
# The pawn can only push two squares if it's being pinned from the
# top
# top side (relative to the pawn itself)
result.pins.add((loc, square, otherPiece.color.doublePush()))
else:
break
@ -1300,7 +1333,6 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked
proc updateSlidingAttacks(self: ChessBoard) =
## Internal helper of updateAttackedSquares
var data: tuple[attacks: Attacked, pins: Attacked]
for loc in self.position.pieces.white.bishops:
data = self.getSlidingAttacks(loc)
@ -1841,6 +1873,7 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = fa
if not bulk:
if len(moves) == 0 and self.inCheck():
result.checkmates = 1
# TODO: Should we count stalemates?
if ply == 0:
result.nodes = 1
return