From d690600fa4389b14ff54b9b68061c671e7703628 Mon Sep 17 00:00:00 2001 From: nocturn9x Date: Sat, 28 Oct 2023 02:32:50 +0200 Subject: [PATCH] Massive improvement to move generation (bugs still exist) --- src/Chess/board.nim | 653 +++++++++++++++++++++++++++++--------------- 1 file changed, 428 insertions(+), 225 deletions(-) diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 4c1e857..8416061 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -60,8 +60,7 @@ type # Useful type aliases Location* = tuple[row, col: int8] - Attacked = seq[tuple[source, target: Location]] - Pinned = seq[tuple[source, target, direction: Location]] + Attacked = seq[tuple[source, target, direction: Location]] Pieces = tuple[king: Location, queens: seq[Location], rooks: seq[Location], bishops: seq[Location], knights: seq[Location], @@ -71,7 +70,6 @@ type Move* = object ## A chess move - piece*: Piece startSquare*: Location targetSquare*: Location flag*: MoveFlag @@ -98,12 +96,21 @@ type pieces: tuple[white: Pieces, black: Pieces] # Potential attacking moves for black and white attacked: tuple[white: Attacked, black: Attacked] - pinned: tuple[white: Pinned, black: Pinned] - # Has any piece been captured to reach this position? + pinned: tuple[white: Attacked, black: Attacked] + # The original piece captured to reach this position (may be empty) captured: Piece + # The piece that moved to reach this position (needed to undo moves) + moved: Piece # Active color turn: PieceColor + CacheEntry[T] = ref object + valid: bool + data: T + + Cache = object + canCastle: tuple[white, black: CacheEntry[tuple[queen, king: bool]]] + inCheck: tuple[white, black: CacheEntry[bool]] ChessBoard* = ref object ## A chess board object @@ -111,6 +118,9 @@ type position: Position # List of reached positions positions: seq[Position] + # Cached results of expensive + # functions in the current position + cache: Cache # Initialized only once, copied every time @@ -123,15 +133,15 @@ func emptyPiece*: Piece {.inline.} = Piece(kind: Empty, color: None) func emptyLocation*: Location {.inline.} = (-1 , -1) func opposite*(c: PieceColor): PieceColor {.inline.} = (if c == White: Black else: White) proc algebraicToLocation*(s: string): Location {.inline.} -proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} -proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} -func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation(), piece: emptyPiece()) +func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSquare: emptyLocation()) func `+`*(a, b: Location): Location = (a.row + b.row, a.col + b.col) func `-`*(a: Location): Location = (-a.row, -a.col) func `-`*(a, b: Location): Location = (a.row - b.row, a.col - b.col) func isValid*(a: Location): bool {.inline.} = a.row in 0..7 and a.col in 0..7 proc generateMoves(self: ChessBoard, location: Location): seq[Move] +proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location] +proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool proc undoMove*(self: ChessBoard, move: Move) proc isLegal(self: ChessBoard, move: Move): bool {.inline.} @@ -140,6 +150,9 @@ proc pretty*(self: ChessBoard): string proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) proc updateAttackedSquares(self: ChessBoard) proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] +proc getAttacks*(self: ChessBoard, loc: Location): Attacked +proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked] +func invalidateCache(self: ChessBoard) {.inline.} proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = @@ -220,18 +233,18 @@ func topRightKnightMove(color: PieceColor, long: bool = true): Location {.inline return (-1, 2) -func getActiveColor*(self: ChessBoard): PieceColor {.inline.} = +func getActiveColor*(self: ChessBoard): PieceColor {.inline.} = ## Returns the currently active color ## (turn of who has to move) return self.position.turn -func getEnPassantTarget*(self: ChessBoard): Location = +func getEnPassantTarget*(self: ChessBoard): Location {.inline.} = ## Returns the current en passant target square return self.position.enPassantSquare.targetSquare -func getMoveCount*(self: ChessBoard): int = +func getMoveCount*(self: ChessBoard): int {.inline.} = ## Returns the number of full moves that ## have been played return self.position.fullMoveCount @@ -281,6 +294,8 @@ proc newChessboard: ChessBoard = new(result) # Turns our flat sequence into an 8x8 grid result.grid = newMatrixFromSeq[Piece](empty, (8, 8)) + result.cache = Cache(canCastle: (white: CacheEntry[tuple[queen, king: bool]](), black: CacheEntry[tuple[queen, king: bool]]()), + inCheck: (white: CacheEntry[bool](), black: CacheEntry[bool]())) result.position = Position(attacked: (@[], @[]), enPassantSquare: emptyMove(), move: emptyMove(), @@ -419,10 +434,6 @@ proc newChessboardFromFEN*(state: string): ChessBoard = discard else: result.position.enPassantSquare.targetSquare = state[index..index+1].algebraicToLocation() - # Just for cleanliness purposes, we fill in the other metadata as - # well - result.position.enPassantSquare.piece.color = result.getActiveColor() - result.position.enPassantSquare.piece.kind = Pawn # Square metadata is 2 bytes long inc(index) of 4: @@ -449,15 +460,16 @@ proc newChessboardFromFEN*(state: string): ChessBoard = -proc newDefaultChessboard*: ChessBoard = +proc newDefaultChessboard*: ChessBoard {.inline.} = ## Initializes a chessboard with the ## starting position return newChessboardFromFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1") proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = - ## Counts the number of pieces with - ## the given color and type + ## Returns the number of pieces with + ## the given color and type in the given + ## position case color: of White: case kind: @@ -475,7 +487,7 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = # There shall be only one, forever return 1 else: - discard + raise newException(ValueError, "invalid piece type") of Black: case kind: of Pawn: @@ -492,18 +504,18 @@ proc countPieces*(self: ChessBoard, kind: PieceKind, color: PieceColor): int = # In perpetuity return 1 else: - discard + raise newException(ValueError, "invalid piece type") of None: - raise newException(ValueError, "invalid piece type") + raise newException(ValueError, "invalid piece color") -proc countPieces*(self: ChessBoard, piece: Piece): int = +func countPieces*(self: ChessBoard, piece: Piece): int {.inline.} = ## Returns the number of pieces on the board that - ## are of the same type and color of the given piece + ## are of the same type and color as the given piece return self.countPieces(piece.kind, piece.color) -func rankToColumn(rank: int): int8 = +func rankToColumn(rank: int): int8 {.inline.} = ## Converts a chess rank (1-indexed) ## into a 0-indexed column value for our ## board. This converter is necessary because @@ -513,7 +525,9 @@ func rankToColumn(rank: int): int8 = return indeces[rank - 1] -func rowToRank(row: int): int = +func rowToRank(row: int): int {.inline.} = + ## Converts a row into our grid into + ## a chess rank const indeces = [8, 7, 6, 5, 4, 3, 2, 1] return indeces[row] @@ -543,23 +557,17 @@ func locationToAlgebraic*(loc: Location): string {.inline.} = return &"{char(uint8(loc.col) + uint8('a'))}{rowToRank(loc.row)}" -func getPiece*(self: ChessBoard, loc: Location): Piece = +func getPiece*(self: ChessBoard, loc: Location): Piece {.inline.} = ## Gets the piece at the given location return self.grid[loc.row, loc.col] -func getPiece*(self: ChessBoard, square: string): Piece = +func getPiece*(self: ChessBoard, square: string): Piece {.inline.} = ## Gets the piece on the given square ## in algebraic notation return self.getPiece(square.algebraicToLocation()) -func isCapture*(move: Move): bool {.inline.} = - ## Returns whether the given move is a capture - ## or not - return move.flag in [Capture, EnPassant] - - func isPromotion*(move: Move): bool {.inline.} = ## Returns whrther the given move is a ## pawn promotion or not @@ -576,9 +584,23 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = color = self.getActiveColor() case color: of White: - return self.isAttacked(self.position.pieces.white.king, Black) + if self.cache.inCheck.white.valid: + return self.cache.inCheck.white.data + result = self.isAttacked(self.position.pieces.white.king, Black) of Black: - return self.isAttacked(self.position.pieces.black.king, White) + if self.cache.inCheck.black.valid: + return self.cache.inCheck.black.data + result = self.isAttacked(self.position.pieces.black.king, White) + else: + # Unreachable + discard + case color: + of White: + self.cache.inCheck.white.valid = true + self.cache.inCheck.white.data = result + of Black: + self.cache.inCheck.black.valid = true + self.cache.inCheck.black.data = result else: # Unreachable discard @@ -586,7 +608,7 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: bool] {.inline.} = ## Returns the sides on which castling is allowed - ## for the given color. If the color is Empty, the + ## for the given color. If the color is None, the ## currently active color is used var color = color if color == None: @@ -595,9 +617,13 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: # rights have been lost case color: of White: + if self.cache.canCastle.white.valid: + return self.cache.canCastle.white.data result.king = self.position.castlingAvailable.white.king result.queen = self.position.castlingAvailable.white.queen of Black: + if self.cache.canCastle.black.valid: + return self.cache.canCastle.black.data result.king = self.position.castlingAvailable.black.king result.queen = self.position.castlingAvailable.black.queen of None: @@ -636,6 +662,7 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: location = location + kingSide if not location.isValid(): break + otherPiece = self.grid[location.row, location.col] if otherPiece.color == None: @@ -645,6 +672,7 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: result.king = false break if location == color.kingSideRook(): + result.king = self.grid[location.row, location.col].kind == Rook break if result.queen: @@ -662,10 +690,59 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: continue if otherPiece.color == color.opposite() or otherPiece.kind != Rook or self.isAttacked(location, color.opposite()): - result.king = false + result.queen = false break + if location == color.queenSideRook(): + result.queen = self.grid[location.row, location.col].kind == Rook break + case color: + of White: + self.cache.canCastle.white.data = result + self.cache.canCastle.white.valid = true + of Black: + self.cache.canCastle.black.data = result + self.cache.canCastle.white.valid = true + else: + discard + + +proc getCheckResolutions(self: ChessBoard, color: PieceColor): seq[Location] = + ## Returns the squares that need to be covered to + ## resolve the current check (includes capturing + ## the checking piece). In case of double check, an + ## empty list is returned (as the king must move) + var king: Location + case color: + of White: + king = self.position.pieces.white.king + of Black: + king = self.position.pieces.black.king + else: + return @[] + + let attackers: seq[Location] = self.getAttackers(king, color.opposite()) + if attackers.len() > 1: + # Double checks require to move the king + return @[] + let + attacker = attackers[0] + attackerPiece = self.grid[attacker.row, attacker.col] + 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) + if attackerPiece.kind notin [Knight, Pawn]: + var location = attacker + while location != king: + location = location + attack.direction + if not location.isValid(): + break + result.add(location) + proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = @@ -689,45 +766,45 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None: locations.add(double) flags.add(DoublePush) - # If the target is the en passant square and the piece under en passant - # is of the opposite color of the pawn being moved, then we check if we - # can do an en passant capture - if self.position.enPassantSquare.piece.color == piece.color.opposite: - if abs(self.position.enPassantSquare.targetSquare.col - location.col) == 1 and abs(self.position.enPassantSquare.targetSquare.row - location.row) == 1: - # Only viable if the piece is on the front diagonal of the target - locations.add(self.position.enPassantSquare.targetSquare) - flags.add(EnPassant) # They can also move on either diagonal one - # square, but only to capture - var diagonal = location + piece.color.topRightDiagonal() - if diagonal.isValid() and self.grid[diagonal.row, diagonal.col].color == piece.color.opposite() and self.grid[diagonal.row, diagonal.col].kind != King: - locations.add(diagonal) - flags.add(Capture) - diagonal = location + piece.color.topLeftDiagonal() - if diagonal.isValid() and self.grid[diagonal.row, diagonal.col].color == piece.color.opposite() and self.grid[diagonal.row, diagonal.col].kind != King: - locations.add(diagonal) - flags.add(Capture) + # square, but only to capture or for en passant + for diagonal in [location + piece.color.topRightDiagonal(), location + piece.color.topLeftDiagonal()]: + if diagonal.isValid(): + if diagonal == self.position.enPassantSquare.targetSquare: + locations.add(diagonal) + flags.add(EnPassant) + elif self.grid[diagonal.row, diagonal.col].color == piece.color.opposite() and self.grid[diagonal.row, diagonal.col].kind != King: + locations.add(diagonal) + flags.add(Capture) var newLocation: Location + newFlags: seq[MoveFlag] newLocations: seq[Location] + # Check for pins let pins = self.getPinnedDirections(location) for pin in pins: newLocation = location + pin - # Pin direction is legal - if newLocation in locations: + let loc = locations.find(newLocation) + if loc != -1: + # Pin direction is legal for this piece newLocations.add(newLocation) + newFlags.add(flags[loc]) if pins.len() > 0: locations = newLocations - + flags = newFlags + let checked = self.inCheck() + let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) var targetPiece: Piece for (target, flag) in zip(locations, flags): + if checked and target notin resolutions: + continue targetPiece = self.grid[target.row, target.col] if target.row == piece.color.getLastRow(): # Pawn reached the other side of the board: generate all potential piece promotions for promotionType in [PromoteToKnight, PromoteToBishop, PromoteToRook, PromoteToQueen]: - result.add(Move(startSquare: location, targetSquare: target, piece: piece, flag: promotionType)) + result.add(Move(startSquare: location, targetSquare: target, flag: promotionType)) continue - result.add(Move(startSquare: location, targetSquare: target, piece: piece, flag: flag)) + result.add(Move(startSquare: location, targetSquare: target, flag: flag)) proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = @@ -749,9 +826,9 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = directions.add(piece.color.leftSide()) let pinned = self.getPinnedDirections(location) if pinned.len() > 0: - # If a sliding piece is pinned then it can only - # move along the pinning direction(s) directions = pinned + let checked = self.inCheck() + let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) for direction in directions: # Slide in this direction as long as it's possible var @@ -766,15 +843,17 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = # A friendly piece is in the way if otherPiece.color == piece.color: break + if checked and square notin resolutions: + continue if otherPiece.color == piece.color.opposite: # Target square contains an enemy piece: capture # it and stop going any further if otherPiece.kind != King: # Can't capture the king - result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) + result.add(Move(startSquare: location, targetSquare: square, flag: Capture)) break # Target square is empty - result.add(Move(startSquare: location, targetSquare: square, piece: piece)) + result.add(Move(startSquare: location, targetSquare: square)) proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = @@ -803,6 +882,8 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = # End of board reached if not square.isValid(): continue + if self.isAttacked(square, piece.color.opposite()): + continue if direction == longCastleKing(): flag = CastleLong elif direction == shortCastleKing(): @@ -812,13 +893,12 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = let otherPiece = self.grid[square.row, square.col] if otherPiece.color == self.getActiveColor.opposite(): flag = Capture - # A friendly piece or the opponent king is in the way, - # move onto the next direction - if otherPiece.color == piece.color or otherPiece.kind == King: + # A friendly piece is in the way, move onto the next direction + if otherPiece.color == piece.color: continue # Target square is empty or contains an enemy piece: # All good for us! - result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: flag)) + result.add(Move(startSquare: location, targetSquare: square, flag: flag)) proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = @@ -838,7 +918,8 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = if pinned.len() > 0: # Knight is pinned: can't move! return @[] - + let checked = self.inCheck() + let resolutions = if not checked: @[] else: self.getCheckResolutions(piece.color) for direction in directions: # Jump to this square let square: Location = location + direction @@ -849,13 +930,16 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = # A friendly piece or the opponent king is is in the way if otherPiece.color == piece.color or otherPiece.kind == King: continue + + if checked and square notin resolutions: + continue if otherPiece.color != None: # Target square contains an enemy piece: capture # it - result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) + result.add(Move(startSquare: location, targetSquare: square, flag: Capture)) else: # Target square is empty - result.add(Move(startSquare: location, targetSquare: square, piece: piece)) + result.add(Move(startSquare: location, targetSquare: square)) proc generateMoves(self: ChessBoard, location: Location): seq[Move] = @@ -883,31 +967,8 @@ proc generateAllMoves*(self: ChessBoard): seq[Move] = if self.grid[i, j].color == self.getActiveColor(): for move in self.generateMoves((int8(i), int8(j))): result.add(move) + - -proc getAttackers*(self: ChessBoard, square: Location, color = None): seq[Location] = - ## Returns all the attackers of the given square - ## for the given color (a color of None means use - ## the currently active color) - var color = color - if color == None: - color = self.getActiveColor() - case color: - of White: - for attack in self.position.attacked.black: - if attack.target == square: - result.add(attack.source) - of Black: - for attack in self.position.attacked.white: - if attack.target == square: - result.add(attack.source) - else: - # Unreachable - discard - -# We don't use getAttackers because this one only cares about whether -# the square is attacked or not (and can therefore exit earlier than -# getAttackers) proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool = ## Returns whether the given location is attacked ## by the given color @@ -927,13 +988,63 @@ proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): boo discard +proc getAttackers*(self: ChessBoard, loc: Location, color: PieceColor): seq[Location] = + ## Returns all the attackers of the given color + ## for the given square + case color: + of Black: + for attack in self.position.attacked.black: + if attack.target == loc: + result.add(attack.source) + of White: + for attack in self.position.attacked.white: + if attack.target == loc: + result.add(attack.source) + of None: + discard + + +proc getAttacks*(self: ChessBoard, loc: Location): Attacked = + ## Returns all the squares attacked by the piece in the given + ## location + let piece = self.grid[loc.row, loc.col] + case piece.color: + of Black: + for attack in self.position.attacked.black: + if attack.source == loc: + result.add(attack) + of White: + for attack in self.position.attacked.white: + if attack.source == loc: + result.add(attack) + of None: + discard + + +proc getAttackFor*(self: ChessBoard, source, target: Location): tuple[source, target, direction: Location] = + ## Returns the first attacks of the piece in the given + ## source location that also attacks the target location + let piece = self.grid[source.row, source.col] + case piece.color: + of Black: + for attack in self.position.attacked.black: + if attack.target == target and attack.source == source: + return attack + of White: + for attack in self.position.attacked.white: + if attack.target == target and attack.source == source: + return attack + of None: + discard + + proc isAttacked*(self: ChessBoard, square: string): bool = ## Returns whether the given square is attacked ## by the current return self.isAttacked(square.algebraicToLocation()) -func addAttack(self: ChessBoard, attack: tuple[source, target: Location], color: PieceColor) {.inline.} = +func addAttack(self: ChessBoard, attack: tuple[source, target, direction: Location], color: PieceColor) {.inline.} = if attack.source.isValid() and attack.target.isValid(): case color: of White: @@ -959,58 +1070,58 @@ proc getPinnedDirections(self: ChessBoard, loc: Location): seq[Location] = result.add(pin.direction) -proc updatePawnAttacks(self: ChessBoard) {.thread.} = +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) - self.addAttack((loc, loc + White.topRightDiagonal()), White) - self.addAttack((loc, loc + White.topLeftDiagonal()), White) + self.addAttack((loc, loc + White.topRightDiagonal(), White.topRightDiagonal()), White) + self.addAttack((loc, loc + White.topLeftDiagonal(), White.topRightDiagonal()), White) # We do the same thing for black for loc in self.position.pieces.black.pawns: - self.addAttack((loc, loc + Black.topRightDiagonal()), Black) - self.addAttack((loc, loc + Black.topLeftDiagonal()), Black) + self.addAttack((loc, loc + Black.topRightDiagonal(), Black.topRightDiagonal()), Black) + self.addAttack((loc, loc + Black.topLeftDiagonal(), Black.topRightDiagonal()), Black) proc updateKingAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares var king = self.position.pieces.white.king - self.addAttack((king, king + White.topRightDiagonal()), White) - self.addAttack((king, king + White.topLeftDiagonal()), White) - self.addAttack((king, king + White.bottomLeftDiagonal()), White) - self.addAttack((king, king + White.bottomRightDiagonal()), White) + self.addAttack((king, king + White.topRightDiagonal(), White.topRightDiagonal()), White) + 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) king = self.position.pieces.black.king - self.addAttack((king, king + Black.topRightDiagonal()), Black) - self.addAttack((king, king + Black.topLeftDiagonal()), Black) - self.addAttack((king, king + Black.bottomLeftDiagonal()), Black) - self.addAttack((king, king + Black.bottomRightDiagonal()), Black) + 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) proc updateKnightAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares for loc in self.position.pieces.white.knights: - self.addAttack((loc, loc + White.topLeftKnightMove()), White) - self.addAttack((loc, loc + White.topRightKnightMove()), White) - self.addAttack((loc, loc + White.bottomLeftKnightMove()), White) - self.addAttack((loc, loc + White.bottomRightKnightMove()), White) - self.addAttack((loc, loc + White.topLeftKnightMove(long=false)), White) - self.addAttack((loc, loc + White.topRightKnightMove(long=false)), White) - self.addAttack((loc, loc + White.bottomLeftKnightMove(long=false)), White) - self.addAttack((loc, loc + White.bottomRightKnightMove(long=false)), White) + self.addAttack((loc, loc + White.topLeftKnightMove(), White.topLeftKnightMove()), White) + self.addAttack((loc, loc + White.topRightKnightMove(), White.topRightKnightMove()), White) + self.addAttack((loc, loc + White.bottomLeftKnightMove(), White.bottomLeftKnightMove()), White) + self.addAttack((loc, loc + White.bottomRightKnightMove(), White.bottomRightKnightMove()), White) + self.addAttack((loc, loc + White.topLeftKnightMove(long=false), White.topLeftKnightMove(long=false)), White) + self.addAttack((loc, loc + White.topRightKnightMove(long=false), White.topRightKnightMove(long=false)), White) + self.addAttack((loc, loc + White.bottomLeftKnightMove(long=false), White.bottomLeftKnightMove(long=false)), White) + self.addAttack((loc, loc + White.bottomRightKnightMove(long=false), White.bottomRightKnightMove(long=false)), White) for loc in self.position.pieces.black.knights: - self.addAttack((loc, loc + Black.topLeftKnightMove()), Black) - self.addAttack((loc, loc + Black.topRightKnightMove()), Black) - self.addAttack((loc, loc + Black.bottomLeftKnightMove()), Black) - self.addAttack((loc, loc + Black.bottomRightKnightMove()), Black) - self.addAttack((loc, loc + Black.topLeftKnightMove(long=false)), Black) - self.addAttack((loc, loc + Black.topRightKnightMove(long=false)), Black) - self.addAttack((loc, loc + Black.bottomLeftKnightMove(long=false)), Black) - self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false)), Black) + self.addAttack((loc, loc + Black.topLeftKnightMove(), Black.topLeftKnightMove()), Black) + self.addAttack((loc, loc + Black.topRightKnightMove(), Black.topRightKnightMove()), Black) + self.addAttack((loc, loc + Black.bottomLeftKnightMove(), Black.bottomLeftKnightMove()), Black) + self.addAttack((loc, loc + Black.bottomRightKnightMove(), Black.bottomRightKnightMove()), Black) + self.addAttack((loc, loc + Black.topLeftKnightMove(long=false), Black.topLeftKnightMove(long=false)), Black) + self.addAttack((loc, loc + Black.topRightKnightMove(long=false), Black.topRightKnightMove(long=false)), Black) + self.addAttack((loc, loc + Black.bottomLeftKnightMove(long=false), Black.bottomLeftKnightMove(long=false)), Black) + self.addAttack((loc, loc + Black.bottomRightKnightMove(long=false), Black.bottomRightKnightMove(long=false)), Black) -proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Pinned] = +proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked, pins: Attacked] = ## Internal helper of updateSlidingAttacks var directions: seq[Location] = @[] @@ -1041,7 +1152,7 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked # Target square is attacked (even if a friendly piece # is present, because in this case we're defending # it) - result.attacks.add((loc, square)) + result.attacks.add((loc, square, direction)) # Empty square, keep going if otherPiece.color == None: continue @@ -1066,14 +1177,14 @@ proc getSlidingAttacks(self: ChessBoard, loc: Location): tuple[attacks: Attacked # minus sign: up for us is down for them and vice versa) result.pins.add((loc, square, -direction)) else: - break + break break proc updateSlidingAttacks(self: ChessBoard) = ## Internal helper of updateAttackedSquares - var data: tuple[attacks: Attacked, pins: Pinned] + var data: tuple[attacks: Attacked, pins: Attacked] for loc in self.position.pieces.white.bishops: data = self.getSlidingAttacks(loc) self.position.attacked.white.extend(data.attacks) @@ -1114,6 +1225,9 @@ proc updateAttackedSquares(self: ChessBoard) = self.updateKnightAttacks() # Kings self.updateKingAttacks() + # Invalidate the cache whenever updates to the + # metadata are made + self.invalidateCache() proc removePiece(self: ChessBoard, location: Location, attack: bool = true) = @@ -1165,9 +1279,10 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = ## is set to false, then this function does ## not update attacked squares metadata, just ## positional info and the grid itself - case move.piece.color: + let piece = self.grid[move.startSquare.row, move.startSquare.col] + case piece.color: of White: - case move.piece.kind: + case piece.kind: of Pawn: # The way things are structured, we don't care about the order # of this list, so we can add and remove entries as we please @@ -1190,7 +1305,7 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = else: discard of Black: - case move.piece.kind: + case piece.kind: of Pawn: self.position.pieces.black.pawns.delete(self.position.pieces.black.pawns.find(move.startSquare)) self.position.pieces.black.pawns.add(move.targetSquare) @@ -1215,24 +1330,23 @@ proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = # Empty out the starting square self.grid[move.startSquare.row, move.startSquare.col] = emptyPiece() # Actually move the piece - self.grid[move.targetSquare.row, move.targetSquare.col] = move.piece + self.grid[move.targetSquare.row, move.targetSquare.col] = piece if attack: self.updateAttackedSquares() + else: + # Just to be sure + self.invalidateCache() proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bool = true) = ## Like the other movePiece(), but with two locations - self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare, - piece: self.grid[startSquare.row, startSquare.col], - ), - attack - ) + self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack) proc updateLocations(self: ChessBoard, move: Move) = ## Internal helper to update the position of ## the pieces on the board after a move - if move.isCapture(): + if move.flag == Capture: self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col] self.removePiece(move.targetSquare, attack=false) @@ -1240,6 +1354,14 @@ proc updateLocations(self: ChessBoard, move: Move) = self.movePiece(move) +func invalidateCache(self: ChessBoard) {.inline.} = + ## Invalidates the internal caches + self.cache.canCastle.white.valid = false + self.cache.canCastle.black.valid = false + self.cache.inCheck.white.valid = false + self.cache.inCheck.black.valid = false + + proc doMove(self: ChessBoard, move: Move) = ## Internal function called by makeMove after ## performing legality checks on the given move. Can @@ -1250,23 +1372,25 @@ proc doMove(self: ChessBoard, move: Move) = # Record the final move in the position self.position.move = move + let piece = self.grid[move.startSquare.row, move.startSquare.col] + self.position.moved = piece # Needed to detect draw by the 50 move rule var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable - if move.piece.kind == Pawn or move.isCapture(): - halfMoveClock = 0 + if piece.kind == Pawn or move.flag == Capture: + self.position.halfMoveClock = 0 else: inc(halfMoveClock) - if move.piece.color == Black: + if piece.color == Black: inc(fullMoveCount) # Castling check: have the rooks moved? - if move.piece.kind == Rook: - case move.piece.color: + if piece.kind == Rook: + case piece.color: of White: - if move.startSquare.row == move.piece.getStartRow(): + if move.startSquare.row == piece.getStartRow(): if move.startSquare.col == 0: # Queen side castlingAvailable.white.queen = false @@ -1274,7 +1398,7 @@ proc doMove(self: ChessBoard, move: Move) = # King side castlingAvailable.white.king = false of Black: - if move.startSquare.row == move.piece.getStartRow(): + if move.startSquare.row == piece.getStartRow(): if move.startSquare.col == 0: # Queen side castlingAvailable.black.queen = false @@ -1284,7 +1408,7 @@ proc doMove(self: ChessBoard, move: Move) = else: discard # Has a rook been captured? - if move.isCapture(): + if move.flag == Capture: let piece = self.grid[move.targetSquare.row, move.targetSquare.col] if piece.kind == Rook: case piece.color: @@ -1306,8 +1430,9 @@ proc doMove(self: ChessBoard, move: Move) = # Unreachable discard # Has the king moved? - if move.piece.kind == King: - case move.piece.color: + if piece.kind == King or move.flag in [CastleLong, CastleShort]: + # Revoke all castling rights for the moving king + case piece.color: of White: castlingAvailable.white.king = false castlingAvailable.white.queen = false @@ -1316,21 +1441,6 @@ proc doMove(self: ChessBoard, move: Move) = castlingAvailable.black.queen = false else: discard - if move.isPromotion(): - # Move is a pawn promotion: get rid of then pawn - # and spawn a new piece - self.removePiece(move.startSquare) - case move.flag: - of PromoteToBishop: - self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: move.piece.color)) - of PromoteToKnight: - self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: move.piece.color)) - of PromoteToRook: - self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: move.piece.color)) - of PromoteToQueen: - self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: move.piece.color)) - else: - discard let previous = self.position # Record final position for future reference self.positions.add(previous) @@ -1345,42 +1455,47 @@ proc doMove(self: ChessBoard, move: Move) = move: emptyMove(), pieces: previous.pieces, ) - - var location: Location - if move.flag in [CastleShort, CastleLong]: - # Revoke all castling rights for the moving king - case move.piece.color: - of White: - self.position.castlingAvailable.white.king = false - self.position.castlingAvailable.white.queen = false - of Black: - self.position.castlingAvailable.black.king = false - self.position.castlingAvailable.black.queen = false + if move.isPromotion(): + # Move is a pawn promotion: get rid of the pawn + # and spawn a new piece + self.removePiece(move.startSquare) + case move.flag: + of PromoteToBishop: + self.spawnPiece(move.targetSquare, Piece(kind: Bishop, color: piece.color)) + of PromoteToKnight: + self.spawnPiece(move.targetSquare, Piece(kind: Knight, color: piece.color)) + of PromoteToRook: + self.spawnPiece(move.targetSquare, Piece(kind: Rook, color: piece.color)) + of PromoteToQueen: + self.spawnPiece(move.targetSquare, Piece(kind: Queen, color: piece.color)) else: discard + if move.flag in [CastleShort, CastleLong]: # Move the rook onto the # correct file var location: Location target: Location if move.flag == CastleShort: - location = move.piece.color.kingSideRook() + location = piece.color.kingSideRook() target = shortCastleRook() else: - location = move.piece.color.queenSideRook() + location = piece.color.queenSideRook() target = longCastleRook() let rook = self.grid[location.row, location.col] - let move = Move(startSquare: location, targetSquare: location + target, piece: rook, flag: move.flag) + let move = Move(startSquare: location, targetSquare: location + target, flag: move.flag) self.movePiece(move, attack=false) + if move.flag == EnPassant: + self.removePiece(move.targetSquare + piece.color.bottomSide()) + # Update position and attack metadata self.updateLocations(move) # Check for double pawn push if move.flag == DoublePush: - self.position.enPassantSquare = Move(piece: move.piece, - startSquare: (move.startSquare.row, move.startSquare.col), - targetSquare: move.targetSquare + move.piece.color.bottomSide()) + self.position.enPassantSquare = Move(startSquare: (move.startSquare.row, move.startSquare.col), + targetSquare: move.targetSquare + piece.color.bottomSide()) else: self.position.enPassantSquare = emptyMove() @@ -1465,15 +1580,17 @@ proc undoMove*(self: ChessBoard, move: Move) = ## Undoes the given move if self.positions.len() == 0: return + self.invalidateCache() let previous = self.position var position = self.positions[^1] while position.move != move: discard self.positions.pop() position = self.positions[^1] + self.position = position - self.grid[move.startSquare.row, move.startSquare.col] = move.piece - if move.isCapture(): + self.grid[move.startSquare.row, move.startSquare.col] = self.position.moved + if move.flag == Capture: self.grid[move.targetSquare.row, move.targetSquare.col] = previous.captured else: self.grid[move.targetSquare.row, move.targetSquare.col] = emptyPiece() @@ -1482,16 +1599,19 @@ proc undoMove*(self: ChessBoard, move: Move) = # list) if move.flag == CastleLong: let - rookOld = move.targetSquare + move.piece.color.rightSide() - rookNew = move.piece.color.queenSideRook() + rookOld = move.targetSquare + (if self.getActiveColor() == White: self.getActiveColor().rightSide() else: self.getActiveColor().leftSide()) + rookNew = self.getActiveColor().queenSideRook() self.grid[rookNew.row, rookNew.col] = self.grid[rookOld.row, rookOld.col] self.grid[rookOld.row, rookOld.col] = emptyPiece() if move.flag == CastleShort: let - rookOld = move.targetSquare + move.piece.color.leftSide() - rookNew = move.piece.color.kingSideRook() + rookOld = move.targetSquare + (if self.getActiveColor() == White: self.getActiveColor().leftSide() else:self.getActiveColor().rightSide()) + rookNew = self.getActiveColor().kingSideRook() self.grid[rookNew.row, rookNew.col] = self.grid[rookOld.row, rookOld.col] self.grid[rookOld.row, rookOld.col] = emptyPiece() + if move.flag == EnPassant: + let target = self.getEnPassantTarget() + self.getActiveColor().topSide() + self.grid[target.row, target.col] = Piece(kind: Pawn, color: self.getActiveColor().opposite()) proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = @@ -1500,7 +1620,7 @@ proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = - ## Like the other makeMove(), but with a Move object + ## Makes a move on the board result = move self.position.move = move if not self.isLegal(move): @@ -1509,27 +1629,6 @@ proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = result = self.position.move -proc makeMove*(self: ChessBoard, startSquare, targetSquare: string): Move {.discardable.} = - ## Makes a move on the board from the chosen start square to - ## the chosen target square, ensuring it is legal (turns are - ## taken into account!). This function returns a Move object: if the move - ## is legal and has been performed, the fields will be populated properly. - ## For efficiency purposes, no exceptions are raised if the move is - ## illegal, but the move's piece kind will be Empty (its color will be None - ## too) and the locations will both be set to the tuple (-1, -1) - var - startLocation = startSquare.algebraicToLocation() - targetLocation = targetSquare.algebraicToLocation() - result = Move(startSquare: startLocation, targetSquare: targetLocation, piece: self.grid[startLocation.row, startLocation.col]) - return self.makeMove(result) - - -proc makeMove*(self: ChessBoard, startSquare, targetSquare: Location): Move {.discardable.} = - ## Like the other makeMove(), but with two locations - result = Move(startSquare: startSquare, targetSquare: targetSquare, piece: self.grid[startSquare.row, startSquare.col]) - return self.makeMove(result) - - proc `$`*(self: ChessBoard): string = result &= "- - - - - - - -" for i, row in self.grid: @@ -1547,6 +1646,12 @@ proc `$`*(self: ChessBoard): string = result &= "\na b c d e f g h" +proc toChar*(piece: Piece): char = + if piece.color == White: + return char(piece.kind).toUpperAscii() + return char(piece.kind) + + proc pretty*(self: ChessBoard): string = ## Returns a colorized version of the ## board for easier visualization @@ -1575,7 +1680,97 @@ proc pretty*(self: ChessBoard): string = result &= "\x1b[0m" -proc perft*(self: ChessBoard, ply: int, verbose: bool = false): CountData = + +proc toFEN*(self: ChessBoard): string = + ## Returns a FEN string of the current + ## position in the chessboard + var skip: int + # Piece placement data + for i, row in self.grid: + skip = 0 + for j, piece in row: + if piece.kind == Empty: + inc(skip) + elif skip > 0: + result &= &"{skip}{piece.toChar()}" + skip = 0 + else: + result &= piece.toChar() + if skip > 0: + result &= $skip + if i < 7: + result &= "/" + result &= " " + # Active color + result &= (if self.getActiveColor() == White: "w" else: "b") + result &= " " + # Castling availability + let castleWhite = self.position.castlingAvailable.white + let castleBlack = self.position.castlingAvailable.black + if not (castleBlack.king or castleBlack.queen or castleWhite.king or castleWhite.queen): + result &= "-" + else: + if castleWhite.king: + result &= "K" + if castleWhite.queen: + result &= "Q" + if castleBlack.king: + result &= "k" + if castleBlack.queen: + result &= "q" + result &= " " + # En passant target + if self.getEnPassantTarget() == emptyLocation(): + result &= "-" + else: + result &= self.getEnPassantTarget().locationToAlgebraic() + result &= " " + # Halfmove clock + result &= $self.getHalfMoveCount() + result &= " " + # Fullmove number + result &= $self.getMoveCount() + + +proc perftBulkCount*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false): int = + ## Version of perft that implements bulk-counting. + ## Only the total number of nodes reached after the + ## given number of ply is returned + let moves = self.generateAllMoves() + if ply == 1: + return len(moves) + for move in moves: + if verbose: + let canCastle = self.canCastle(self.getActiveColor()) + echo &"Ply: {self.position.plyFromRoot}" + echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})" + echo &"Turn: {self.getActiveColor()}" + echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}" + echo &"Flag: {move.flag}" + echo &"In check: {(if self.inCheck(self.getActiveColor()): \"yes\" else: \"no\")}" + echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" + echo &"Before: {self.toFEN()}\n" + echo self.pretty() + self.doMove(move) + if verbose: + echo &"Now: {self.toFEN()}\n" + echo self.pretty() + try: + discard readLine(stdin) + except IOError: + discard + except EOFError: + discard + let next = self.perftBulkCount(ply - 1, verbose) + result += next + if divide: + echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}: {next}" + if verbose: + echo "" + self.undoMove(move) + + +proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false): CountData = ## Counts (and debugs) the number of legal positions reached after ## the given number of ply var verbose = verbose @@ -1587,23 +1782,21 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false): CountData = inc(result.checkmates) for move in moves: if verbose: - let canCastle = self.canCastle(move.piece.color) - #echo "\x1Bc" + let canCastle = self.canCastle(self.getActiveColor()) echo &"Ply: {self.position.plyFromRoot}" echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}, from ({move.startSquare.row}, {move.startSquare.col}) to ({move.targetSquare.row}, {move.targetSquare.col})" - echo &"Turn: {move.piece.color}" - echo &"Piece: {move.piece.kind}" + echo &"Turn: {self.getActiveColor()}" + echo &"Piece: {self.grid[move.startSquare.row, move.startSquare.col].kind}" echo &"Flag: {move.flag}" echo &"In check: {(if self.inCheck(): \"yes\" else: \"no\")}" echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" - echo "Before:\n" - echo self.pretty() - try: - discard readLine(stdin) - except IOError: - discard - except EOFError: - discard + echo &"Position before move: {self.toFEN()}" + stdout.write("En Passant target: ") + if self.getEnPassantTarget() != emptyLocation(): + echo self.getEnPassantTarget().locationToAlgebraic() + else: + echo "None" + echo "\n", self.pretty() self.doMove(move) case move.flag: of Capture: @@ -1620,8 +1813,13 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false): CountData = # Opponent king is in check inc(result.checks) if verbose: - echo "Now:\n" - echo self.pretty() + let canCastle = self.canCastle(self.getActiveColor()) + echo "\n" + echo &"Opponent in check: {(if self.inCheck(): \"yes\" else: \"no\")}" + echo &"Opponent can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" + echo &"Position after move: {self.toFEN()}" + echo "\n", self.pretty() + stdout.write(">>> ") try: discard readLine(stdin) except IOError: @@ -1629,6 +1827,10 @@ proc perft*(self: ChessBoard, ply: int, verbose: bool = false): CountData = except EOFError: discard let next = self.perft(ply - 1, verbose) + if divide: + echo &"{move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()}: {next.nodes}" + if verbose: + echo "" result.nodes += next.nodes result.captures += next.captures result.checks += next.checks @@ -1697,6 +1899,7 @@ when isMainModule: when compileOption("profiler"): import nimprof - # b = newChessboardFromFEN("fen") - echo b.perft(4, verbose=true) + b = newChessboardFromFEN("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - ") + #echo b.perftBulkCount(4, divide=true) + echo b.perft(2, verbose=false, divide=true) echo "All tests were successful" \ No newline at end of file