diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 500c0a0..1e55d21 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -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