From 720645092e94d643387e1e2246dc850170d6770d Mon Sep 17 00:00:00 2001 From: Mattia Giambirtone Date: Thu, 25 Apr 2024 15:20:55 +0200 Subject: [PATCH] Massive eval improvement. Perform search in a separate thread. Implement more UCI commands --- Chess/nim.cfg | 1 + Chess/nimfish/nimfishpkg/board.nim | 2 +- Chess/nimfish/nimfishpkg/eval.nim | 228 +++++++++++++++++++++++++-- Chess/nimfish/nimfishpkg/movegen.nim | 2 +- Chess/nimfish/nimfishpkg/moves.nim | 5 +- Chess/nimfish/nimfishpkg/search.nim | 123 +++++++++++++-- Chess/nimfish/nimfishpkg/uci.nim | 147 +++++++++++------ 7 files changed, 429 insertions(+), 79 deletions(-) diff --git a/Chess/nim.cfg b/Chess/nim.cfg index b95609e..4c13138 100644 --- a/Chess/nim.cfg +++ b/Chess/nim.cfg @@ -3,3 +3,4 @@ -d:danger --passL:"-flto" --passC:"-Ofast -flto -march=native -mtune=native" +--threads:on \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/board.nim b/Chess/nimfish/nimfishpkg/board.nim index 51c731b..fbe064f 100644 --- a/Chess/nimfish/nimfishpkg/board.nim +++ b/Chess/nimfish/nimfishpkg/board.nim @@ -222,7 +222,7 @@ proc removePiece*(self: Chessboard, square: Square) = ## Removes a piece from the board, updating necessary ## metadata when not defined(danger): - let Piece = self.getPiece(square) + let piece = self.getPiece(square) doAssert piece.kind != Empty and piece.color != None, self.toFEN() self.removePieceFromBitboard(square) self.grid[square] = nullPiece() diff --git a/Chess/nimfish/nimfishpkg/eval.nim b/Chess/nimfish/nimfishpkg/eval.nim index 0da4a5a..8bea3ea 100644 --- a/Chess/nimfish/nimfishpkg/eval.nim +++ b/Chess/nimfish/nimfishpkg/eval.nim @@ -16,7 +16,161 @@ import board type - Score* = int16 + Score* = int32 + + +# Stolen from https://www.chessprogramming.org/PeSTO's_Evaluation_Function +const + PAWN_MIDDLEGAME_SCORES: array[64, Score] = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 98, 134, 61, 95, 68, 126, 34, -11, + -6, 7, 26, 31, 65, 56, 25, -20, + -14, 13, 6, 21, 23, 12, 17, -23, + -27, -2, -5, 12, 17, 6, 10, -25, + -26, -4, -4, -10, 3, 3, 33, -12, + -35, -1, -20, -23, -15, 24, 38, -22, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + + PAWN_ENDGAME_SCORES: array[64, Score] = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 178, 173, 158, 134, 147, 132, 165, 187, + 94, 100, 85, 67, 56, 53, 82, 84, + 32, 24, 13, 5, -2, 4, 17, 17, + 13, 9, -3, -7, -7, -8, 3, -1, + 4, 7, -6, 1, 0, -5, -1, -8, + 13, 8, 8, 10, 13, 0, 2, -7, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + + KNIGHT_MIDDLEGAME_SCORES: array[64, Score] = [ + -167, -89, -34, -49, 61, -97, -15, -107, + -73, -41, 72, 36, 23, 62, 7, -17, + -47, 60, 37, 65, 84, 129, 73, 44, + -9, 17, 19, 53, 37, 69, 18, 22, + -13, 4, 16, 13, 28, 19, 21, -8, + -23, -9, 12, 10, 19, 17, 25, -16, + -29, -53, -12, -3, -1, 18, -14, -19, + -105, -21, -58, -33, -17, -28, -19, -23, + ] + + KNIGHT_ENDGAME_SCORES: array[64, Score] = [ + -58, -38, -13, -28, -31, -27, -63, -99, + -25, -8, -25, -2, -9, -25, -24, -52, + -24, -20, 10, 9, -1, -9, -19, -41, + -17, 3, 22, 22, 22, 11, 8, -18, + -18, -6, 16, 25, 16, 17, 4, -18, + -23, -3, -1, 15, 10, -3, -20, -22, + -42, -20, -10, -5, -2, -20, -23, -44, + -29, -51, -23, -15, -22, -18, -50, -64, + ] + + BISHOP_MIDDLEGAME_SCORES: array[64, Score] = [ + -29, 4, -82, -37, -25, -42, 7, -8, + -26, 16, -18, -13, 30, 59, 18, -47, + -16, 37, 43, 40, 35, 50, 37, -2, + -4, 5, 19, 50, 37, 37, 7, -2, + -6, 13, 13, 26, 34, 12, 10, 4, + 0, 15, 15, 15, 14, 27, 18, 10, + 4, 15, 16, 0, 7, 21, 33, 1, + -33, -3, -14, -21, -13, -12, -39, -21, + ] + + BISHOP_ENDGAME_SCORES: array[64, Score] = [ + -14, -21, -11, -8, -7, -9, -17, -24, + -8, -4, 7, -12, -3, -13, -4, -14, + 2, -8, 0, -1, -2, 6, 0, 4, + -3, 9, 12, 9, 14, 10, 3, 2, + -6, 3, 13, 19, 7, 10, -3, -9, + -12, -3, 8, 10, 13, 3, -7, -15, + -14, -18, -7, -1, 4, -9, -15, -27, + -23, -9, -23, -5, -9, -16, -5, -17, + ] + + ROOK_MIDDLEGAME_SCORES: array[64, Score] = [ + 32, 42, 32, 51, 63, 9, 31, 43, + 27, 32, 58, 62, 80, 67, 26, 44, + -5, 19, 26, 36, 17, 45, 61, 16, + -24, -11, 7, 26, 24, 35, -8, -20, + -36, -26, -12, -1, 9, -7, 6, -23, + -45, -25, -16, -17, 3, 0, -5, -33, + -44, -16, -20, -9, -1, 11, -6, -71, + -19, -13, 1, 17, 16, 7, -37, -26, + ] + + ROOK_ENDGAME_SCORES: array[64, Score] = [ + 13, 10, 18, 15, 12, 12, 8, 5, + 11, 13, 13, 11, -3, 3, 8, 3, + 7, 7, 7, 5, 4, -3, -5, -3, + 4, 3, 13, 1, 2, 1, -1, 2, + 3, 5, 8, 4, -5, -6, -8, -11, + -4, 0, -5, -1, -7, -12, -8, -16, + -6, -6, 0, 2, -9, -9, -11, -3, + -9, 2, 3, -1, -5, -13, 4, -20, + ] + + QUEEN_MIDDLEGAME_SCORES: array[64, Score] = [ + -28, 0, 29, 12, 59, 44, 43, 45, + -24, -39, -5, 1, -16, 57, 28, 54, + -13, -17, 7, 8, 29, 56, 47, 57, + -27, -27, -16, -16, -1, 17, -2, 1, + -9, -26, -9, -10, -2, -4, 3, -3, + -14, 2, -11, -2, -5, 2, 14, 5, + -35, -8, 11, 2, 8, 15, -3, 1, + -1, -18, -9, 10, -15, -25, -31, -50, + ] + + QUEEN_ENDGAME_SCORES: array[64, Score] = [ + -9, 22, 22, 27, 27, 19, 10, 20, + -17, 20, 32, 41, 58, 25, 30, 0, + -20, 6, 9, 49, 47, 35, 19, 9, + 3, 22, 24, 45, 57, 40, 57, 36, + -18, 28, 19, 47, 31, 34, 39, 23, + -16, -27, 15, 6, 9, 17, 10, 5, + -22, -23, -30, -16, -16, -23, -36, -32, + -33, -28, -22, -43, -5, -32, -20, -41, + ] + + KING_MIDDLEGAME_SCORES: array[64, Score] = [ + -65, 23, 16, -15, -56, -34, 2, 13, + 29, -1, -20, -7, -8, -4, -38, -29, + -9, 24, 2, -16, -20, 6, 22, -22, + -17, -20, -12, -27, -30, -25, -14, -36, + -49, -1, -27, -39, -46, -44, -33, -51, + -14, -14, -22, -46, -44, -30, -15, -27, + 1, 7, -8, -64, -43, -16, 9, 8, + -15, 36, 12, -54, 8, -28, 24, 14, + ] + + KING_ENDGAME_SCORES: array[64, Score] = [ + -74, -35, -18, -18, -11, 15, 4, -17, + -12, 17, 14, 17, 17, 38, 23, 11, + 10, 17, 23, 15, 20, 45, 44, 13, + -8, 22, 24, 27, 26, 33, 26, 3, + -18, -4, 21, 24, 27, 23, 9, -11, + -19, -3, 11, 21, 23, 16, 7, -9, + -27, -11, 4, 13, 14, 4, -5, -17, + -53, -34, -21, -11, -28, -14, -24, -43 + ] + + MIDDLEGAME_TABLES: array[6, array[64, Score]] = [ + BISHOP_MIDDLEGAME_SCORES, + KING_MIDDLEGAME_SCORES, + KNIGHT_MIDDLEGAME_SCORES, + PAWN_MIDDLEGAME_SCORES, + QUEEN_MIDDLEGAME_SCORES, + ROOK_MIDDLEGAME_SCORES + ] + + ENDGAME_TABLES: array[6, array[64, Score]] = [ + BISHOP_ENDGAME_SCORES, + KING_ENDGAME_SCORES, + KNIGHT_ENDGAME_SCORES, + PAWN_ENDGAME_SCORES, + QUEEN_ENDGAME_SCORES, + ROOK_ENDGAME_SCORES + ] + proc getPieceValue(kind: PieceKind): Score = @@ -36,12 +190,68 @@ proc getPieceValue(kind: PieceKind): Score = discard -proc getPieceScore(board: Chessboard, square: Square): Score = - ## Returns the value of the piece located at +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 getEstimatedMoveScore*(board: Chessboard, move: Move): Score = + ## Returns an estimated static score for the move + result = Score(0) + 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 piece of the captured score by 100, to give + # it priority) + result = 100 * board.getPieceScore(move.targetSquare) - + board.getPieceScore(move.startSquare) + + +proc getGamePhase(board: Chessboard): int = + ## Computes the game phase according to + ## how many pieces are left on the board + result = 0 + for sq in board.getOccupancy(): + case board.getPiece(sq).kind: + of Bishop, Knight: + inc(result) + of Queen: + inc(result, 4) + of Rook: + inc(result, 2) + else: + discard + # Caps the value in case of early + # promotions + result = min(24, result) + + +proc evaluatePiecePositions(board: ChessBoard): Score = + ## Returns the evaluation of the current + ## material's position relative to white + let + middleGamePhase = board.getGamePhase() + endGamePhase = 24 - middleGamePhase + var + # White, Black + middleGameScores: array[2, Score] = [0, 0] + endGameScores: array[2, Score] = [0, 0] + + 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] + + let + sideToMove = board.position.sideToMove + nonSideToMove = sideToMove.opposite() + middleGameScore = middleGameScores[sideToMove.int] - middleGameScores[nonSideToMove.int] + endGameScore = endGameScores[sideToMove.int] - endGameScores[nonSideToMove.int] + + result = Score((middleGameScore * middleGamePhase + endGameScore * endGamePhase) div 24) + proc evaluateMaterial(board: ChessBoard): Score = ## Returns the material evaluation of the @@ -50,20 +260,20 @@ proc evaluateMaterial(board: ChessBoard): Score = var whiteScore: Score blackScore: Score - + 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 - + proc evaluate*(board: Chessboard): Score = ## Evaluates the current position - - result = board.evaluateMaterial() \ No newline at end of file + result = board.evaluateMaterial() + result += board.evaluatePiecePositions() \ No newline at end of file diff --git a/Chess/nimfish/nimfishpkg/movegen.nim b/Chess/nimfish/nimfishpkg/movegen.nim index 0e7107b..70bac34 100644 --- a/Chess/nimfish/nimfishpkg/movegen.nim +++ b/Chess/nimfish/nimfishpkg/movegen.nim @@ -285,7 +285,7 @@ proc generateMoves*(self: Chessboard, moves: var MoveList) = # King is in double check: no need to generate any more # moves return - + self.generateCastling(moves) # We pass a mask to our move generators to remove stuff diff --git a/Chess/nimfish/nimfishpkg/moves.nim b/Chess/nimfish/nimfishpkg/moves.nim index b99828e..d0e7a8b 100644 --- a/Chess/nimfish/nimfishpkg/moves.nim +++ b/Chess/nimfish/nimfishpkg/moves.nim @@ -40,11 +40,12 @@ type targetSquare*: Square flags*: uint16 - MoveList* = object + MoveList* = ref object ## A list of moves - data: array[218, Move] + data*: array[218, Move] len: int8 + func `[]`*(self: MoveList, i: SomeInteger): Move = when not defined(danger): if i >= self.len: diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index 6200b91..dfaa054 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -18,12 +18,16 @@ import movegen import eval +import std/times import std/atomics +import std/algorithm +import std/monotimes +import std/strformat -func lowestEval*: Score {.inline.} = Score(-32000'i16) -func highestEval*: Score {.inline.} = Score(32000'i16) -func mateScore*: Score {.inline.} = lowestEval() - Score(1) +func lowestEval*: Score {.inline.} = Score(int32.low() + 1_000_000) +func highestEval*: Score {.inline.} = Score(int32.high() - 1_000_000) +func mateScore*: Score {.inline.} = lowestEval() type @@ -31,20 +35,72 @@ type ## A simple state storage ## for our search stopFlag*: Atomic[bool] # Can be used to cancel the search from another thread - bestMove*: Move + board: Chessboard + bestMoveRoot: Move + searchStart: MonoTime + searchDeadline: MonoTime + nodeCount: uint64 + maxNodes: uint64 + searchMoves: seq[Move] -proc search*(self: SearchManager, board: Chessboard, depth, ply: int, alpha, beta: Score): Score {.discardable.} = - ## Simple negamax search with alpha-beta pruning - if self.stopFlag.load(): +proc newSearchManager*(board: Chessboard): SearchManager = + new(result) + result.board = board + result.bestMoveRoot = nullMove() + result.searchMoves = @[] + + +proc reorderMoves(self: SearchManager, moves: var MoveList) = + ## Reorders the list of moves in-place, trying + ## to place the best ones first + + proc orderer(a, b: Move): int {.closure.} = + return cmp(self.board.getEstimatedMoveScore(a), self.board.getEstimatedMoveScore(b)) + + moves.data.sort(orderer, SortOrder.Descending) + + +proc timedOut(self: SearchManager): bool = getMonoTime() >= self.searchDeadline +proc cancelled(self: SearchManager): bool = self.stopFlag.load() + + +proc log(self: SearchManager, depth: int) = + let + elapsed = getMonoTime() - self.searchStart + elapsedMsec = elapsed.inMilliseconds.uint64 + 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()}" + echo logMsg + + +proc shouldStop(self: SearchManager): bool = + ## Returns whether searching should + ## stop + if self.cancelled(): # Search has been cancelled! + return true + if self.timedOut(): + # We ran out of time! + return true + if self.maxNodes > 0 and self.nodeCount >= self.maxNodes: + # Ran out of nodes + return true + + +proc search*(self: SearchManager, depth, ply: int, alpha, beta: Score): Score {.discardable.} = + ## Simple negamax search with alpha-beta pruning + if self.shouldStop(): return if depth == 0: - return board.evaluate() + return self.board.evaluate() var moves = MoveList() - board.generateMoves(moves) + self.board.generateMoves(moves) + self.reorderMoves(moves) if moves.len() == 0: - if board.inCheck(): + if self.board.inCheck(): # Checkmate! We add the current ply # because mating in 3 is better than # mating in 5 (and conversely being @@ -55,13 +111,20 @@ proc search*(self: SearchManager, board: Chessboard, depth, ply: int, alpha, bet return Score(0) var bestScore = lowestEval() var alpha = alpha - for move in moves: - board.makeMove(move) + for i, move in moves: + if ply == 0 and self.searchMoves.len() > 0 and move notin self.searchMoves: + continue + self.board.makeMove(move) + inc(self.nodeCount) # Find the best move for us (worst move # for our opponent, hence the negative sign) - let eval = -self.search(board, depth - 1, ply + 1, -beta, -alpha) - board.unmakeMove() - + let eval = -self.search(depth - 1, ply + 1, -beta, -alpha) + self.board.unmakeMove() + # When a search is cancelled or times out, we need + # to make sure the entire call stack unwindss back + # to the root move. This is why the check is duplicated + if self.shouldStop(): + return bestScore = max(eval, bestScore) if eval >= beta: # This move was too good for us, opponent will not search it @@ -69,6 +132,32 @@ proc search*(self: SearchManager, board: Chessboard, depth, ply: int, alpha, bet if eval > alpha: alpha = eval if ply == 0: - self.bestMove = move + self.bestMoveRoot = move + return bestScore - return bestScore \ No newline at end of file + +proc findBestMove*(self: SearchManager, maxSearchTime, maxDepth: int, maxNodes: uint64, searchMoves: seq[Move]): Move = + ## Finds the best move in the current position + ## and returns it, limiting search time to + ## maxSearchTime milliseconds and to maxDepth + ## ply (if maxDepth is -1, a reasonable limit + ## is picked). If maxNodes is supplied and is nonzero, + ## search will stop once it has analyzed the given number + ## of nodes. If searchMoves is provided and is not empty, + ## search will be restricted to the moves in the list + self.bestMoveRoot = nullMove() + result = self.bestMoveRoot + self.maxNodes = maxNodes + self.searchMoves = searchMoves + self.searchStart = getMonoTime() + self.searchDeadline = self.searchStart + initDuration(milliseconds=maxSearchTime) + var maxDepth = maxDepth + if maxDepth == -1: + maxDepth = 30 + # Iterative deepening loop + for i in 1..maxDepth: + self.search(i, 0, lowestEval(), highestEval()) + self.log(i) + if self.shouldStop(): + break + result = self.bestMoveRoot diff --git a/Chess/nimfish/nimfishpkg/uci.nim b/Chess/nimfish/nimfishpkg/uci.nim index f213daf..8bbf3a8 100644 --- a/Chess/nimfish/nimfishpkg/uci.nim +++ b/Chess/nimfish/nimfishpkg/uci.nim @@ -15,10 +15,7 @@ ## Implementation of a UCI compatible server import std/strutils import std/strformat -import std/random - - -randomize() +import std/atomics import board @@ -60,44 +57,13 @@ type movesToGo: int depth: int moveTime: int + nodes: uint64 + searchmoves: seq[Move] else: discard -proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand = - result = UCICommand(kind: Go) - result.wtime = -1 - result.btime = -1 - result.winc = 0 - result.binc = 0 - result.movesToGo = 0 - result.depth = 0 - result.moveTime = -1 - var - current = 0 - while current < command.len(): - case command[current]: - of "infinite": - discard - of "wtime": - result.wtime = command[current + 1].parseInt() - of "btime": - result.btime = command[current + 1].parseInt() - of "winc": - result.winc = command[current + 1].parseInt() - of "binc": - result.binc = command[current + 1].parseInt() - of "result.movestogo": - result.movesToGo = command[current + 1].parseInt() - of "depth": - result.depth = command[current + 1].parseInt() - of "movetime": - result.moveTime = command[current + 1].parseInt() - else: - return - - -proc handleUCIMove(session: UCISession, move: string): tuple[move: Move, cmd: UCICommand] {.discardable.} = +proc parseUCIMove(session: UCISession, move: string): tuple[move: Move, command: UCICommand] = var startSquare: Square targetSquare: Square @@ -141,10 +107,68 @@ proc handleUCIMove(session: UCISession, move: string): tuple[move: Move, cmd: UC flags.add(Castle) elif targetSquare == session.board.position.enPassantSquare: flags.add(EnPassant) - let move = createMove(startSquare, targetSquare, flags) + result.move = createMove(startSquare, targetSquare, flags) + + +proc handleUCIMove(session: UCISession, move: string): tuple[move: Move, cmd: UCICommand] {.discardable.} = if session.debug: echo &"info string making move {move}" - result.move = session.board.makeMove(move) + let + r = session.parseUCIMove(move) + move = r.move + command = r.command + if move == nullMove(): + return (move, command) + else: + result.move = session.board.makeMove(move) + + +proc handleUCIGoCommand(session: UCISession, command: seq[string]): UCICommand = + result = UCICommand(kind: Go) + result.wtime = 0 + result.btime = 0 + result.winc = 0 + result.binc = 0 + result.movesToGo = 0 + result.depth = -1 + result.moveTime = -1 + result.nodes = 0 + var + current = 1 # Skip the "go" + while current < command.len(): + let flag = command[current] + inc(current) + case flag: + of "infinite": + result.wtime = int32.high() + result.btime = int32.high() + of "wtime": + result.wtime = command[current].parseInt() + of "btime": + result.btime = command[current].parseInt() + of "winc": + result.winc = command[current].parseInt() + of "binc": + result.binc = command[current].parseInt() + of "movestogo": + result.movesToGo = command[current].parseInt() + of "depth": + result.depth = command[current].parseInt() + of "movetime": + result.moveTime = command[current].parseInt() + of "nodes": + result.nodes = command[current].parseBiggestUInt() + of "searchmoves": + while current < command.len(): + inc(current) + if command[current] == "": + break + let move = session.parseUCIMove(command[current]).move + if move == nullMove(): + return UCICommand(kind: Unknown, reason: &"invalid move '{command[current]}' for searchmoves") + result.searchmoves.add(move) + else: + discard proc handleUCIPositionCommand(session: UCISession, command: seq[string]): UCICommand = @@ -244,15 +268,36 @@ proc parseUCICommand(session: UCISession, command: string): UCICommand = inc(current) -proc bestMove(session: UCISession, command: UCICommand) = +proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.} = ## Finds the best move in the current position - session.searching = true - session.currentSearch = SearchManager() - session.currentSearch.search(session.board, 6, 0, lowestEval(), highestEval()) - session.searching = false - let move = session.currentSearch.bestMove - echo &"bestmove {move.toAlgebraic()}" - + {.cast(gcsafe).}: + setControlCHook(proc () {.noconv.} = quit(0)) + # Yes yes nim sure this isn't gcsafe. Now stfu and spawn a thread + var session = args.session + var command = args.command + session.searching = true + session.currentSearch = newSearchManager(session.board) + session.currentSearch.stopFlag.store(false) + var + timeRemaining = (if session.board.position.sideToMove == White: command.wtime else: command.btime) + increment = (if session.board.position.sideToMove == White: command.winc else: command.binc) + maxTime = (timeRemaining div 20) + (increment div 2) + # Buffer to avoid loosing on time + if maxTime == 0: + maxTime = int32.high() + else: + maxTime -= 100 + if command.moveTime != -1: + maxTime = command.moveTime + if session.debug: + echo &"info string starting search to depth {command.depth} for at most {maxTime} ms and {command.nodes} nodes" + if session.debug and command.searchmoves.len() > 0: + echo &"""info string restricting search to: {command.searchmoves.join(" ")}""" + var move = session.currentSearch.findBestMove(maxTime, command.depth, command.nodes, command.searchmoves) + session.searching = false + echo &"bestmove {move.toAlgebraic()}" + + proc startUCISession* = ## Begins listening for UCI commands @@ -287,7 +332,11 @@ proc startUCISession* = of NewGame: session.board = newDefaultChessboard() of Go: - session.bestMove(cmd) + var thread: Thread[tuple[session: UCISession, command: UCICommand]] + createThread(thread, bestMove, (session, cmd)) + of Stop: + if session.searching: + session.currentSearch.stopFlag.store(true) of Position: discard else: