diff --git a/src/Chess/board.nim b/src/Chess/board.nim index 7fa3427..db61f9c 100644 --- a/src/Chess/board.nim +++ b/src/Chess/board.nim @@ -122,7 +122,6 @@ 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.} -func getCapture*(self: ChessBoard, move: Move): Location 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.} @@ -130,11 +129,10 @@ func emptyMove*: Move {.inline.} = Move(startSquare: emptyLocation(), targetSqua 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 isAttacked*(self: ChessBoard, loc: Location): bool +proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool proc undoMove*(self: ChessBoard, move: Move) -proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool +proc isLegal(self: ChessBoard, move: Move): bool {.inline.} proc doMove(self: ChessBoard, move: Move) -proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool proc pretty*(self: ChessBoard): string proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) proc updateAttackedSquares(self: ChessBoard) @@ -535,18 +533,16 @@ func getPiece*(self: ChessBoard, square: string): Piece = -func isCapture*(self: ChessBoard, move: Move): bool {.inline.} = +func isCapture*(move: Move): bool {.inline.} = ## Returns whether the given move is a capture ## or not return move.flag in [Capture, EnPassant] -func getCapture*(self: ChessBoard, move: Move): Location = - ## Returns the location that would be captured if this - ## move were played on the board - if not self.isCapture(move): - return emptyLocation() - return move.targetSquare +func isPromotion*(move: Move): bool {.inline.} = + ## Returns whrther the given move is a + ## pawn promotion or not + return move.flag in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen] proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = @@ -559,9 +555,9 @@ proc inCheck*(self: ChessBoard, color: PieceColor = None): bool = color = self.getActiveColor() case color: of White: - return self.isAttacked(self.position.pieces.white.king) + return self.isAttacked(self.position.pieces.white.king, color) of Black: - return self.isAttacked(self.position.pieces.black.king) + return self.isAttacked(self.position.pieces.black.king, color) else: # Unreachable discard @@ -586,65 +582,67 @@ proc canCastle*(self: ChessBoard, color: PieceColor = None): tuple[queen, king: of None: # Unreachable discard - var - loc: Location - queenSide: Location - kingSide: Location - # If the path between the king and a rook is blocked, then castling - # is prohibited on that side - case color: - of White: - loc = self.position.pieces.white.king - queenSide = color.leftSide() - kingSide = color.rightSide() - of Black: - loc = self.position.pieces.black.king - queenSide = color.rightSide() - kingSide = color.leftSide() - of None: - # Unreachable - discard + if result.king or result.queen: + var + loc: Location + queenSide: Location + kingSide: Location + if self.inCheck(color): + # King can not castle out of check + return (false, false) + # If the path between the king and rook on a given side is blocked or any of the + # squares where the king would travel to are attacked by the opponent, + # then castling is (temporarily) prohibited on that side + case color: + of White: + loc = self.position.pieces.white.king + queenSide = color.leftSide() + kingSide = color.rightSide() + of Black: + loc = self.position.pieces.black.king + queenSide = color.rightSide() + kingSide = color.leftSide() + of None: + # Unreachable + discard - if result.king: - # Short castle - var location = loc - while true: - location = location + kingSide - if not location.isValid(): - break - if self.grid[location.row, location.col].kind == Empty: - continue - if location == color.kingSideRook() and self.grid[location.row, location.col].kind == Rook: - break - # Blocked by a piece - result.king = false - break - - if result.queen: - # Long castle - var location = loc - while true: - location = location + queenSide - if not location.isValid(): - break - if self.grid[location.row, location.col].kind == Empty: - continue - if location == color.queenSideRook() and self.grid[location.row, location.col].kind == Rook: - break - # Blocked by a piece - result.queen = false - break - - # If the castling king would walk into, through or out of check - # while castling on a given side, then it is not possible to castle - # on that side until the threat exists + if result.king: + # Short castle + var location = loc + while true: + location = location + kingSide + if not location.isValid(): + break + if self.grid[location.row, location.col].color == color: + # Blocked by own piece + result.king = false + break + # Square is attacked or blocked by enemy piece + if self.isAttacked(location, color) or self.grid[location.row, location.col].color != None: + result.king = false + break + # Square is occupied by our rook: we're done. No need to check the color or type of it (because + # if it weren't the right color, castling rights would've already been lost and we wouldn't + # have got this far) + if location == color.kingSideRook(): + break + # Square is empty and not attacked. Keep going - if (result.king or result.queen) and self.inCheck(color): - # Only check for checks if castling is still available - # by this point (if we can avoid calls to generateMoves, - # we should) - return - # TODO: Check for attacks in the various other squares + if result.queen: + # Long castle + var location = loc + while true: + location = location + queenSide + if not location.isValid(): + break + if self.grid[location.row, location.col].color == color: + result.queen = false + break + if self.isAttacked(location, color) or self.grid[location.row, location.col].color != None: + result.queen = false + break + if location == color.queenSideRook(): + break proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = @@ -656,48 +654,46 @@ proc generatePawnMoves(self: ChessBoard, location: Location): seq[Move] = flags: seq[MoveFlag] = @[] doAssert piece.kind == Pawn, &"generatePawnMoves called on a {piece.kind}" # Pawns can move forward one square - let forwardOffset = piece.color.forward() - let forward = (forwardOffset + location) + let forward = (piece.color.forward() + location) # Only if the square is empty though if forward.isValid() and self.grid[forward.row, forward.col].color == None: - locations.add(forwardOffset) + locations.add(forward) flags.add(Default) # If the pawn is on its first rank, it can push two squares if location.row == piece.getStartRow(): - let doubleOffset = piece.color.doublePush() - let double = location + doubleOffset - # Check if both squares are available + let double = location + piece.color.doublePush() + # Check that both squares are empty if double.isValid() and self.grid[forward.row, forward.col].color == None and self.grid[double.row, double.col].color == None: - locations.add(piece.color.doublePush()) + 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 diagonal of the target + # 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 = piece.color.topRightDiagonal() - if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)): + 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 = piece.color.topLeftDiagonal() - if (diagonal + location).isValid() and self.isCapture(Move(piece: piece, startSquare: location, targetSquare: location + diagonal)): + 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) - var - newLocation: Location - targetPiece: Piece + var targetPiece: Piece for (target, flag) in zip(locations, flags): - newLocation = location + target - targetPiece = self.grid[newLocation.row, newLocation.col] - if newLocation.row == piece.color.getLastRow(): + 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: newLocation, piece: piece, flag: promotionType)) + result.add(Move(startSquare: location, targetSquare: target, piece: piece, flag: promotionType)) continue - result.add(Move(startSquare: location, targetSquare: newLocation, piece: piece, flag: flag)) + result.add(Move(startSquare: location, targetSquare: target, piece: piece, flag: flag)) proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = @@ -734,7 +730,9 @@ proc generateSlidingMoves(self: ChessBoard, location: Location): seq[Move] = if otherPiece.color == piece.color.opposite: # Target square contains an enemy piece: capture # it and stop going any further - result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) + if otherPiece.kind != King: + # Can't capture the king + result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) break # Target square is empty result.add(Move(startSquare: location, targetSquare: square, piece: piece)) @@ -754,30 +752,30 @@ proc generateKingMoves(self: ChessBoard, location: Location): seq[Move] = piece.color.leftSide(), piece.color.rightSide()] # Castling - let canCastle = self.canCastle() + let canCastle = self.canCastle(piece.color) if canCastle.queen: directions.add(longCastleKing()) if canCastle.king: directions.add(shortCastleKing()) var flag = Default for direction in directions: + # Step in this direction once + let square: Location = location + direction + # End of board reached + if not square.isValid(): + continue if direction == longCastleKing(): flag = CastleLong elif direction == shortCastleKing(): flag = CastleShort else: flag = Default - # Step in this direction once - let square: Location = location + direction - # End of board reached - if not square.isValid(): - continue let otherPiece = self.grid[square.row, square.col] if otherPiece.color == self.getActiveColor.opposite(): flag = Capture - # A friendly piece is in the way, move onto the next - # direction - if otherPiece.color == piece.color: + # 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: continue # Target square is empty or contains an enemy piece: # All good for us! @@ -804,20 +802,20 @@ proc generateKnightMoves(self: ChessBoard, location: Location): seq[Move] = if not square.isValid(): continue let otherPiece = self.grid[square.row, square.col] - # A friendly piece is in the way - if otherPiece.color == piece.color: + # A friendly piece or the opponent king is is in the way + if otherPiece.color == piece.color or otherPiece.kind == King: continue - if otherPiece.color == piece.color.opposite: + if otherPiece.color != None: # Target square contains an enemy piece: capture # it result.add(Move(startSquare: location, targetSquare: square, piece: piece, flag: Capture)) - continue - # Target square is empty - result.add(Move(startSquare: location, targetSquare: square, piece: piece)) + else: + # Target square is empty + result.add(Move(startSquare: location, targetSquare: square, piece: piece)) proc generateMoves(self: ChessBoard, location: Location): seq[Move] = - ## Returns the list of possible pseudo-legal chess moves for the + ## Returns the list of possible legal chess moves for the ## piece in the given location let piece = self.grid[location.row, location.col] case piece.kind: @@ -834,7 +832,7 @@ proc generateMoves(self: ChessBoard, location: Location): seq[Move] = proc generateAllMoves*(self: ChessBoard): seq[Move] = - ## Returns the list of all possible pseudo-legal moves + ## Returns the list of all possible legal moves ## in the current position for i, row in self.grid: for j, piece in row: @@ -843,23 +841,36 @@ proc generateAllMoves*(self: ChessBoard): seq[Move] = result.add(move) -proc getAttackers*(self: ChessBoard, square: Location): seq[Location] = +proc getAttackers*(self: ChessBoard, square: Location, color = None): seq[Location] = ## Returns all the attackers of the given square - for attack in self.position.attacked.black: - if attack.dest == square: - result.add(attack.source) - for attack in self.position.attacked.white: - if attack.dest == square: - result.add(attack.source) + ## 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.dest == square: + result.add(attack.source) + of Black: + for attack in self.position.attacked.white: + if attack.dest == 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): bool = +proc isAttacked*(self: ChessBoard, loc: Location, color: PieceColor = None): bool = ## Returns whether the given location is attacked - ## by the current opponent - let piece = self.grid[loc.row, loc.col] - case piece.color: + ## by the given opponent + var color = color + if color == None: + color = self.getActiveColor() + case color: of White: for attack in self.position.attacked.black: if attack.dest == loc: @@ -869,17 +880,7 @@ proc isAttacked*(self: ChessBoard, loc: Location): bool = if attack.dest == loc: return true of None: - case self.getActiveColor(): - of White: - for attack in self.position.attacked.black: - if attack.dest == loc: - return true - of Black: - for attack in self.position.attacked.black: - if attack.dest == loc: - return true - else: - discard + discard proc isAttacked*(self: ChessBoard, square: string): bool = @@ -888,20 +889,66 @@ proc isAttacked*(self: ChessBoard, square: string): bool = return self.isAttacked(square.algebraicToLocation()) +func addAttack(self: ChessBoard, attack: tuple[source, dest: Location], color: PieceColor) {.inline.} = + if attack.source.isValid() and attack.dest.isValid(): + case color: + of White: + self.position.attacked.white.add(attack) + of Black: + self.position.attacked.black.add(attack) + else: + discard + + 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) - let piece = self.grid[loc.row, loc.col] - self.position.attacked.white.add((loc, loc + piece.color.topRightDiagonal())) - self.position.attacked.white.add((loc, loc + piece.color.topLeftDiagonal())) + self.addAttack((loc, loc + White.topRightDiagonal()), White) + self.addAttack((loc, loc + White.topRightDiagonal()), White) # We do the same thing for black for loc in self.position.pieces.black.pawns: - let piece = self.grid[loc.row, loc.col] - self.position.attacked.black.add((loc, loc + piece.color.topRightDiagonal())) - self.position.attacked.black.add((loc, loc + piece.color.topLeftDiagonal())) + self.addAttack((loc, loc + Black.topRightDiagonal()), Black) + self.addAttack((loc, loc + 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) + 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) + + +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) + + 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) proc getSlidingAttacks(self: ChessBoard, loc: Location): Attacked = @@ -947,24 +994,24 @@ proc updateSlidingAttacks(self: ChessBoard) = # Bishops for loc in self.position.pieces.white.bishops: for attack in self.getSlidingAttacks(loc): - self.position.attacked.white.add(attack) + self.addAttack(attack, White) for loc in self.position.pieces.black.bishops: for attack in self.getSlidingAttacks(loc): - self.position.attacked.black.add(attack) + self.addAttack(attack, Black) # Rooks for loc in self.position.pieces.white.rooks: for attack in self.getSlidingAttacks(loc): - self.position.attacked.white.add(attack) + self.addAttack(attack, White) for loc in self.position.pieces.black.rooks: for attack in self.getSlidingAttacks(loc): - self.position.attacked.black.add(attack) + self.addAttack(attack, Black) # Queens for loc in self.position.pieces.white.queens: for attack in self.getSlidingAttacks(loc): - self.position.attacked.white.add(attack) + self.addAttack(attack, White) for loc in self.position.pieces.black.queens: for attack in self.getSlidingAttacks(loc): - self.position.attacked.black.add(attack) + self.addAttack(attack, Black) proc updateAttackedSquares(self: ChessBoard) = @@ -977,28 +1024,16 @@ proc updateAttackedSquares(self: ChessBoard) = self.updatePawnAttacks() # Sliding pieces self.updateSlidingAttacks() - # Knights - for loc in self.position.pieces.white.knights: - for move in self.generateMoves(loc): - self.position.attacked.white.add((move.startSquare, move.targetSquare)) - # King - for move in self.generateMoves(self.position.pieces.white.king): - self.position.attacked.white.add((move.startSquare, move.targetSquare)) - # Knights - for loc in self.position.pieces.black.knights: - for move in self.generateMoves(loc): - self.position.attacked.black.add((move.startSquare, move.targetSquare)) - # King - for move in self.generateMoves(self.position.pieces.black.king): - self.position.attacked.black.add((move.startSquare, move.targetSquare)) + self.updateKnightAttacks() + # Kings + self.updateKingAttacks() -proc removePiece(self: ChessBoard, location: Location, emptyGrid: bool = true) = +proc removePiece(self: ChessBoard, location: Location, attack: bool = true) = ## Removes a piece from the board, updating necessary ## metadata var piece = self.grid[location.row, location.col] - if emptyGrid: - self.grid[location.row, location.col] = emptyPiece() + self.grid[location.row, location.col] = emptyPiece() case piece.color: of White: case piece.kind: @@ -1034,6 +1069,8 @@ proc removePiece(self: ChessBoard, location: Location, emptyGrid: bool = true) = discard else: discard + if attack: + self.updateAttackedSquares() proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = @@ -1108,10 +1145,14 @@ proc movePiece(self: ChessBoard, startSquare, targetSquare: Location, attack: bo proc updateLocations(self: ChessBoard, move: Move) = ## Internal helper to update the position of ## the pieces on the board after a move - let capture = self.getCapture(move) - if capture != emptyLocation(): - self.position.captured = self.grid[capture.row, capture.col] - self.removePiece(capture) + if move.isCapture(): + self.position.captured = self.grid[move.targetSquare.row, move.targetSquare.col] + try: + self.removePiece(move.targetSquare, attack=false) + except AssertionDefect: + echo move + raise + # Update the positional metadata of the moving piece self.movePiece(move) @@ -1132,8 +1173,7 @@ proc doMove(self: ChessBoard, move: Move) = halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castlingAvailable = self.position.castlingAvailable - let capture = self.getCapture(move) - if move.piece.kind == Pawn or self.isCapture(move): + if move.piece.kind == Pawn or move.isCapture(): halfMoveClock = 0 else: inc(halfMoveClock) @@ -1161,22 +1201,22 @@ proc doMove(self: ChessBoard, move: Move) = else: discard # Has a rook been captured? - if capture != emptyLocation(): - let piece = self.grid[capture.row, capture.col] + if move.isCapture(): + let piece = self.grid[move.targetSquare.row, move.targetSquare.col] if piece.kind == Rook: case piece.color: of White: - if capture == piece.color.queenSideRook(): + if move.targetSquare == piece.color.queenSideRook(): # Queen side castlingAvailable.white.queen = false - elif capture == piece.color.kingSideRook(): + elif move.targetSquare == piece.color.kingSideRook(): # King side castlingAvailable.white.king = false of Black: - if capture == piece.color.queenSideRook(): + if move.targetSquare == piece.color.queenSideRook(): # Queen side castlingAvailable.black.queen = false - elif capture == piece.color.kingSideRook(): + elif move.targetSquare == piece.color.kingSideRook(): # King side castlingAvailable.black.king = false else: @@ -1193,7 +1233,22 @@ 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) @@ -1211,6 +1266,16 @@ proc doMove(self: ChessBoard, move: Move) = 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 + else: + discard # Move the rook onto the # correct file var @@ -1283,63 +1348,82 @@ proc spawnPiece(self: ChessBoard, location: Location, piece: Piece) = self.grid[location.row, location.col] = piece +proc resetBoard*(self: ChessBoard) = + ## Resets the internal grid representation + ## according to the positional data stored + ## in the chessboard. Warning: this can be + ## expensive, especially in critical paths + ## or tight loops + self.grid = newMatrixFromSeq[Piece](empty, (8, 8)) + for loc in self.position.pieces.white.pawns: + self.grid[loc.row, loc.col] = Piece(color: White, kind: Pawn) + for loc in self.position.pieces.black.pawns: + self.grid[loc.row, loc.col] = Piece(color: Black, kind: Pawn) + for loc in self.position.pieces.white.bishops: + self.grid[loc.row, loc.col] = Piece(color: White, kind: Bishop) + for loc in self.position.pieces.black.bishops: + self.grid[loc.row, loc.col] = Piece(color: Black, kind: Bishop) + for loc in self.position.pieces.white.knights: + self.grid[loc.row, loc.col] = Piece(color: White, kind: Knight) + for loc in self.position.pieces.black.knights: + self.grid[loc.row, loc.col] = Piece(color: Black, kind: Knight) + for loc in self.position.pieces.white.rooks: + self.grid[loc.row, loc.col] = Piece(color: White, kind: Rook) + for loc in self.position.pieces.black.rooks: + self.grid[loc.row, loc.col] = Piece(color: Black, kind: Rook) + for loc in self.position.pieces.white.queens: + self.grid[loc.row, loc.col] = Piece(color: White, kind: Queen) + for loc in self.position.pieces.black.queens: + self.grid[loc.row, loc.col] = Piece(color: Black, kind: Queen) + self.grid[self.position.pieces.white.king.row, self.position.pieces.white.king.col] = Piece(color: White, kind: King) + self.grid[self.position.pieces.black.king.row, self.position.pieces.black.king.col] = Piece(color: Black, kind: King) + + proc undoMove*(self: ChessBoard, move: Move) = ## Undoes the given move if self.positions.len() == 0: return + let previous = self.position var position = self.positions[^1] - while true: - if position.move == move: - break + 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 self.isCapture(move): - self.grid[move.targetSquare.row, move.targetSquare.col] = self.position.captured + if move.isCapture(): + self.grid[move.targetSquare.row, move.targetSquare.col] = previous.captured else: self.grid[move.targetSquare.row, move.targetSquare.col] = emptyPiece() - self.position = position + # Reset the location of the rook in the + # grid (it's already correct in the piece + # list) + if move.flag == CastleLong: + let + rookOld = move.targetSquare + move.piece.color.rightSide() + rookNew = move.piece.color.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() + self.grid[rookNew.row, rookNew.col] = self.grid[rookOld.row, rookOld.col] + self.grid[rookOld.row, rookOld.col] = emptyPiece() - -proc isLegal(self: ChessBoard, move: Move, keep: bool = false): bool = +proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = ## Returns whether the given move is legal - self.doMove(move) - if move notin self.generateMoves(move.startSquare): - # Piece cannot arrive to destination (blocked - # or otherwise invalid move) - return false - if not keep: - defer: self.undoMove(move) - # Move would reveal an attack - # on our king: not allowed - return self.inCheck(move.piece.color) - - -proc isLegalFast(self: ChessBoard, move: Move, keep: bool = false): bool = - ## Returns whether the given move is legal - ## assuming that the input move is pseudo legal - self.position.move = move - self.doMove(move) - if not keep: - defer: self.undoMove(move) - - # Move would reveal an attack - # on our king: not allowed - if self.inCheck(move.piece.color): - return false - # All checks have passed: move is legal - result = true + return move in self.generateMoves(move.startSquare) proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = ## Like the other makeMove(), but with a Move object result = move self.position.move = move - let legal = self.isLegal(move, keep=true) - if not legal: + if not self.isLegal(move): return emptyMove() + self.doMove(move) result = self.position.move @@ -1409,37 +1493,59 @@ proc pretty*(self: ChessBoard): string = result &= "\x1b[0m" -proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current: int = 0): int = +proc countLegalMoves*(self: ChessBoard, ply: int, verbose: bool = false, current: int = 0): tuple[nodes: int, captures: int, castles: int, checks: int, promotions: int] = ## Counts (and debugs) the number of legal positions reached after - ## the given number of half moves + ## the given number of ply if ply == 0: - result = 1 + result = (1, 0, 0, 0, 0) else: - var before: string + var + before: string for move in self.generateAllMoves(): before = self.pretty() self.doMove(move) - if not self.inCheck(move.piece.color): - if verbose: - let canCastle = self.canCastle() - echo "\x1Bc" - echo &"Ply: {self.position.plyFromRoot} (move {current + result + 1})" - echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))" - echo &"Turn: {move.piece.color}" - echo &"Piece: {move.piece.kind}" - echo &"Flag: {move.flag}" - echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" - echo "\nBefore:" - echo before - echo "\nNow: " - echo self.pretty() - try: - discard readLine(stdin) - except IOError: - discard - except EOFError: - discard - result += self.countLegalMoves(ply - 1, verbose, result + 1) + case move.flag: + of Capture: + inc(result.captures) + of CastleShort, CastleLong: + inc(result.castles) + of PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook: + inc(result.promotions) + else: + discard + if self.inCheck(): + # Opponent king is in check + inc(result.checks) + if verbose: + let canCastle = self.canCastle(move.piece.color) + echo "\x1Bc" + echo &"Ply: {self.position.plyFromRoot} (move {current + result.nodes + 1})" + echo &"Move: {move.startSquare.locationToAlgebraic()}{move.targetSquare.locationToAlgebraic()} (({move.startSquare.row}, {move.startSquare.col}) -> ({move.targetSquare.row}, {move.targetSquare.col}))" + echo &"Turn: {move.piece.color}" + echo &"Piece: {move.piece.kind}" + echo &"Flag: {move.flag}" + echo &"Can castle:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" + echo "\nBefore:" + echo before + echo "\nNow: " + echo self.pretty() + echo &"\n\nTotal captures: {result.captures}" + echo &"Total castles: {result.castles}" + echo &"Total checks: {result.checks}" + echo &"Total promotions: {result.promotions}" + + try: + discard readLine(stdin) + except IOError: + discard + except EOFError: + discard + let next = self.countLegalMoves(ply - 1, verbose, result.nodes + 1) + result.nodes += next.nodes + result.captures += next.captures + result.checks += next.checks + result.promotions += next.promotions + result.castles += next.castles self.undoMove(move) @@ -1501,6 +1607,6 @@ when isMainModule: when compileOption("profiler"): import nimprof - - echo b.countLegalMoves(4, verbose=false) + #b = newChessboardFromFEN("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - ") + echo b.countLegalMoves(3, verbose=false) echo "All tests were successful" \ No newline at end of file