diff --git a/Chess/nimfish/nimfishpkg/eval.nim b/Chess/nimfish/nimfishpkg/eval.nim index 75a1751..5de265f 100644 --- a/Chess/nimfish/nimfishpkg/eval.nim +++ b/Chess/nimfish/nimfishpkg/eval.nim @@ -153,7 +153,11 @@ const -53, -34, -21, -11, -28, -14, -24, -43 ] - MIDDLEGAME_TABLES: array[6, array[64, Score]] = [ + # Bishop, King, Knight, Pawn, Queen, Rook + MIDDLEGAME_WEIGHTS: array[6, Score] = [365, 0, 337, 82, 1025, 477] + ENDGAME_WEIGHTS: array[6, Score] = [297, 0, 281, 94, 936, 512] + + MIDDLEGAME_PSQ_TABLES: array[6, array[64, Score]] = [ BISHOP_MIDDLEGAME_SCORES, KING_MIDDLEGAME_SCORES, KNIGHT_MIDDLEGAME_SCORES, @@ -162,7 +166,7 @@ const ROOK_MIDDLEGAME_SCORES ] - ENDGAME_TABLES: array[6, array[64, Score]] = [ + ENDGAME_PSQ_TABLES: array[6, array[64, Score]] = [ BISHOP_ENDGAME_SCORES, KING_ENDGAME_SCORES, KNIGHT_ENDGAME_SCORES, @@ -171,33 +175,31 @@ const ROOK_ENDGAME_SCORES ] - -func lowestEval*: Score {.inline.} = Score(-20_000) -func highestEval*: Score {.inline.} = Score(20_000) -func mateScore*: Score {.inline.} = lowestEval() + DOUBLED_PAWNS_MALUS: array[9, Score] = [0, -5, -10, -20, -30, -30, -30, -30, -30] + ISOLATED_PAWN_MALUS: array[9, Score] = [0, -10, -25, -50, -75, -75, -75, -75, -75] -proc getPieceValue(kind: PieceKind): Score = - ## Returns the absolute value of a piece - case kind: - of Pawn: - return Score(100) - of Bishop: - return Score(330) - of Knight: - return Score(280) - of Rook: - return Score(525) - of Queen: - return Score(950) - else: - discard +var + MIDDLEGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]] + ENDGAME_VALUE_TABLES: array[2, array[6, array[64, Score]]] -proc getPieceScore*(board: Chessboard, square: Square): Score = - ## Returns the value of the piece located at - ## the given square - return board.getPiece(square).kind.getPieceValue() + +proc initializeTables = + for kind in [Bishop, King, Knight, Pawn, Queen, Rook]: + for sq in 0..63: + MIDDLEGAME_VALUE_TABLES[White.int][kind.int][sq] = MIDDLEGAME_WEIGHTS[kind.int] + MIDDLEGAME_PSQ_TABLES[kind.int][sq] + ENDGAME_VALUE_TABLES[White.int][kind.int][sq] = ENDGAME_WEIGHTS[kind.int] + ENDGAME_PSQ_TABLES[kind.int][sq] + MIDDLEGAME_VALUE_TABLES[Black.int][kind.int][sq] = MIDDLEGAME_WEIGHTS[kind.int] + MIDDLEGAME_PSQ_TABLES[kind.int][sq xor 56] + ENDGAME_VALUE_TABLES[Black.int][kind.int][sq] = ENDGAME_WEIGHTS[kind.int] + ENDGAME_PSQ_TABLES[kind.int][sq xor 56] + + +initializeTables() + + +func lowestEval*: Score {.inline.} = Score(-25_000) +func highestEval*: Score {.inline.} = Score(25_000) +func mateScore*: Score {.inline.} = lowestEval() + 1 proc getGamePhase(board: Chessboard): int = @@ -219,9 +221,33 @@ proc getGamePhase(board: Chessboard): int = result = min(24, result) -proc evaluatePiecePositions(board: ChessBoard): Score = - ## Returns the evaluation of the current - ## material's position relative to white +proc getPieceScore*(board: Chessboard, square: Square): Score = + ## Returns the value of the piece located at + ## the given square + let + piece = board.getPiece(square) + middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int] + endGameScore = ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int] + middleGamePhase = board.getGamePhase() + endGamePhase = 24 - middleGamePhase + + result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) + + +proc getPieceScore*(board: Chessboard, piece: Piece, square: Square): Score = + ## Returns the value the given piece would have if it + ## were at the given square + let + middleGameScore = MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int] + endGameScore = ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][square.int] + middleGamePhase = board.getGamePhase() + endGamePhase = 24 - middleGamePhase + + result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) + +proc evaluateMaterial(board: ChessBoard): Score = + ## Returns a material and position evaluation + ## for the current side to move let middleGamePhase = board.getGamePhase() endGamePhase = 24 - middleGamePhase @@ -232,8 +258,8 @@ proc evaluatePiecePositions(board: ChessBoard): Score = for sq in board.getOccupancy(): let piece = board.getPiece(sq) - middleGameScores[piece.color.int] += MIDDLEGAME_TABLES[piece.kind.int][sq.int] - endGameScores[piece.color.int] += ENDGAME_TABLES[piece.kind.int][sq.int] + middleGameScores[piece.color.int] += MIDDLEGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int] + endGameScores[piece.color.int] += ENDGAME_VALUE_TABLES[piece.color.int][piece.kind.int][sq.int] let sideToMove = board.position.sideToMove @@ -244,27 +270,35 @@ proc evaluatePiecePositions(board: ChessBoard): Score = result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) -proc evaluateMaterial(board: ChessBoard): Score = - ## Returns the material evaluation of the - ## current position relative to white (positive - ## if in white's favor, negative otherwise) - var - whiteScore: Score - blackScore: Score +proc evaluatePawnStructure(board: Chessboard): Score = + ## Evaluates the pawn structure of the current + ## position for the side to move + let + sideToMove = board.position.sideToMove + friendlyPawns = board.getOccupancyFor(sideToMove) - for sq in board.getOccupancyFor(White): - whiteScore += board.getPieceScore(sq) - - for sq in board.getOccupancyFor(Black): - blackScore += board.getPieceScore(sq) - - result = whiteScore - blackScore - if board.position.sideToMove == Black: - result *= -1 + # Doubled pawns are a bad idea + var doubledPawns = 0 + for file in 0..7: + if (getFileMask(file) and friendlyPawns).countSquares() > 1: + inc(doubledPawns) + + # Isolated pawns are also a bad idea + var isolatedPawns = 0 + for pawn in friendlyPawns: + let file = fileFromSquare(pawn) + var fileMask = getFileMask(file) + if file - 1 in 0..7: + fileMask = fileMask or getFileMask(file - 1) + if file + 1 in 0..7: + fileMask = fileMask or getFileMask(file + 1) + if (friendlyPawns and fileMask) == 0: + inc(isolatedPawns) + return DOUBLED_PAWNS_MALUS[doubledPawns] + ISOLATED_PAWN_MALUS[isolatedPawns] proc evaluate*(board: Chessboard): Score = ## Evaluates the current position result = board.evaluateMaterial() - result += board.evaluatePiecePositions() \ No newline at end of file + result += board.evaluatePawnStructure() \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 1857a6e..092ec4a 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -226,7 +226,7 @@ proc generateBishopMoves(self: Chessboard, moves: var MoveList, destinationMask: moves.add(createMove(square, target, Capture)) -proc generateKingMoves(self: Chessboard, moves: var MoveList) = +proc generateKingMoves(self: Chessboard, moves: var MoveList, capturesOnly=false) = let sideToMove = self.position.sideToMove king = self.getBitboard(King, sideToMove) @@ -235,9 +235,10 @@ proc generateKingMoves(self: Chessboard, moves: var MoveList) = enemyPieces = self.getOccupancyFor(nonSideToMove) and not self.getBitboard(King, nonSideToMove) bitboard = getKingAttacks(king.toSquare()) noKingOccupancy = occupancy and not king - for square in bitboard and not occupancy: - if not self.isOccupancyAttacked(square, noKingOccupancy): - moves.add(createMove(king, square)) + if not capturesOnly: + for square in bitboard and not occupancy: + if not self.isOccupancyAttacked(square, noKingOccupancy): + moves.add(createMove(king, square)) for square in bitboard and enemyPieces: if not self.isOccupancyAttacked(square, noKingOccupancy): moves.add(createMove(king, square, Capture)) @@ -271,12 +272,15 @@ proc generateCastling(self: Chessboard, moves: var MoveList) = moves.add(createMove(kingSquare, kingPiece.queenSideCastling(), Castle)) -proc generateMoves*(self: Chessboard, moves: var MoveList) = +proc generateMoves*(self: Chessboard, moves: var MoveList, capturesOnly: bool = false) = ## Generates the list of all possible legal moves - ## in the current position + ## in the current position. If capturesOnly is + ## true, only capture moves are generated - let sideToMove = self.position.sideToMove - self.generateKingMoves(moves) + let + sideToMove = self.position.sideToMove + nonSideToMove = sideToMove.opposite() + self.generateKingMoves(moves, capturesOnly) if self.position.checkers.countSquares() > 1: # King is in double check: no need to generate any more # moves @@ -304,8 +308,8 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) = let checker = self.position.checkers.lowestSquare() checkerBB = checker.toBitboard() - epTarget = self.position.enPassantSquare - checkerPiece = self.getPiece(checker) + # epTarget = self.position.enPassantSquare + # checkerPiece = self.getPiece(checker) destinationMask = getRayBetween(checker, self.getBitboard(King, sideToMove).toSquare()) or checkerBB # TODO: This doesn't really work. I've addressed the issue for now, but it's kinda ugly. Find a better # solution @@ -315,6 +319,10 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) = # # for most pieces, because the move generators won't allow them to move there, but it does matter # # for pawns # destinationMask = destinationMask or epTarget.toBitboard() + if capturesOnly: + # Note: This does not cover en passant (which is good because it's a capture, + # but the "fix" stands on flimsy ground) + destinationMask = destinationMask and self.getOccupancyFor(nonSideToMove) self.generatePawnMoves(moves, destinationMask) self.generateKnightMoves(moves, destinationMask) self.generateRookMoves(moves, destinationMask) @@ -427,7 +435,7 @@ proc doMove*(self: Chessboard, move: Move) = proc isLegal*(self: Chessboard, move: Move): bool {.inline.} = ## Returns whether the given move is legal - var moves = MoveList() + var moves = newMoveList() self.generateMoves(moves) return move in moves diff --git a/Chess/nimfish/nimfishpkg/moves.nim b/Chess/nimfish/nimfishpkg/moves.nim index d0e7a8b..a6ac363 100644 --- a/Chess/nimfish/nimfishpkg/moves.nim +++ b/Chess/nimfish/nimfishpkg/moves.nim @@ -193,4 +193,10 @@ func toAlgebraic*(self: Move): string = of PromoteToRook: result &= "r" else: - discard \ No newline at end of file + discard + + +proc newMoveList*: MoveList = + new(result) + for i in 0..result.data.high(): + result.data[i] = nullMove() \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/position.nim b/Chess/nimfish/nimfishpkg/position.nim index 3f1645d..946a927 100644 --- a/Chess/nimfish/nimfishpkg/position.nim +++ b/Chess/nimfish/nimfishpkg/position.nim @@ -30,7 +30,7 @@ type # Number of half-moves that were performed # to reach this position starting from the # root of the tree - plyFromRoot*: int8 + plyFromRoot*: uint8 # Number of half moves since # last piece capture or pawn movement. # Used for the 50-move rule diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index f8ae60c..6b130d6 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -33,6 +33,7 @@ type stopFlag*: Atomic[bool] # Can be used to cancel the search from another thread board: Chessboard bestMoveRoot: Move + bestRootScore: Score searchStart: MonoTime searchDeadline: MonoTime nodeCount: uint64 @@ -40,6 +41,7 @@ type searchMoves: seq[Move] previousBestMove: Move transpositionTable: TTable + currentExtensionCount: uint8 proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager = @@ -52,17 +54,37 @@ proc newSearchManager*(board: Chessboard, transpositions: TTable): SearchManager proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = ## Returns an estimated static score for the move result = Score(0) + let + sideToMove = self.board.position.sideToMove + nonSideToMove = sideToMove.opposite() if self.previousBestMove != nullMove() and move == self.previousBestMove: - result = highestEval() + 1 - elif move.isCapture(): + return highestEval() + 1 + if move.isCapture(): # Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker # We prioritize moves that capture the most valuable pieces, and as a # second goal we want to use our least valuable pieces to do so (this # is why we multiply the score of the captured piece by 100, to give # it priority) - result = 100 * self.board.getPieceScore(move.targetSquare) - - self.board.getPieceScore(move.startSquare) - + result += 100 * self.board.getPieceScore(move.targetSquare) - self.board.getPieceScore(move.startSquare) + if move.isPromotion(): + # Promotions are a good idea to search first + var piece: Piece + case move.getPromotionType(): + of PromoteToBishop: + piece = Piece(kind: Bishop, color: sideToMove) + of PromoteToKnight: + piece = Piece(kind: Knight, color: sideToMove) + of PromoteToRook: + piece = Piece(kind: Rook, color: sideToMove) + of PromoteToQueen: + piece = Piece(kind: Queen, color: sideToMove) + else: + discard # Unreachable + result += self.board.getPieceScore(piece, move.targetSquare) + if (self.board.getPawnAttacks(move.targetSquare, nonSideToMove) and self.board.getBitboard(Pawn, nonSideToMove)) != 0: + # Moving on a square attacked by an enemy pawn is _usually_ a very bad + # idea. Assume the piece is lost + result -= self.board.getPieceScore(move.startSquare) proc reorderMoves(self: SearchManager, moves: var MoveList) = @@ -71,8 +93,9 @@ proc reorderMoves(self: SearchManager, moves: var MoveList) = proc orderer(a, b: Move): int {.closure.} = return cmp(self.getEstimatedMoveScore(a), self.getEstimatedMoveScore(b)) - - moves.data.sort(orderer, SortOrder.Descending) + + # Ignore null moves beyond the lenght of the movelist + sort(moves.data.toOpenArray(0, moves.len - 1), orderer, SortOrder.Descending) proc timedOut(self: SearchManager): bool = getMonoTime() >= self.searchDeadline @@ -86,7 +109,7 @@ proc log(self: SearchManager, depth: int) = nps = 1000 * (self.nodeCount div max(elapsedMsec, 1)) var logMsg = &"info depth {depth} time {elapsedMsec} nodes {self.nodeCount} nps {nps}" if self.bestMoveRoot != nullMove(): - logMsg &= &" pv {self.bestMoveRoot.toAlgebraic()}" + logMsg &= &" bestmove {self.bestMoveRoot.toAlgebraic()} score {self.bestRootScore}" echo logMsg @@ -104,7 +127,74 @@ proc shouldStop(self: SearchManager): bool = return true -proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.discardable.} = +proc getSearchExtension(self: SearchManager, move: Move): int = + ## Returns the number of extensions that should be performed + ## when exploring the given move + if self.currentExtensionCount == 16: + return 0 + if self.board.inCheck(): + # Opponent is in check: extend the search to see + # if we can do other interesting things! + inc(self.currentExtensionCount) + return 1 + let piece = self.board.getPiece(move.targetSquare) + # If a pawn has just moved to its second-last rank, extend to + # see if a promotion would yield some good position + if piece.kind == Pawn: + let rank = if piece.color == White: getRankMask(1) else: getRankMask(6) + if (move.targetSquare.toBitboard() and rank) != 0: + inc(self.currentExtensionCount, 1) + return 1 + + +proc qsearch(self: SearchManager, ply: uint8, alpha, beta: Score): Score = + ## Negamax search with a/b pruning that is restricted to + ## capture moves (commonly called quiescent search). The + ## purpose of this extra search step is to mitigate the + ## so called horizon effect that stems from the fact that, + ## at some point, the engine will have to stop searching, possibly + ## thinking a bad move is good because it couldn't see far enough + ## ahead (this usually results in the engine blundering captures + ## or sacking pieces for apparently no reason: the reason is that it + ## did not look at the opponent's responses, because it stopped earlier. + ## That's the horizon). To address this, we look at all possible captures + ## in the current position and make sure that a position is not evaluated as + ## bad if only bad capture moves exist, if good non-capture moves do + if self.shouldStop(): + return + if ply == 127: + return Score(0) + let score = self.board.evaluate() + if score >= beta: + # Same as with the regular alpha-beta search + return score + var moves = newMoveList() + self.board.generateMoves(moves, capturesOnly=true) + self.reorderMoves(moves) + var bestScore = score + var alpha = max(alpha, score) + for move in moves: + self.board.doMove(move) + inc(self.nodeCount) + # Find the best move for us (worst move + # for our opponent, hence the negative sign) + var score = -self.qsearch(ply + 1, -beta, -alpha) + self.board.unmakeMove() + bestScore = max(score, bestScore) + if score >= beta: + # This move was too good for us, opponent will not search it + break + # When a search is cancelled or times out, we need + # to make sure the entire call stack unwinds back + # to the root move. This is why the check is duplicated + if self.shouldStop(): + return + if score > alpha: + alpha = score + return bestScore + + +proc search(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.discardable.} = ## Simple negamax search with alpha-beta pruning if self.shouldStop(): return @@ -120,8 +210,8 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {. if query.entry.score <= alpha: return query.entry.score if depth == 0: - return self.board.evaluate() - var moves = MoveList() + return self.qsearch(0, alpha, beta) + var moves = newMoveList() var depth = depth self.board.generateMoves(moves) self.reorderMoves(moves) @@ -136,46 +226,49 @@ proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {. # Stalemate return Score(0) var bestScore = lowestEval() - var alpha = alpha + var alpha = alpha for i, move in moves: if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves: continue self.board.doMove(move) + var extension = self.getSearchExtension(move) let zobrist = self.board.position.zobristKey inc(self.nodeCount) # Find the best move for us (worst move # for our opponent, hence the negative sign) - var score = -self.search(depth - 1, ply + 1, -beta, -alpha) - if self.board.position.repetitionDraw: + var score: Score + var fullDepth = true + if extension == 0 and i >= 3 and not move.isCapture(): + # Late Move Reduction: assume our move orderer did a good job, + # so it is not worth to look at all moves at the same depth equally. + # If this move turns out to be better than we expected, we'll re-search + # it at full depth + const reduction = 1 + score = -self.search(depth - 1 - reduction, ply + 1, -beta, -alpha) + fullDepth = score > alpha + if fullDepth: + score = -self.search(depth - 1 + extension, ply + 1, -beta, -alpha) + if self.board.position.halfMoveClock >= 100 or self.board.position.repetitionDraw: # Drawing by repetition is *bad* - score = lowestEval() div 2 + score = Score(0) self.board.unmakeMove() # When a search is cancelled or times out, we need - # to make sure the entire call stack unwindss back + # to make sure the entire call stack unwinds back # to the root move. This is why the check is duplicated if self.shouldStop(): return bestScore = max(score, bestScore) - if score >= beta: - # If we meet this position again, mark the fact that this score is a - # lower bound for the actual true score of the node (i.e. its score - # will NOT be lower than this) - self.transpositionTable.store(depth.uint8, score, zobrist, LowerBound) + let nodeType = if score >= beta: LowerBound elif score <= alpha: UpperBound else: Exact + self.transpositionTable.store(depth.uint8, score, zobrist, nodeType) + if nodeType == LowerBound: + # score >= beta # This move was too good for us, opponent will not search it break - if score <= alpha: - # If we meet this position again, mark the fact that this score is an - # upper bound for the actual true score of the node (i.e. its score - # will NOT be higher than this) - self.transpositionTable.store(depth.uint8, score, zobrist, UpperBound) - else: - # The position didn't cause any cutoffs, so the score stored here is - # the actual true score of the position - self.transpositionTable.store(depth.uint8, score, zobrist, Exact) if score > alpha: alpha = score if ply == 0: self.bestMoveRoot = move + self.bestRootScore = bestScore return bestScore @@ -202,16 +295,16 @@ proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes: # Search the previous best move first self.previousBestMove = self.bestMoveRoot self.search(i, 0, lowestEval(), highestEval()) - let shouldStop = self.shouldStop() - if shouldStop: - self.log(i - 1) - else: - self.log(i) # Since we always search the best move from the # previous iteration, we can use partial search # results: the engine will either not have changed # its mind, or it will have found an even better move # in the meantime, which we should obviously use! result = self.bestMoveRoot + let shouldStop = self.shouldStop() if shouldStop: - break + self.log(i - 1) + else: + self.log(i) + if shouldStop: + break \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/transpositions.nim b/Chess/nimfish/nimfishpkg/transpositions.nim index d3d124b..7fbef27 100644 --- a/Chess/nimfish/nimfishpkg/transpositions.nim +++ b/Chess/nimfish/nimfishpkg/transpositions.nim @@ -48,7 +48,8 @@ proc newTranspositionTable*(size: uint64): TTable = ## Initializes a new transposition table of ## size bytes new(result) - result.data = newSeq[TTEntry](size) + let numEntries = size div sizeof(TTEntry).uint64 + result.data = newSeq[TTEntry](numEntries) func getIndex(self: TTable, key: ZobristKey): uint64 = diff --git a/Chess/nimfish/nimfishpkg/tui.nim b/Chess/nimfish/nimfishpkg/tui.nim index df65718..42a8b0f 100644 --- a/Chess/nimfish/nimfishpkg/tui.nim +++ b/Chess/nimfish/nimfishpkg/tui.nim @@ -13,6 +13,7 @@ # limitations under the License. import movegen +import eval import uci @@ -28,12 +29,12 @@ type CountData = tuple[nodes: uint64, captures: uint64, castles: uint64, checks: uint64, promotions: uint64, enPassant: uint64, checkmates: uint64] -proc perft*(board: Chessboard, ply: int, verbose: bool = false, divide: bool = false, bulk: bool = false): CountData = +proc perft*(board: Chessboard, ply: int, verbose = false, divide = false, bulk = false, capturesOnly = false): CountData = ## Counts (and debugs) the number of legal positions reached after ## the given number of ply - var moves = MoveList() - board.generateMoves(moves) + var moves = newMoveList() + board.generateMoves(moves, capturesOnly=capturesOnly) if not bulk: if len(moves) == 0 and board.inCheck(): result.checkmates = 1 @@ -146,6 +147,7 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) = args = command[2].splitWhitespace() bulk = false verbose = false + captures = false if args.len() > 1: var ok = true for arg in args[1..^1]: @@ -154,6 +156,8 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) = bulk = true of "verbose": verbose = true + of "captures": + captures = true else: echo &"Error: go: perft: invalid argument '{args[1]}'" ok = false @@ -164,13 +168,13 @@ proc handleGoCommand(board: Chessboard, command: seq[string]) = let ply = parseInt(args[0]) if bulk: let t = cpuTime() - let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose).nodes + let nodes = board.perft(ply, divide=true, bulk=true, verbose=verbose, capturesOnly=captures).nodes let tot = cpuTime() - t echo &"\nNodes searched (bulk-counting: on): {nodes}" echo &"Time taken: {round(tot, 3)} seconds\nNodes per second: {round(nodes / tot).uint64}" else: let t = cpuTime() - let data = board.perft(ply, divide=true, verbose=verbose) + let data = board.perft(ply, divide=true, verbose=verbose, capturesOnly=captures) let tot = cpuTime() - t echo &"\nNodes searched (bulk-counting: off): {data.nodes}" echo &" - Captures: {data.captures}" @@ -327,6 +331,7 @@ const HELP_TEXT = """Nimfish help menu: Options: - bulk: Enable bulk-counting (significantly faster, gives less statistics) - verbose: Enable move debugging (for each and every move, not recommended on large searches) + - captures: Only generate capture moves Example: go perft 5 bulk - position: Get/set board position Subcommands: @@ -362,6 +367,8 @@ const HELP_TEXT = """Nimfish help menu: - uci: enter UCI mode - quit: exit - zobrist: Print the zobrist key for the current position + - eval: Evaluate the current position + - rep: Show whether this position is a draw by repetition """ @@ -450,6 +457,8 @@ proc commandLoop*: int = echo board.position.zobristKey.uint64 of "rep": echo board.position.repetitionDraw + of "eval": + echo &"Eval: {board.evaluate()}" else: echo &"Unknown command '{cmd[0]}'. Type 'help' for more information." except IOError: