# Copyright 2023 Mattia Giambirtone & All Contributors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import std/strutils import std/strformat import std/times import std/math import std/bitops import nimfishpkg/bitboards import nimfishpkg/magics import nimfishpkg/pieces import nimfishpkg/moves export bitboards, magics, pieces, moves type CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64] Position* = object ## A chess position # Castling metadata. Updated on every move castling: array[64, uint8] # Number of half-moves that were performed # to reach this position starting from the # root of the tree plyFromRoot: int8 # Number of half moves since # last piece capture or pawn movement. # Used for the 50-move rule halfMoveClock: int8 # Full move counter. Increments # every 2 ply (half-moves) fullMoveCount: int8 # En passant target square (see https://en.wikipedia.org/wiki/En_passant) enPassantSquare*: Square # Active color turn: PieceColor # Positional bitboards for all pieces pieces: tuple[white, black: tuple[king, queens, rooks, bishops, knights, pawns: Bitboard]] # Pinned pieces for each side pins: tuple[white, black: Bitboard] # Checking pieces checkers: tuple[white, black: Bitboard] ChessBoard* = ref object ## A chess board object # The actual board where pieces live grid: array[64, Piece] # The current position position: Position # List of all previously reached positions positions: seq[Position] # Index of the current position currPos: int # A bunch of simple utility functions and forward declarations proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} proc isLegal(self: ChessBoard, move: Move): bool {.inline.} proc doMove(self: ChessBoard, move: Move) proc pretty*(self: ChessBoard): string proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) proc toFEN*(self: ChessBoard): string proc unmakeMove*(self: ChessBoard) proc movePiece(self: ChessBoard, move: Move, attack: bool = true) proc removePiece(self: ChessBoard, square: Square, attack: bool = true) proc extend[T](self: var seq[T], other: openarray[T]) {.inline.} = for x in other: self.add(x) proc updateBoard*(self: ChessBoard) # A bunch of getters func getSideToMove*(self: ChessBoard): PieceColor {.inline.} = ## Returns the currently side to move return self.position.turn func getEnPassantTarget*(self: ChessBoard): Square {.inline.} = ## Returns the current en passant target square return self.position.enPassantSquare func getMoveCount*(self: ChessBoard): int {.inline.} = ## Returns the number of full moves that ## have been played return self.position.fullMoveCount func getHalfMoveCount*(self: ChessBoard): int {.inline.} = ## Returns the current number of half-moves ## since the last irreversible move return self.position.halfMoveClock func getStartRank(piece: Piece): int {.inline.} = ## Retrieves the starting row of ## the given piece inside our 8x8 ## grid case piece.color: of None: return -1 of White: case piece.kind: of Pawn: return 6 else: return 7 of Black: case piece.kind: of Pawn: return 1 else: return 0 func getKingStartingSquare(color: PieceColor): Square {.inline.} = ## Retrieves the starting square of the king ## for the given color case color: of White: return "e1".toSquare() of Black: return "e8".toSquare() else: discard func kingSideRook(color: PieceColor): Square {.inline.} = (if color == White: "h1".toSquare() else: "h8".toSquare()) func queenSideRook(color: PieceColor): Square {.inline.} = (if color == White: "a8".toSquare() else: "a1".toSquare()) func longCastleKing(color: PieceColor): Square {.inline.} = (if color == White: "c1".toSquare() else: "c8".toSquare()) func shortCastleKing(color: PieceColor): Square {.inline.} = (if color == White: "g1".toSquare() else: "g8".toSquare()) func longCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(7, 3) else: makeSquare(7, 5)) func shortCastleRook(color: PieceColor): Square {.inline.} = (if color == White: makeSquare(0, 0) else: makeSquare(0, 2)) proc inCheck(self: ChessBoard, side: PieceColor): bool proc newChessboard: ChessBoard = ## Returns a new, empty chessboard new(result) for i in 0..63: result.grid[i] = nullPiece() result.position = Position(enPassantSquare: nullSquare(), turn: White) # Indexing operations func `[]`(self: array[64, Piece], square: Square): Piece {.inline.} = self[square.int8] func `[]=`(self: var array[64, Piece], square: Square, piece: Piece) {.inline.} = self[square.int8] = piece func getDirectionMask(self: ChessBoard, square: Square, direction: Direction): Bitboard = ## Like getDirectionMask(), but used within the board context ## with a piece square and direction only return getDirectionMask(square, self.grid[square].color, direction) func getBitboard(self: ChessBoard, kind: PieceKind, color: PieceColor): Bitboard = ## Returns the positional bitboard for the given piece kind and color case color: of White: case kind: of Pawn: return self.position.pieces.white.pawns of Knight: return self.position.pieces.white.knights of Bishop: return self.position.pieces.white.bishops of Rook: return self.position.pieces.white.rooks of Queen: return self.position.pieces.white.queens of King: return self.position.pieces.white.king else: discard of Black: case kind: of Pawn: return self.position.pieces.black.pawns of Knight: return self.position.pieces.black.knights of Bishop: return self.position.pieces.black.bishops of Rook: return self.position.pieces.black.rooks of Queen: return self.position.pieces.black.queens of King: return self.position.pieces.black.king else: discard else: discard func getBitboard(self: ChessBoard, piece: Piece): Bitboard = ## Returns the positional bitboard for the given piece type return self.getBitboard(piece.kind, piece.color) proc newChessboardFromFEN*(fen: string): ChessBoard = ## Initializes a chessboard with the ## position encoded by the given FEN string result = newChessboard() var # Current square in the grid row: int8 = 0 column: int8 = 0 # Current section in the FEN string section = 0 # Current index into the FEN string index = 0 # Temporary variable to store a piece piece: Piece pieces: int # See https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation while index <= fen.high(): var c = fen[index] if c == ' ': # Next section inc(section) inc(index) continue case section: of 0: # Piece placement data case c.toLowerAscii(): # Piece of 'r', 'n', 'b', 'q', 'k', 'p': let square: Square = makeSquare(row, column) bitIndex = square.int8 # We know for a fact these values are in our # enumeration, so all is good {.warning[HoleEnumConv]:off.} piece = Piece(kind: PieceKind(c.toLowerAscii()), color: if c.isUpperAscii(): White else: Black) case piece.color: of Black: case piece.kind: of Pawn: result.position.pieces.black.pawns.uint64.uint64.setBit(bitIndex) of Bishop: result.position.pieces.black.bishops.uint64.setBit(bitIndex) of Knight: result.position.pieces.black.knights.uint64.setBit(bitIndex) of Rook: result.position.pieces.black.rooks.uint64.setBit(bitIndex) of Queen: result.position.pieces.black.queens.uint64.setBit(bitIndex) of King: if result.position.pieces.black.king != 0: raise newException(ValueError, "invalid position: exactly one king of each color must be present") result.position.pieces.black.king.uint64.setBit(bitIndex) else: discard of White: case piece.kind: of Pawn: result.position.pieces.white.pawns.uint64.setBit(bitIndex) of Bishop: result.position.pieces.white.bishops.uint64.setBit(bitIndex) of Knight: result.position.pieces.white.knights.uint64.setBit(bitIndex) of Rook: result.position.pieces.white.rooks.uint64.setBit(bitIndex) of Queen: result.position.pieces.white.queens.uint64.setBit(bitIndex) of King: if result.position.pieces.white.king != 0: raise newException(ValueError, "invalid position: exactly one king of each color must be present") result.position.pieces.white.king.uint64.setBit(bitIndex) else: discard else: discard result.grid[square] = piece inc(column) of '/': # Next row inc(row) column = 0 of '0'..'9': # Skip x columns let x = int(uint8(c) - uint8('0')) if x > 8: raise newException(ValueError, &"invalid FEN: invalid column skip size ({x} > 8)") column += int8(x) else: raise newException(ValueError, &"invalid FEN: unknown piece identifier '{c}'") of 1: # Active color case c: of 'w': result.position.turn = White of 'b': result.position.turn = Black else: raise newException(ValueError, &"invalid FEN: invalid active color identifier '{c}'") of 2: # Castling availability case c: # TODO of '-': discard of 'K': discard # result.position.castlingAvailable.white.king = true of 'Q': discard # result.position.castlingAvailable.white.queen = true of 'k': discard # result.position.castlingAvailable.black.king = true of 'q': discard # result.position.castlingAvailable.black.queen = true else: raise newException(ValueError, &"invalid FEN: unknown symbol '{c}' found in castling availability section") of 3: # En passant target square case c: of '-': # Field is already uninitialized to the correct state discard else: result.position.enPassantSquare = fen[index..index+1].toSquare() # Square metadata is 2 bytes long inc(index) of 4: # Halfmove clock var s = "" while not fen[index].isSpaceAscii(): s.add(fen[index]) inc(index) # Backtrack so the space is seen by the # next iteration of the loop dec(index) result.position.halfMoveClock = parseInt(s).int8 of 5: # Fullmove number var s = "" while index <= fen.high(): s.add(fen[index]) inc(index) result.position.fullMoveCount = parseInt(s).int8 else: raise newException(ValueError, "invalid FEN: too many fields in FEN string") inc(index) if result.inCheck(result.getSideToMove().opposite): # Opponent king cannot be captured on the next move raise newException(ValueError, "invalid position: opponent king can be captured") if result.position.pieces.white.king == 0 or result.position.pieces.black.king == 0: # Both kings must be on the board raise newException(ValueError, "invalid position: exactly one king of each color must be present") 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 = ## Returns the number of pieces with ## the given color and type in the ## current position case color: of White: case kind: of Pawn: return self.position.pieces.white.pawns.uint64.countSetBits() of Bishop: return self.position.pieces.white.bishops.uint64.countSetBits() of Knight: return self.position.pieces.white.knights.uint64.countSetBits() of Rook: return self.position.pieces.white.rooks.uint64.countSetBits() of Queen: return self.position.pieces.white.queens.uint64.countSetBits() of King: return self.position.pieces.white.king.uint64.countSetBits() else: raise newException(ValueError, "invalid piece type") of Black: case kind: of Pawn: return self.position.pieces.black.pawns.uint64.countSetBits() of Bishop: return self.position.pieces.black.bishops.uint64.countSetBits() of Knight: return self.position.pieces.black.knights.uint64.countSetBits() of Rook: return self.position.pieces.black.rooks.uint64.countSetBits() of Queen: return self.position.pieces.black.queens.uint64.countSetBits() of King: return self.position.pieces.black.king.uint64.countSetBits() else: raise newException(ValueError, "invalid piece type") of None: raise newException(ValueError, "invalid piece color") func countPieces*(self: ChessBoard, piece: Piece): int {.inline.} = ## Returns the number of pieces on the board that ## are of the same type and color as the given piece return self.countPieces(piece.kind, piece.color) proc getPiece*(self: ChessBoard, square: Square): Piece {.inline.} = ## Gets the piece at the given square return self.grid[square] proc getPiece*(self: ChessBoard, square: string): Piece {.inline.} = ## Gets the piece on the given square ## in algebraic notation return self.getPiece(square.toSquare()) func isPromotion*(move: Move): bool {.inline.} = ## Returns whether the given move is a ## pawn promotion for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]: if (move.flags and promotion.uint16) != 0: return true func getPromotionType*(move: Move): MoveFlag {.inline.} = ## Returns the promotion type of the given move. ## The return value of this function is only valid ## if isPromotion() returns true for promotion in [PromoteToBishop, PromoteToKnight, PromoteToRook, PromoteToQueen]: if (move.flags and promotion.uint16) != 0: return promotion func isCapture*(move: Move): bool {.inline.} = ## Returns whether the given move is a ## cature result = (move.flags and Capture.uint16) == Capture.uint16 func isCastling*(move: Move): bool {.inline.} = ## Returns whether the given move is a ## castle for flag in [CastleLong, CastleShort]: if (move.flags and flag.uint16) != 0: return true func getCastlingType*(move: Move): MoveFlag {.inline.} = ## Returns the castling type of the given move. ## The return value of this function is only valid ## if isCastling() returns true for flag in [CastleLong, CastleShort]: if (move.flags and flag.uint16) != 0: return flag func isEnPassant*(move: Move): bool {.inline.} = ## Returns whether the given move is an ## en passant capture result = (move.flags and EnPassant.uint16) != 0 func isDoublePush*(move: Move): bool {.inline.} = ## Returns whether the given move is a ## double pawn push result = (move.flags and DoublePush.uint16) != 0 func getFlags*(move: Move): seq[MoveFlag] = ## Gets all the flags of this move for flag in [EnPassant, Capture, DoublePush, CastleLong, CastleShort, PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: if (move.flags and flag.uint16) == flag.uint16: result.add(flag) if result.len() == 0: result.add(Default) func getKingSquare(self: ChessBoard, color: PieceColor = None): Square {.inline.} = ## Returns the square of the king for the given ## side (if it is None, the side to move is used) var color = color if color == None: color = self.getSideToMove() case color: of White: return self.position.pieces.white.king.toSquare() of Black: return self.position.pieces.black.king.toSquare() else: discard proc getOccupancyFor(self: ChessBoard, color: PieceColor): Bitboard = ## Get the occupancy bitboard for every piece of the given color case color: of White: let b = self.position.pieces.white return b.pawns or b.knights or b.bishops or b.rooks or b.queens or b.king of Black: let b = self.position.pieces.black return b.pawns or b.knights or b.bishops or b.rooks or b.queens or b.king else: # huh? discard proc getOccupancy(self: ChessBoard): Bitboard = ## Get the occupancy bitboard for every piece on ## the chessboard result = self.getOccupancyFor(Black) or self.getOccupancyFor(White) proc getPawnAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = ## Returns the attack bitboard for the given square from ## the pawns of the given side let sq = square.toBitboard() pawns = self.getBitboard(Pawn, attacker) bottomLeft = sq.backwardLeftRelativeTo(attacker) bottomRight = sq.backwardRightRelativeTo(attacker) return pawns and (bottomLeft or bottomRight) proc getKingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = ## Returns the attack bitboard for the given square from ## the king of the given side result = Bitboard(0) let sq = square.toBitboard() king = self.getBitboard(King, attacker) if (KING_BITBOARDS[square.uint] and king) != 0: result = result or sq proc getKnightAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = ## Returns the attack bitboard for the given square from ## the knights of the given side let sq = square.toBitboard() knights = self.getBitboard(Knight, attacker) result = Bitboard(0) for knight in knights: if (KNIGHT_BITBOARDS[square.uint] and knight.toBitboard()) != 0: result = result or knight.toBitboard() proc getSlidingAttacks(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = ## Returns the attack bitboard for the given square from ## the sliding pieces of the given side let sq = square.toBitboard() queens = self.getBitboard(Queen, attacker) rooks = self.getBitboard(Rook, attacker) bishops = self.getBitboard(Bishop, attacker) occupancy = self.getOccupancy() result = Bitboard(0) for rook in rooks: let blockers = Rook.getRelevantBlockers(square) if (getRookMoves(square, blockers) and rook.toBitboard()) != 0: result = result or rook.toBitboard() for bishop in bishops: let blockers = Bishop.getRelevantBlockers(square) if (getBishopMoves(square, blockers) and bishop.toBitboard()) != 0: result = result or bishop.toBitboard() for queen in queens: let rookBlockers = Rook.getRelevantBlockers(square) if (getRookMoves(square, rookBlockers) and queen.toBitboard()) != 0: result = result or queen.toBitboard() let bishopBlockers = Bishop.getRelevantBlockers(square) if (getBishopMoves(square, bishopBlockers) and queen.toBitboard()) != 0: result = result or queen.toBitboard() proc getAttacksTo(self: ChessBoard, square: Square, attacker: PieceColor): Bitboard = ## Computes the attack bitboard for the given square from ## the given side result = Bitboard(0) let squareBitboard = square.toBitboard() result = result or self.getPawnAttacks(square, attacker) result = result or self.getKingAttacks(square, attacker) result = result or self.getKnightAttacks(square, attacker) result = result or self.getSlidingAttacks(square, attacker) proc inCheck(self: ChessBoard, side: PieceColor): bool = ## Returns if the current side to move is in check return self.getAttacksTo(self.getKingSquare(side), side.opposite()) != 0 proc canCastle(self: ChessBoard, side: PieceColor): tuple[king, queen: bool] = ## Returns if the current side to move can castle return (false, false) # TODO proc getCapturablePieces(self: ChessBoard, side: PieceColor): Bitboard {.inline.} = ## Returns the set of pieces of the given color that can ## be captured # Just a handy helper to filter out the king and avoid code duplication return self.getOccupancyFor(side) and not self.getBitboard(King, side) proc generatePawnMovements(self: ChessBoard, moves: var MoveList) = ## Helper of generatePawnMoves for generating all non-capture ## and non-promotion pawn moves let sideToMove = self.getSideToMove() pawns = self.getBitboard(Pawn, sideToMove) # We can only move to squares that are *not* occupied by another piece. # We also cannot move to the last rank, as that will result in a promotion # and is handled elsewhere allowedSquares = not (self.getOccupancy() or sideToMove.getLastRank()) # Single push for square in pawns.forwardRelativeTo(sideToMove) and allowedSquares: moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square)) # Double push let rank = sideToMove.getFirstRank() # Only pawns on their starting rank can double push for square in (pawns and rank).doubleForwardRelativeTo(sideToMove) and allowedSquares: moves.add(createMove(square.toBitboard().doubleBackwardRelativeTo(sideToMove), square, DoublePush)) proc generatePawnCaptures(self: ChessBoard, moves: var MoveList) = ## Helper of generatePawnMoves for generating all capture ## pawn moves let sideToMove = self.getSideToMove() nonSideToMove = sideToMove.opposite() pawns = self.getBitboard(Pawn, sideToMove) # We can only capture enemy pieces (except the king) enemyPieces = self.getCapturablePieces(nonSideToMove) rightMovement = pawns.forwardRightRelativeTo(sideToMove) leftMovement = pawns.forwardLeftRelativeTo(sideToMove) epTarget = self.getEnPassantTarget() let epBitboard = if (epTarget != nullSquare()): epTarget.toBitboard() else: Bitboard(0) # Top right attacks for square in rightMovement and enemyPieces: moves.add(createMove(square.toBitboard().backwardLeftRelativeTo(sideToMove), square, Capture)) # Top left attacks for square in leftMovement and enemyPieces: moves.add(createMove(square.toBitboard().backwardRightRelativeTo(sideToMove), square, Capture)) # Special case for en passant let epLeft = epBitboard and leftMovement epRight = epBitboard and rightMovement if epLeft != 0: moves.add(createMove(epBitboard.forwardLeftRelativeTo(nonSideToMove), epBitboard, EnPassant)) elif epRight != 0: moves.add(createMove(epBitboard.forwardRightRelativeTo(nonSideToMove), epBitboard, EnPassant)) proc generatePawnPromotions(self: ChessBoard, moves: var MoveList) = ## Helper of generatePawnMoves for generating all pawn promotion ## moves let sideToMove = self.getSideToMove() pawns = self.getBitboard(Pawn, sideToMove) occupancy = self.getOccupancy() for square in pawns.forwardRelativeTo(sideToMove) and not occupancy and sideToMove.getLastRank(): for promotion in [PromoteToBishop, PromoteToKnight, PromoteToQueen, PromoteToRook]: moves.add(createMove(square.toBitboard().backwardRelativeTo(sideToMove), square, promotion)) proc generatePawnMoves(self: ChessBoard, moves: var MoveList) = ## Generates all the legal pawn moves for the side to move self.generatePawnMovements(moves) self.generatePawnCaptures(moves) self.generatePawnPromotions(moves) proc generateRookMovements(self: ChessBoard, moves: var MoveList) = ## Helper of generateRookMoves to generate all non-capture ## rook moves let sideToMove = self.getSideToMove() occupancy = self.getOccupancy() friendlyPieces = self.getOccupancyFor(sideToMove) rooks = self.getBitboard(Rook, sideToMove) for square in rooks: let blockers = occupancy and Rook.getRelevantBlockers(square) var moveset = getRookMoves(square, blockers) # Can't move over our own pieces moveset = moveset and not friendlyPieces for target in moveset: moves.add(createMove(square, target)) proc generateRookCaptures(self: ChessBoard, moves: var MoveList) = ## Helper of generateRookMoves to generate all capture ## rook moves let sideToMove = self.getSideToMove() occupancy = self.getOccupancy() enemyPieces = self.getCapturablePieces(sideToMove.opposite()) rooks = self.getBitboard(Rook, sideToMove) for square in rooks: let blockers = occupancy and Rook.getRelevantBlockers(square) var moveset = getRookMoves(square, blockers) # Can only cature enemy pieces moveset = moveset and enemyPieces for target in moveset: moves.add(createMove(square, target, Capture)) proc generateRookMoves(self: ChessBoard, moves: var MoveList) = ## Helper of generateSlidingMoves to generate rook moves self.generateRookMovements(moves) self.generateRookCaptures(moves) proc generateBishopMovements(self: ChessBoard, moves: var MoveList) = ## Helper of generateBishopMoves to generate all non-capture ## bishop moves let sideToMove = self.getSideToMove() occupancy = self.getOccupancy() friendlyPieces = self.getOccupancyFor(sideToMove) bishops = self.getBitboard(Bishop, sideToMove) for square in bishops: let blockers = occupancy and Bishop.getRelevantBlockers(square) var moveset = getBishopMoves(square, blockers) # Can't move over our own pieces moveset = moveset and not friendlyPieces for target in moveset: moves.add(createMove(square, target)) proc generateBishopCaptures(self: ChessBoard, moves: var MoveList) = ## Helper of generateBishopMoves to generate all capture ## bishop moves let sideToMove = self.getSideToMove() occupancy = self.getOccupancy() enemyPieces = self.getCapturablePieces(sideToMove.opposite()) bishops = self.getBitboard(Bishop, sideToMove) for square in bishops: let blockers = occupancy and Bishop.getRelevantBlockers(square) var moveset = getRookMoves(square, blockers) # Can only cature enemy pieces moveset = moveset and enemyPieces for target in moveset: moves.add(createMove(square, target, Capture)) proc generateBishopMoves(self: ChessBoard, moves: var MoveList) = ## Helper of generateSlidingMoves to generate bishop moves self.generateBishopMovements(moves) self.generateBishopCaptures(moves) proc generateQueenMovements(self: ChessBoard, moves: var MoveList) = ## Helper of generateQueenMoves to generate all non-capture ## bishop moves let sideToMove = self.getSideToMove() occupancy = self.getOccupancy() friendlyPieces = self.getOccupancyFor(sideToMove) queens = self.getBitboard(Queen, sideToMove) for square in queens: # A queen is just a rook plus a bishop in terms of move # generation let rookBlockers = Rook.getRelevantBlockers(square) and occupancy bishopBlockers = Bishop.getRelevantBlockers(square) and occupancy rookMoves = getRookMoves(square, rookBlockers) bishopMoves = getBishopMoves(square, bishopBlockers) var moveset = rookMoves or bishopMoves # Can't move over our own pieces moveset = moveset and not friendlyPieces for target in moveset: moves.add(createMove(square, target)) proc generateQueenCaptures(self: ChessBoard, moves: var MoveList) = ## Helper of generateQueenMoves to generate all capture ## queen moves let sideToMove = self.getSideToMove() occupancy = self.getOccupancy() enemyPieces = self.getCapturablePieces(sideToMove.opposite()) queens = self.getBitboard(Queen, sideToMove) for square in queens: # A queen is just a rook plus a bishop in terms of move # generation let rookBlockers = Rook.getRelevantBlockers(square) and occupancy bishopBlockers = Bishop.getRelevantBlockers(square) and occupancy rookMoves = getRookMoves(square, rookBlockers) bishopMoves = getBishopMoves(square, bishopBlockers) var moveset = rookMoves or bishopMoves # Can only capture the enemy pieces moveset = moveset and enemyPieces for target in moveset: moves.add(createMove(square, target, Capture)) proc generateQueenMoves(self: ChessBoard, moves: var MoveList) = ## Helper of generateSlidingMoves to generate queen moves self.generateQueenMovements(moves) self.generateQueenCaptures(moves) proc generateSlidingMoves(self: ChessBoard, moves: var MoveList) = ## Generates all legal sliding moves for the side to move self.generateRookMoves(moves) self.generateBishopMoves(moves) self.generateQueenMoves(moves) proc generateKingMoves(self: ChessBoard, moves: var MoveList) = ## Generates all legal king moves for the side to move let sideToMove = self.getSideToMove() king = self.getBitboard(King, sideToMove) moveIdx = king.toSquare().uint64 allowedSquares = not self.getOccupancy() nonSideToMove = sideToMove.opposite() enemyPieces = self.getCapturablePieces(nonSideToMove) # Regular moves for square in KING_BITBOARDS[moveIdx] and allowedSquares: moves.add(createMove(king, square)) # Captures for square in KING_BITBOARDS[moveIdx] and enemyPieces: moves.add(createMove(king, square, Capture)) proc generateKnightMoves(self: ChessBoard, moves: var MoveList)= ## Generates all the legal knight moves for the side to move let sideToMove = self.getSideToMove() knights = self.getBitboard(Knight, sideToMove) allowedSquares = not self.getOccupancy() nonSideToMove = sideToMove.opposite() enemyPieces = self.getCapturablePieces(nonSideToMove) for square in knights: # Regular moves for target in KNIGHT_BITBOARDS[square.uint64] and allowedSquares: moves.add(createMove(square, target)) # Captures for target in KNIGHT_BITBOARDS[square.uint64] and enemyPieces: moves.add(createMove(square, target, Capture)) proc checkInsufficientMaterialPieceCount(self: ChessBoard, color: PieceColor): bool = ## Helper function for checkInsufficientMaterial let friendlyPawns = self.countPieces(Piece(kind: Pawn, color: color)) friendlyRooks = self.countPieces(Piece(kind: Rook, color: color)) friendlyQueens = self.countPieces(Piece(kind: Queen, color: color)) friendlyKnights = self.countPieces(Piece(kind: Knight, color: color)) friendlyBishops = self.countPieces(Piece(kind: Bishop, color: color)) enemyPawns = self.countPieces(Piece(kind: Pawn, color: color.opposite())) enemyRooks = self.countPieces(Piece(kind: Rook, color: color.opposite())) enemyQueens = self.countPieces(Piece(kind: Queen, color: color.opposite())) enemyKnights = self.countPieces(Piece(kind: Knight, color: color.opposite())) enemyBishops = self.countPieces(Piece(kind: Bishop, color: color.opposite())) if friendlyPawns > 0 or friendlyRooks > 0 or friendlyQueens > 0: return false if friendlyKnights >= 2: return false if friendlyKnights + friendlyBishops >= 2: return false if friendlyKnights >= 1 and (enemyPawns > 0 or enemyRooks > 0 or enemyBishops > 0 or enemyKnights > 0 or enemyQueens > 0): return false if friendlyBishops >= 1 and (enemyKnights > 0 or enemyPawns > 0): return false return true proc checkInsufficientMaterial(self: ChessBoard): bool = ## Checks if the given position has not enough material for either side to ## checkmate the enemy king. Note that the criteria as implemented here are ## not fully compliant with FIDE rules (they just define a draw by insufficient ## material as "[...] the position is such that the opponent cannot checkmate ## the player’s king by any possible series of legal moves.", which is really ## tricky to implement efficiently). For more info see https://www.reddit.com/r/chess/comments/se89db/a_writeup_on_definitions_of_insufficient_material/ if not (self.checkInsufficientMaterialPieceCount(White) and self.checkInsufficientMaterialPieceCount(Black)): return false let whiteBishops = self.countPieces(Piece(kind: Bishop, color: White)) blackBishops = self.countPieces(Piece(kind: Bishop, color: Black)) if blackBishops + whiteBishops >= 2: var darkSquare = 0 lightSquare = 0 for bishop in self.position.pieces.black.bishops: if bishop.isLightSquare(): lightSquare += 1 else: darkSquare += 1 for bishop in self.position.pieces.white.bishops: if bishop.isLightSquare(): lightSquare += 1 else: darkSquare += 1 if darkSquare >= 1 and lightSquare >= 1: return false return true proc generateMoves*(self: ChessBoard, moves: var MoveList) = ## Generates the list of all possible legal moves ## in the current position if self.position.halfMoveClock >= 100: # Draw by 50-move rule return # TODO: Check for draw by insufficient material #[ if self.checkInsufficientMaterial(): return @[] ]# # TODO: Check for repetitions (requires zobrist hashing + table) self.generatePawnMoves(moves) self.generateKingMoves(moves) self.generateKnightMoves(moves) self.generateSlidingMoves(moves) proc removePieceFromBitboard(self: ChessBoard, square: Square) = ## Removes a piece at the given square in the chessboard from ## its respective bitboard let piece = self.grid[square] case piece.color: of White: case piece.kind: of Pawn: self.position.pieces.white.pawns.uint64.clearBit(square.int8) of Bishop: self.position.pieces.white.bishops.uint64.clearBit(square.int8) of Knight: self.position.pieces.white.knights.uint64.clearBit(square.int8) of Rook: self.position.pieces.white.rooks.uint64.clearBit(square.int8) of Queen: self.position.pieces.white.queens.uint64.clearBit(square.int8) of King: self.position.pieces.white.king.uint64.clearBit(square.int8) else: discard of Black: case piece.kind: of Pawn: self.position.pieces.black.pawns.uint64.clearBit(square.int8) of Bishop: self.position.pieces.black.bishops.uint64.clearBit(square.int8) of Knight: self.position.pieces.black.knights.uint64.clearBit(square.int8) of Rook: self.position.pieces.black.rooks.uint64.clearBit(square.int8) of Queen: self.position.pieces.black.queens.uint64.clearBit(square.int8) of King: self.position.pieces.black.king.uint64.clearBit(square.int8) else: discard else: discard proc addPieceToBitboard(self: ChessBoard, square: Square, piece: Piece) = ## Adds the given piece at the given square in the chessboard to ## its respective bitboard case piece.color: of White: case piece.kind: of Pawn: self.position.pieces.white.pawns.uint64.setBit(square.int8) of Bishop: self.position.pieces.white.bishops.uint64.setBit(square.int8) of Knight: self.position.pieces.white.knights.uint64.setBit(square.int8) of Rook: self.position.pieces.white.rooks.uint64.setBit(square.int8) of Queen: self.position.pieces.white.queens.uint64.setBit(square.int8) of King: self.position.pieces.white.king.uint64.setBit(square.int8) else: discard of Black: case piece.kind: of Pawn: self.position.pieces.black.pawns.uint64.setBit(square.int8) of Bishop: self.position.pieces.black.bishops.uint64.setBit(square.int8) of Knight: self.position.pieces.black.knights.uint64.setBit(square.int8) of Rook: self.position.pieces.black.rooks.uint64.setBit(square.int8) of Queen: self.position.pieces.black.queens.uint64.setBit(square.int8) of King: self.position.pieces.black.king.uint64.setBit(square.int8) else: discard else: discard proc removePiece(self: ChessBoard, square: Square, attack: bool = true) = ## Removes a piece from the board, updating necessary ## metadata var piece = self.grid[square] self.grid[square] = nullPiece() self.removePieceFromBitboard(square) #[if attack: self.updateAttackedSquares()]# proc updateMovepieces(self: ChessBoard, move: Move) = ## Updates our bitboard representation after a move: note that this ## does *not* handle captures, en passant, promotions etc. as those ## are already called by helpers such as removePiece() and spawnPiece() var bitboard: uint64 let piece = self.grid[move.startSquare] # TODO: Should we use our helpers or is it faster to branch only once? case piece.color: of White: case piece.kind: of Pawn: self.position.pieces.white.pawns.uint64.setBit(move.targetSquare.int8) self.position.pieces.white.pawns.uint64.clearBit(move.startSquare.int8) of Bishop: self.position.pieces.white.bishops.uint64.setBit(move.targetSquare.int8) self.position.pieces.white.bishops.uint64.clearBit(move.startSquare.int8) of Knight: self.position.pieces.white.knights.uint64.setBit(move.targetSquare.int8) self.position.pieces.white.knights.uint64.clearBit(move.startSquare.int8) of Rook: self.position.pieces.white.rooks.uint64.setBit(move.targetSquare.int8) self.position.pieces.white.rooks.uint64.clearBit(move.startSquare.int8) of Queen: self.position.pieces.white.queens.uint64.setBit(move.targetSquare.int8) self.position.pieces.white.queens.uint64.clearBit(move.startSquare.int8) of King: self.position.pieces.white.king.uint64.setBit(move.targetSquare.int8) self.position.pieces.white.king.uint64.clearBit(move.startSquare.int8) else: discard of Black: case piece.kind: of Pawn: self.position.pieces.black.pawns.uint64.setBit(move.targetSquare.int8) self.position.pieces.black.pawns.uint64.clearBit(move.startSquare.int8) of Bishop: self.position.pieces.black.bishops.uint64.setBit(move.targetSquare.int8) self.position.pieces.black.bishops.uint64.clearBit(move.startSquare.int8) of Knight: self.position.pieces.black.knights.uint64.setBit(move.targetSquare.int8) self.position.pieces.black.knights.uint64.clearBit(move.startSquare.int8) of Rook: self.position.pieces.black.rooks.uint64.setBit(move.targetSquare.int8) self.position.pieces.black.rooks.uint64.clearBit(move.startSquare.int8) of Queen: self.position.pieces.black.queens.uint64.setBit(move.targetSquare.int8) self.position.pieces.black.queens.uint64.clearBit(move.startSquare.int8) of King: self.position.pieces.black.king.uint64.setBit(move.targetSquare.int8) self.position.pieces.black.king.uint64.clearBit(move.startSquare.int8) else: discard else: discard proc movePiece(self: ChessBoard, move: Move, attack: bool = true) = ## Internal helper to move a piece. If attack ## is set to false, then this function does ## not update attacked squares metadata, just ## positional info and the grid itself let piece = self.grid[move.startSquare] let targetSquare = self.getPiece(move.targetSquare) if targetSquare.color != None: raise newException(AccessViolationDefect, &"attempted to overwrite a piece! {move}") # Update positional metadata self.updateMovePieces(move) # Empty out the starting square self.grid[move.startSquare] = nullPiece() # Actually move the piece on the board self.grid[move.targetSquare] = piece #[if attack: self.updateAttackedSquares()]# proc movePiece(self: ChessBoard, startSquare, targetSquare: Square, attack: bool = true) = ## Like the other movePiece(), but with two squares self.movePiece(Move(startSquare: startSquare, targetSquare: targetSquare), attack) proc doMove(self: ChessBoard, move: Move) = ## Internal function called by makeMove after ## performing legality checks. Can be used in ## performance-critical paths where a move is ## already known to be legal # Record final position for future reference self.positions.add(self.position) # Final checks let piece = self.grid[move.startSquare] var halfMoveClock = self.position.halfMoveClock fullMoveCount = self.position.fullMoveCount castling = self.position.castling enPassantTarget = nullSquare() # Needed to detect draw by the 50 move rule if piece.kind == Pawn or move.isCapture() or move.isEnPassant(): halfMoveClock = 0 else: inc(halfMoveClock) if piece.color == Black: inc(fullMoveCount) if move.isDoublePush(): enPassantTarget = move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare() # Castling check: have the rooks moved? if piece.kind == Rook: discard # case piece.color: # of White: # if rowFromSquare(move.startSquare) == piece.getStartRank(): # if columnFromSquare(move.startSquare) == 0: # # Queen side # castlingAvailable.white.queen = false # elif columnfromSquare(move.startSquare) == 7: # # King side # castlingAvailable.white.king = false # of Black: # if rowFromSquare(move.startSquare) == piece.getStartRank(): # if columnFromSquare(move.startSquare) == 0: # # Queen side # castlingAvailable.black.queen = false # elif columnFromSquare(move.startSquare) == 7: # # King side # castlingAvailable.black.king = false # else: # discard # Has a rook been captured? # Create new position self.position = Position(plyFromRoot: self.position.plyFromRoot + 1, halfMoveClock: halfMoveClock, fullMoveCount: fullMoveCount, turn: self.getSideToMove().opposite, castling: castling, enPassantSquare: enPassantTarget, pieces: self.position.pieces ) # Update position metadata if move.isCastling(): # Move the rook onto the # correct file when castling var square: Square target: Square flag: MoveFlag if move.getCastlingType() == CastleShort: square = piece.color.kingSideRook() target = shortCastleRook(piece.color) flag = CastleShort else: square = piece.color.queenSideRook() target = longCastleRook(piece.color) flag = CastleLong let rook = self.grid[square] self.movePiece(createMove(square, target, flag), attack=false) if move.isEnPassant(): # Make the en passant pawn disappear self.removePiece(move.targetSquare.toBitboard().backwardRelativeTo(piece.color).toSquare(), attack=false) if move.isCapture(): # Get rid of captured pieces self.removePiece(move.targetSquare, attack=false) # Move the piece to its target square and update attack metadata self.movePiece(move, attack=false) if move.isPromotion(): # Move is a pawn promotion: get rid of the pawn # and spawn a new piece self.removePiece(move.targetSquare) case move.getPromotionType(): 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: # Unreachable discard #self.updateAttackedSquares() proc spawnPiece(self: ChessBoard, square: Square, piece: Piece) = ## Internal helper to "spawn" a given piece at the given ## square. Note that this will overwrite whatever piece ## was previously located there: use with caution. Does ## not automatically update the attacked square metadata self.addPieceToBitboard(square, piece) self.grid[square] = piece proc updateBoard*(self: ChessBoard) = ## Updates the internal grid representation ## according to the positional data stored ## in the chessboard for i in 0..63: self.grid[i] = nullPiece() for sq in self.position.pieces.white.pawns: self.grid[sq] = Piece(color: White, kind: Pawn) for sq in self.position.pieces.black.pawns: self.grid[sq] = Piece(color: Black, kind: Pawn) for sq in self.position.pieces.white.bishops: self.grid[sq] = Piece(color: White, kind: Bishop) for sq in self.position.pieces.black.bishops: self.grid[sq] = Piece(color: Black, kind: Bishop) for sq in self.position.pieces.white.knights: self.grid[sq] = Piece(color: White, kind: Knight) for sq in self.position.pieces.black.knights: self.grid[sq] = Piece(color: Black, kind: Knight) for sq in self.position.pieces.white.rooks: self.grid[sq] = Piece(color: White, kind: Rook) for sq in self.position.pieces.black.rooks: self.grid[sq] = Piece(color: Black, kind: Rook) for sq in self.position.pieces.white.queens: self.grid[sq] = Piece(color: White, kind: Queen) for sq in self.position.pieces.black.queens: self.grid[sq] = Piece(color: Black, kind: Queen) self.grid[self.position.pieces.white.king.toSquare()] = Piece(color: White, kind: King) self.grid[self.position.pieces.black.king.toSquare()] = Piece(color: Black, kind: King) proc unmakeMove*(self: ChessBoard) = ## Reverts to the previous board position, ## if one exists if self.currPos > 0: dec(self.currPos) self.position = self.positions[self.currPos] self.updateBoard() proc redoMove*(self: ChessBoard) = ## Reverts to the next board position, if one ## exists. Only makes sense after a call to ## unmakeMove if self.positions.high() > self.currPos: inc(self.currPos) self.position = self.positions[self.currPos] self.updateBoard() proc isLegal(self: ChessBoard, move: Move): bool {.inline.} = ## Returns whether the given move is legal var moves = MoveList() self.generateMoves(moves) return move in moves proc makeMove*(self: ChessBoard, move: Move): Move {.discardable.} = ## Makes a move on the board result = move if not self.isLegal(move): return nullMove() self.doMove(move) proc toChar*(piece: Piece): char = if piece.color == White: return char(piece.kind).toUpperAscii() return char(piece.kind) proc `$`*(self: ChessBoard): string = result &= "- - - - - - - -" var file = 8 for i in 0..7: result &= "\n" for j in 0..7: let piece = self.grid[makeSquare(i, j)] if piece.kind == Empty: result &= "x " continue result &= &"{piece.toChar()} " result &= &"{file}" dec(file) result &= "\n- - - - - - - -" result &= "\na b c d e f g h" proc toPretty*(piece: Piece): string = case piece.color: of White: case piece.kind: of King: return "\U2654" of Queen: return "\U2655" of Rook: return "\U2656" of Bishop: return "\U2657" of Knight: return "\U2658" of Pawn: return "\U2659" else: discard of Black: case piece.kind: of King: return "\U265A" of Queen: return "\U265B" of Rook: return "\U265C" of Bishop: return "\U265D" of Knight: return "\U265E" of Pawn: return "\240\159\168\133" else: discard else: discard proc pretty*(self: ChessBoard): string = ## Returns a colored version of the ## board for easier visualization var file = 8 for i in 0..7: if i > 0: result &= "\n" for j in 0..7: # Equivalent to (i + j) mod 2 # (I'm just evil) if ((i + j) and 1) == 0: result &= "\x1b[39;44;1m" else: result &= "\x1b[39;40;1m" let piece = self.grid[makeSquare(i, j)] if piece.kind == Empty: result &= " \x1b[0m" else: result &= &"{piece.toPretty()} \x1b[0m" result &= &" \x1b[33;1m{file}\x1b[0m" dec(file) result &= "\n\x1b[31;1ma b c d e f g h" result &= "\x1b[0m" proc toFEN*(self: ChessBoard): string = ## Returns a FEN string of the current ## position in the chessboard var skip: int # Piece placement data for i in 0..7: skip = 0 for j in 0..7: let piece = self.grid[makeSquare(i, j)] 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.getSideToMove() == White: "w" else: "b") result &= " " # Castling availability result &= "-" # 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() == nullSquare(): result &= "-" else: result &= self.getEnPassantTarget().toAlgebraic() result &= " " # Halfmove clock result &= $self.getHalfMoveCount() result &= " " # Fullmove number result &= $self.getMoveCount() proc perft*(self: ChessBoard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData = ## Counts (and debugs) the number of legal positions reached after ## the given number of ply var moves = MoveList() self.generateMoves(moves) if not bulk: if len(moves) == 0 and self.inCheck(self.getSideToMove()): result.checkmates = 1 # TODO: Should we count stalemates/draws? if ply == 0: result.nodes = 1 return elif ply == 1 and bulk: if divide: var postfix = "" for move in moves: case move.getPromotionType(): of PromoteToBishop: postfix = "b" of PromoteToKnight: postfix = "n" of PromoteToRook: postfix = "r" of PromoteToQueen: postfix = "q" else: postfix = "" echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: 1" if verbose: echo "" return (uint64(len(moves)), 0, 0, 0, 0, 0, 0) for move in moves: if verbose: let canCastle = self.canCastle(self.getSideToMove()) echo &"Ply (from root): {self.position.plyFromRoot}" echo &"Move: {move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}" echo &"Turn: {self.getSideToMove()}" echo &"Piece: {self.grid[move.startSquare].kind}" echo &"Flags: {move.getFlags()}" echo &"In check: {(if self.inCheck(self.getSideToMove()): \"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 &"Position before move: {self.toFEN()}" stdout.write("En Passant target: ") if self.getEnPassantTarget() != nullSquare(): echo self.getEnPassantTarget().toAlgebraic() else: echo "None" echo "\n", self.pretty() self.doMove(move) if ply == 1: if move.isCapture(): inc(result.captures) if move.isCastling(): inc(result.castles) if move.isPromotion(): inc(result.promotions) if move.isEnPassant(): inc(result.enPassant) if self.inCheck(self.getSideToMove()): # Opponent king is in check inc(result.checks) if verbose: let canCastle = self.canCastle(self.getSideToMove()) echo "\n" echo &"Opponent in check: {(if self.inCheck(self.getSideToMove()): \"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("nextpos>> ") try: discard readLine(stdin) except IOError: discard except EOFError: discard let next = self.perft(ply - 1, verbose, bulk=bulk) self.unmakeMove() if divide and (not bulk or ply > 1): var postfix = "" if move.isPromotion(): case move.getPromotionType(): of PromoteToBishop: postfix = "b" of PromoteToKnight: postfix = "n" of PromoteToRook: postfix = "r" of PromoteToQueen: postfix = "q" else: discard echo &"{move.startSquare.toAlgebraic()}{move.targetSquare.toAlgebraic()}{postfix}: {next.nodes}" if verbose: echo "" result.nodes += next.nodes result.captures += next.captures result.checks += next.checks result.promotions += next.promotions result.castles += next.castles result.enPassant += next.enPassant result.checkmates += next.checkmates proc handleGoCommand(board: ChessBoard, command: seq[string]) = if len(command) < 2: echo &"Error: go: invalid number of arguments" return case command[1]: of "perft": if len(command) == 2: echo &"Error: go: perft: invalid number of arguments" return var args = command[2].splitWhitespace() bulk = false verbose = false if args.len() > 1: var ok = true for arg in args[1..^1]: case arg: of "bulk": bulk = true of "verbose": verbose = true else: echo &"Error: go: perft: invalid argument '{args[1]}'" ok = false break if not ok: return try: let ply = parseInt(args[0]) if bulk: let t = cpuTime() let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes echo &"\nNodes searched (bulk-counting: on): {nodes}" echo &"Time taken: {round(cpuTime() - t, 3)} seconds\n" else: let t = cpuTime() let data = board.perft(ply, divide=true, verbose=verbose) echo &"\nNodes searched (bulk-counting: off): {data.nodes}" echo &" - Captures: {data.captures}" echo &" - Checks: {data.checks}" echo &" - E.P: {data.enPassant}" echo &" - Checkmates: {data.checkmates}" echo &" - Castles: {data.castles}" echo &" - Promotions: {data.promotions}" echo "" echo &"Time taken: {round(cpuTime() - t, 3)} seconds" except ValueError: echo "Error: go: perft: invalid depth" else: echo &"Error: go: unknown subcommand '{command[1]}'" proc handleMoveCommand(board: ChessBoard, command: seq[string]): Move {.discardable.} = if len(command) != 2: echo &"Error: move: invalid number of arguments" return let moveString = command[1] if len(moveString) notin 4..5: echo &"Error: move: invalid move syntax" return var startSquare: Square targetSquare: Square flags: seq[MoveFlag] try: startSquare = moveString[0..1].toSquare() except ValueError: echo &"Error: move: invalid start square ({moveString[0..1]})" return try: targetSquare = moveString[2..3].toSquare() except ValueError: echo &"Error: move: invalid target square ({moveString[2..3]})" return # Since the user tells us just the source and target square of the move, # we have to figure out all the flags by ourselves (whether it's a double # push, a capture, a promotion, castling, etc.) if board.grid[targetSquare].kind != Empty: flags.add(Capture) #elif board.grid[startSquare].kind == Pawn and abs(rowFromSquare(startSquare) - rowFromSquare(targetSquare)) == 2: # flags.add(DoublePush) if len(moveString) == 5: # Promotion case moveString[4]: of 'b': flags.add(PromoteToBishop) of 'n': flags.add(PromoteToKnight) of 'q': flags.add(PromoteToQueen) of 'r': flags.add(PromoteToRook) else: echo &"Error: move: invalid promotion type" return var move = createMove(startSquare, targetSquare, flags) let piece = board.getPiece(move.startSquare) if piece.kind == King and move.startSquare == board.getSideToMove().getKingStartingSquare(): if move.targetSquare == longCastleKing(piece.color): move.flags = move.flags or CastleLong.uint16 elif move.targetSquare == shortCastleKing(piece.color): move.flags = move.flags or CastleShort.uint16 if move.targetSquare == board.getEnPassantTarget(): move.flags = move.flags or EnPassant.uint16 result = board.makeMove(move) if result == nullMove(): echo &"Error: move: {moveString} is illegal" proc handlePositionCommand(board: var ChessBoard, command: seq[string]) = if len(command) < 2: echo "Error: position: invalid number of arguments" return # Makes sure we don't leave the board in an invalid state if # some error occurs var tempBoard: ChessBoard case command[1]: of "startpos": tempBoard = newDefaultChessboard() if command.len() > 2: let args = command[2].splitWhitespace() if args.len() > 0: var i = 0 while i < args.len(): case args[i]: of "moves": var j = i + 1 while j < args.len(): if handleMoveCommand(tempBoard, @["move", args[j]]) == nullMove(): return inc(j) inc(i) board = tempBoard of "fen": if len(command) == 2: echo &"Current position: {board.toFEN()}" return var args = command[2].splitWhitespace() fenString = "" stop = 0 for i, arg in args: if arg in ["moves", ]: break if i > 0: fenString &= " " fenString &= arg inc(stop) args = args[stop..^1] try: tempBoard = newChessboardFromFEN(fenString) except ValueError: echo &"error: position: {getCurrentExceptionMsg()}" return if args.len() > 0: var i = 0 while i < args.len(): case args[i]: of "moves": var j = i + 1 while j < args.len(): if handleMoveCommand(tempBoard, @["move", args[j]]) == nullMove(): return inc(j) inc(i) board = tempBoard of "print": echo board of "pretty": echo board.pretty() else: echo &"error: position: unknown subcommand '{command[1]}'" return proc handleUCICommand(board: var ChessBoard, command: seq[string]) = echo "id name Nimfish 0.1" echo "id author Nocturn9x & Contributors (see LICENSE)" # TODO echo "uciok" const HELP_TEXT = """Nimfish help menu: - go: Begin a search Subcommands: - perft [options]: Run the performance test at the given depth (in ply) and print the results Options: - bulk: Enable bulk-counting (significantly faster, gives less statistics) - verbose: Enable move debugging (for each and every move, not recommended on large searches) Example: go perft 5 bulk - position: Get/set board position Subcommands: - fen [string]: Set the board to the given fen string if one is provided, or print the current position as a FEN string if no arguments are given - startpos: Set the board to the starting position - pretty: Pretty-print the current position - print: Print the current position using ASCII characters only Options: - moves {moveList}: Perform the given moves (space-separated, all-lowercase) in algebraic notation after the position is loaded. This option only applies to the "startpos" and "fen" subcommands: it is ignored otherwise Examples: - position startpos - position fen "..." moves a2a3 a7a6 - clear: Clear the screen - move : Perform the given move in algebraic notation - castle: Print castling rights for each side - check: Print if the current side to move is in check - undo, u: Undoes the last move. Can be used in succession - turn: Print which side is to move - ep: Print the current en passant target - pretty: Shorthand for "position pretty" - print: Shorthand for "position print" - fen: Shorthand for "position fen" - pos : Shorthand for "position " - get : Get the piece on the given square - atk: Print the attack bitboard of the given square for the side to move - uci: enter UCI mode (WIP) """ proc main: int = ## Nimfish's control interface echo "Nimfish by nocturn9x (see LICENSE)" var board = newDefaultChessboard() uciMode = false while true: var cmd: seq[string] cmdStr: string try: if not uciMode: stdout.write(">>> ") stdout.flushFile() cmdStr = readLine(stdin).strip(leading=true, trailing=true, chars={'\t', ' '}) if cmdStr.len() == 0: continue cmd = cmdStr.splitWhitespace(maxsplit=2) case cmd[0]: of "uci": handleUCICommand(board, cmd) uciMode = true of "clear": echo "\x1Bc" of "help": echo HELP_TEXT of "go": handleGoCommand(board, cmd) of "position", "pos": handlePositionCommand(board, cmd) of "move": handleMoveCommand(board, cmd) of "pretty", "print", "fen": handlePositionCommand(board, @["position", cmd[0]]) of "undo", "u": board.unmakeMove() of "turn": echo &"Active color: {board.getSideToMove()}" of "atk": if len(cmd) != 2: echo "error: atk: invalid number of arguments" continue try: echo board.getAttacksTo(cmd[1].toSquare(), board.getSideToMove()) except ValueError: echo "error: atk: invalid square" continue of "ep": let target = board.getEnPassantTarget() if target != nullSquare(): echo &"En passant target: {target.toAlgebraic()}" else: echo "En passant target: None" of "get": if len(cmd) != 2: echo "error: get: invalid number of arguments" continue try: echo board.getPiece(cmd[1]) except ValueError: echo "error: get: invalid square" continue of "castle": let canCastle = board.canCastle(board.getSideToMove()) echo &"Castling rights for {($board.getSideToMove()).toLowerAscii()}:\n - King side: {(if canCastle.king: \"yes\" else: \"no\")}\n - Queen side: {(if canCastle.queen: \"yes\" else: \"no\")}" of "check": echo &"{board.getSideToMove()} king in check: {(if board.inCheck(board.getSideToMove()): \"yes\" else: \"no\")}" else: echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." except IOError: echo "" return 0 except EOFError: echo "" return 0 when isMainModule: proc testPiece(piece: Piece, kind: PieceKind, color: PieceColor) = doAssert piece.kind == kind and piece.color == color, &"expected piece of kind {kind} and color {color}, got {piece.kind} / {piece.color} instead" proc testPieceCount(board: ChessBoard, kind: PieceKind, color: PieceColor, count: int) = let pieces = board.countPieces(kind, color) doAssert pieces == count, &"expected {count} pieces of kind {kind} and color {color}, got {pieces} instead" proc testPieceBitboard(bitboard: Bitboard, squares: seq[Square]) = var i = 0 for square in bitboard: doAssert squares[i] == square, &"squares[{i}] != bitboard[i]: {squares[i]} != {square}" inc(i) if i != squares.len(): doAssert false, &"bitboard.len() ({i}) != squares.len() ({squares.len()})" var b = newDefaultChessboard() # Ensure correct number of pieces testPieceCount(b, Pawn, White, 8) testPieceCount(b, Pawn, Black, 8) testPieceCount(b, Knight, White, 2) testPieceCount(b, Knight, Black, 2) testPieceCount(b, Bishop, White, 2) testPieceCount(b, Bishop, Black, 2) testPieceCount(b, Rook, White, 2) testPieceCount(b, Rook, Black, 2) testPieceCount(b, Queen, White, 1) testPieceCount(b, Queen, Black, 1) testPieceCount(b, King, White, 1) testPieceCount(b, King, Black, 1) # Ensure pieces are in the correct squares. This is testing the FEN # parser # Pawns for loc in ["a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2"]: testPiece(b.getPiece(loc), Pawn, White) for loc in ["a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7"]: testPiece(b.getPiece(loc), Pawn, Black) # Rooks testPiece(b.getPiece("a1"), Rook, White) testPiece(b.getPiece("h1"), Rook, White) testPiece(b.getPiece("a8"), Rook, Black) testPiece(b.getPiece("h8"), Rook, Black) # Knights testPiece(b.getPiece("b1"), Knight, White) testPiece(b.getPiece("g1"), Knight, White) testPiece(b.getPiece("b8"), Knight, Black) testPiece(b.getPiece("g8"), Knight, Black) # Bishops testPiece(b.getPiece("c1"), Bishop, White) testPiece(b.getPiece("f1"), Bishop, White) testPiece(b.getPiece("c8"), Bishop, Black) testPiece(b.getPiece("f8"), Bishop, Black) # Kings testPiece(b.getPiece("e1"), King, White) testPiece(b.getPiece("e8"), King, Black) # Queens testPiece(b.getPiece("d1"), Queen, White) testPiece(b.getPiece("d8"), Queen, Black) # Ensure our bitboards match with the board let whitePawns = b.getBitboard(Pawn, White) whiteKnights = b.getBitboard(Knight, White) whiteBishops = b.getBitboard(Bishop, White) whiteRooks = b.getBitboard(Rook, White) whiteQueens = b.getBitboard(Queen, White) whiteKing = b.getBitboard(King, White) blackPawns = b.getBitboard(Pawn, Black) blackKnights = b.getBitboard(Knight, Black) blackBishops = b.getBitboard(Bishop, Black) blackRooks = b.getBitboard(Rook, Black) blackQueens = b.getBitboard(Queen, Black) blackKing = b.getBitboard(King, Black) whitePawnSquares = @[makeSquare(6'i8, 0'i8), makeSquare(6, 1), makeSquare(6, 2), makeSquare(6, 3), makeSquare(6, 4), makeSquare(6, 5), makeSquare(6, 6), makeSquare(6, 7)] whiteKnightSquares = @[makeSquare(7'i8, 1'i8), makeSquare(7, 6)] whiteBishopSquares = @[makeSquare(7'i8, 2'i8), makeSquare(7, 5)] whiteRookSquares = @[makeSquare(7'i8, 0'i8), makeSquare(7, 7)] whiteQueenSquares = @[makeSquare(7'i8, 3'i8)] whiteKingSquares = @[makeSquare(7'i8, 4'i8)] blackPawnSquares = @[makeSquare(1'i8, 0'i8), makeSquare(1, 1), makeSquare(1, 2), makeSquare(1, 3), makeSquare(1, 4), makeSquare(1, 5), makeSquare(1, 6), makeSquare(1, 7)] blackKnightSquares = @[makeSquare(0'i8, 1'i8), makeSquare(0, 6)] blackBishopSquares = @[makeSquare(0'i8, 2'i8), makeSquare(0, 5)] blackRookSquares = @[makeSquare(0'i8, 0'i8), makeSquare(0, 7)] blackQueenSquares = @[makeSquare(0'i8, 3'i8)] blackKingSquares = @[makeSquare(0'i8, 4'i8)] testPieceBitboard(whitePawns, whitePawnSquares) testPieceBitboard(whiteKnights, whiteKnightSquares) testPieceBitboard(whiteBishops, whiteBishopSquares) testPieceBitboard(whiteRooks, whiteRookSquares) testPieceBitboard(whiteQueens, whiteQueenSquares) testPieceBitboard(whiteKing, whiteKingSquares) testPieceBitboard(blackPawns, blackPawnSquares) testPieceBitboard(blackKnights, blackKnightSquares) testPieceBitboard(blackBishops, blackBishopSquares) testPieceBitboard(blackRooks, blackRookSquares) testPieceBitboard(blackQueens, blackQueenSquares) testPieceBitboard(blackKing, blackKingSquares) setControlCHook(proc () {.noconv.} = quit(0)) quit(main())