diff --git a/Chess/nim.cfg b/Chess/nim.cfg index a3f6e07..69c3509 100644 --- a/Chess/nim.cfg +++ b/Chess/nim.cfg @@ -1,6 +1,6 @@ --cc:clang -o:"bin/nimfish" --d:danger +-d:debug --passL:"-flto -lmimalloc" --passC:"-flto -march=native -mtune=native" -d:useMalloc diff --git a/Chess/nimfish/nimfishpkg/search.nim b/Chess/nimfish/nimfishpkg/search.nim index ec7a327..a9d66ef 100644 --- a/Chess/nimfish/nimfishpkg/search.nim +++ b/Chess/nimfish/nimfishpkg/search.nim @@ -30,6 +30,7 @@ import threading/smartptrs type HistoryTable* = array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), array[Square(0)..Square(63), Score]]] + KillersTable* = seq[array[2, Move]] SearchManager* = ref object ## A simple state storage ## for our search @@ -46,15 +47,15 @@ type searchMoves: seq[Move] previousBestMove: Move transpositionTable: SharedPtr[TTable] - # Storage for our history euristic history: SharedPtr[HistoryTable] + killers: SharedPtr[KillersTable] proc newSearchManager*(board: Chessboard, transpositions: SharedPtr[TTable], stopFlag, searchFlag: SharedPtr[Atomic[bool]], - history: SharedPtr[HistoryTable]): SearchManager = + history: SharedPtr[HistoryTable], killers: SharedPtr[KillersTable]): SearchManager = new(result) result = SearchManager(board: board, bestMoveRoot: nullMove(), transpositionTable: transpositions, stopFlag: stopFlag, - searchFlag: searchFlag, history: history) + searchFlag: searchFlag, history: history, killers: killers) proc isSearching*(self: SearchManager): bool = @@ -71,19 +72,24 @@ proc stop*(self: SearchManager) = proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = - ## Returns an estimated static score for the move + ## Returns an estimated static score for the move used + ## during move ordering result = Score(0) let sideToMove = self.board.position.sideToMove nonSideToMove = sideToMove.opposite() + if self.previousBestMove != nullMove() and move == self.previousBestMove: return highestEval() + 1 + let query = self.transpositionTable[].get(self.board.position.zobristKey) if query.success and query.entry.bestMove != nullMove() and query.entry.bestMove == move: return highestEval() + 1 + if not move.isCapture(): # History euristic bonus result += self.history[][sideToMove][move.startSquare][move.targetSquare] + if move.isCapture(): # Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker # We prioritize moves that capture the most valuable pieces, and as a @@ -91,6 +97,7 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = # is why we multiply the score of the captured piece by 10, to give # it priority) result += 10 * self.board.position.getPieceScore(move.targetSquare) - self.board.position.getPieceScore(move.startSquare) + if move.isPromotion(): # Promotions are a good idea to search first var piece: Piece @@ -106,6 +113,7 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score = else: discard # Unreachable result += self.board.position.getPieceScore(piece, move.targetSquare) + if self.board.position.getPawnAttacks(move.targetSquare, nonSideToMove) != 0: # Moving on a square attacked by an enemy pawn is _usually_ a very bad # idea. Assume the piece is lost and give a malus based on the fact that @@ -342,8 +350,8 @@ proc findBestMove*(self: SearchManager, timeRemaining, increment: int64, maxDept # Apparently negative remaining time is a thing. Welp let - maxSearchTime = max(1, (timeRemaining div 20) + (increment div 2)) - softLimit = max(1, (timeRemaining div 30) + (increment div 2)) + maxSearchTime = max(1, (timeRemaining div 10) + (increment div 2)) + softLimit = max(1, maxSearchTime div 3) self.bestMoveRoot = nullMove() result = self.bestMoveRoot self.maxNodes = maxNodes @@ -357,6 +365,8 @@ proc findBestMove*(self: SearchManager, timeRemaining, increment: int64, maxDept self.searchFlag[].store(true) # Iterative deepening loop for i in 1..maxDepth: + if self.killers[].len() < i: + self.killers[].add([nullMove(), nullMove()]) # Search the previous best move first self.previousBestMove = self.bestMoveRoot self.search(i, 0, lowestEval(), highestEval()) @@ -369,17 +379,10 @@ proc findBestMove*(self: SearchManager, timeRemaining, increment: int64, maxDept # Soft time management: don't start a new search iteration # if the soft limit has expired, as it is unlikely to complete # anyway - when defined(softTM): - if self.shouldStop() or getMonoTime() >= self.softLimit: - self.log(i - 1) - break - else: - self.log(i) + if self.shouldStop() or getMonoTime() >= self.softLimit: + self.log(i - 1) + break else: - if self.shouldStop(): - self.log(i - 1) - break - else: - self.log(i) + self.log(i) self.searchFlag[].store(false) self.stopFlag[].store(false) diff --git a/Chess/nimfish/nimfishpkg/uci.nim b/Chess/nimfish/nimfishpkg/uci.nim index 5852dc8..3e79cca 100644 --- a/Chess/nimfish/nimfishpkg/uci.nim +++ b/Chess/nimfish/nimfishpkg/uci.nim @@ -31,13 +31,23 @@ type UCISession = object ## A UCI session debug: bool + # Previously reached positions history: seq[Position] + # The current position position: Position + # Atomic boolean flag to interrupt the search stopFlag: SharedPtr[Atomic[bool]] + # Atomic search flag used to know whether a search + # is in progress searchFlag: SharedPtr[Atomic[bool]] + # Size of the transposition table (in megabytes) hashTableSize: uint64 + # The transposition table transpositionTable: SharedPtr[TTable] + # Storage for our history heuristic historyTable: SharedPtr[HistoryTable] + # Storage for our killer move heuristic + killerMoves: SharedPtr[KillersTable] UCICommandType = enum ## A UCI command type enumeration @@ -305,14 +315,15 @@ proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.} ## Finds the best move in the current position setControlCHook(proc () {.noconv.} = quit(0)) + # Yes yes nim sure this isn't gcsafe. Now stfu and spawn a thread {.cast(gcsafe).}: - # Yes yes nim sure this isn't gcsafe. Now stfu and spawn a thread var session = args.session var board = newChessboard() board.position = session.position board.positions = session.history let command = args.command - var searcher = newSearchManager(board, session.transpositionTable, session.stopFlag, session.searchFlag, session.historyTable) + var searcher = newSearchManager(board, session.transpositionTable, session.stopFlag, + session.searchFlag, session.historyTable, session.killerMoves) var timeRemaining = (if session.position.sideToMove == White: command.wtime else: command.btime) increment = (if session.position.sideToMove == White: command.winc else: command.binc) @@ -339,6 +350,7 @@ proc startUCISession* = session.searchFlag = newSharedPtr(Atomic[bool]) session.transpositionTable[] = newTranspositionTable(session.hashTableSize * 1024 * 1024) session.historyTable = newSharedPtr(HistoryTable) + session.killerMoves = newSharedPtr(KillersTable) session.stopFlag[].store(false) # Fun fact, nim doesn't collect the memory of thread vars. Another stupid fucking design pitfall # of nim's AWESOME threading model. Someone is getting a pipebomb in their mailbox about this, mark