package gambit final case class SearchResult(bestMove: Int, score: Int, depth: Int, nodes: Long, pv: List[Int]) final class Searcher: import Searcher.* val tt = new TranspositionTable() var nodes: Long = 0L var useTt: Boolean = true var useNullMove: Boolean = true var onIteration: (Int, Int, Long, Long, List[Int]) => Unit = (_, _, _, _, _) => () private val maxPly = 128 private val killers = Array.ofDim[Int](maxPly, 2) private val historyTable = Array.ofDim[Int](2, 128 * 128) private val pathHashes = new Array[Long](4096) private val staticEvals = new Array[Int](maxPly + 4) private var historyLen = 0 var useLmr: Boolean = true var usePruning: Boolean = true private var deadline = 0L private var useTime = false @volatile private var stopped = false private var startNanos = 0L def stop(): Unit = stopped = true def setGameHistory(hashes: Array[Long], len: Int): Unit = var i = 0 while i < len do pathHashes(i) = hashes(i) i += 1 historyLen = len def wouldRepeat(pos: Position, ancestors: Array[Long]): Boolean = setGameHistory(ancestors, ancestors.length) pathHashes(historyLen) = pos.hash isRepetition(pos, 0) def search(pos: Position, maxDepth: Int, timeMs: Long): SearchResult = newSearch() useTime = timeMs > 0 startNanos = System.nanoTime() deadline = startNanos + timeMs * 1000000L val depthCap = math.min(maxDepth, maxPly - 1) var bestMove = 0 var bestScore = 0 var pv: List[Int] = Nil var completed = 0 var window = AspirationWindow var depth = 1 while depth <= depthCap && !stopped do val useAspiration = depth >= 4 val alpha0 = if useAspiration then bestScore - window else -Infinity val beta0 = if useAspiration then bestScore + window else Infinity var (score, move) = searchRoot(pos, depth, alpha0, beta0) if !stopped && (score <= alpha0 || score >= beta0) then val res = searchRoot(pos, depth, -Infinity, Infinity) score = res._1 move = res._2 if !stopped then bestMove = move bestScore = score completed = depth pv = extractPv(pos, depth) onIteration(depth, bestScore, nodes, elapsedMs(), pv) depth += 1 SearchResult(bestMove, bestScore, completed, nodes, pv) def bestMove(pos: Position, depth: Int): (Int, Int) = historyLen = 0 val r = search(pos, depth, 0) (r.bestMove, r.score) private def newSearch(): Unit = nodes = 0L stopped = false var p = 0 while p < maxPly do killers(p)(0) = 0 killers(p)(1) = 0 p += 1 java.util.Arrays.fill(historyTable(0), 0) java.util.Arrays.fill(historyTable(1), 0) private def elapsedMs(): Long = (System.nanoTime() - startNanos) / 1000000L private def searchRoot(pos: Position, depth: Int, alpha0: Int, beta: Int): (Int, Int) = pathHashes(historyLen) = pos.hash val moves = new MoveList() MoveGen.generateLegal(pos, moves) val ttMove = if useTt then tt.probeMove(pos.hash) else 0 orderMoves(pos, moves, ttMove, 0) var alpha = alpha0 var best = -Infinity var bestMove = if moves.size > 0 then moves(0) else 0 var i = 0 while i < moves.size && !stopped do val m = moves(i) val undo = pos.make(m) var score = if i == 0 then -negamax(pos, depth - 1, -beta, -alpha, 1) else val trial = -negamax(pos, depth - 1, -alpha - 1, -alpha, 1) if !stopped && trial > alpha && trial < beta then -negamax(pos, depth - 1, -beta, -alpha, 1) else trial pos.unmake(m, undo) if !stopped then if score > best then best = score bestMove = m if best > alpha then alpha = best if alpha >= beta then i = moves.size i += 1 if !stopped && useTt then val flag = if best <= alpha0 then TT.Upper else if best >= beta then TT.Lower else TT.Exact tt.store(pos.hash, depth, best, flag, bestMove, 0) (best, bestMove) private def negamax(pos: Position, depth0: Int, alpha0: Int, beta: Int, ply: Int): Int = nodes += 1 if useTime && (nodes & 2047L) == 0L && System.nanoTime() >= deadline then stopped = true if stopped then return 0 pathHashes(historyLen + ply) = pos.hash if isRepetition(pos, ply) || Draw.isFiftyMove(pos) then return DrawScore val pvNode = beta - alpha0 > 1 val side = pos.sideToMove val inChk = pos.inCheck(side) val depth = if inChk && ply < maxPly - 1 then depth0 + 1 else depth0 val ttScore = if useTt then tt.probe(pos.hash, depth, alpha0, beta, ply) else TT.Unknown val ttMove = if useTt then tt.hitMove else 0 if ttScore != TT.Unknown && !pvNode then return ttScore if depth <= 0 then return quiescence(pos, alpha0, beta, ply) val notMateBound = beta < TT.MateBound && beta > -TT.MateBound val staticEval = if inChk then -Infinity else Eval.evaluate(pos) staticEvals(ply) = staticEval val improving = !inChk && ply >= 2 && staticEval > staticEvals(ply - 2) if usePruning && !pvNode && !inChk && notMateBound then if depth <= SearchParams.reverseFutilityDepth && staticEval - SearchParams.reverseFutilityMargin * depth >= beta then return staticEval - SearchParams.reverseFutilityMargin * depth if depth <= SearchParams.razorDepth && staticEval + SearchParams.razorMargin(depth) <= alpha0 then val razorScore = quiescence(pos, alpha0, alpha0 + 1, ply) if razorScore <= alpha0 then return razorScore if useNullMove && !pvNode && !inChk && depth >= 3 && notMateBound && staticEval >= beta && hasNonPawnMaterial(pos, side) then val reduction = 2 + depth / 6 val nullState = makeNull(pos) val nullScore = -negamax(pos, depth - 1 - reduction, -beta, -beta + 1, ply + 1) unmakeNull(pos, nullState) if stopped then return 0 if nullScore >= beta then return beta val moves = new MoveList() MoveGen.generateLegal(pos, moves) if moves.size == 0 then if inChk then return -MateValue + ply else return 0 orderMoves(pos, moves, ttMove, ply) val futilityBase = staticEval + SearchParams.futilityMargin * depth var alpha = alpha0 var best = -Infinity var bestMove = 0 var cutoff = false var searched = 0 var i = 0 while i < moves.size && !cutoff do val m = moves(i) val quiet = !Move.isCapture(m) && !Move.isPromotion(m) val undo = pos.make(m) val givesCheck = pos.inCheck(pos.sideToMove) val prune = usePruning && !pvNode && !inChk && quiet && !givesCheck && searched > 0 && best > -TT.MateBound && ((depth <= SearchParams.lmpDepth && i >= SearchParams.lmpBase(if improving then 1 else 0)(depth)) || (depth <= SearchParams.futilityDepth && futilityBase <= alpha)) if prune then pos.unmake(m, undo) else val reduction = if useLmr && depth >= SearchParams.lmrMinDepth && searched >= SearchParams.lmrMinIndex && quiet && !inChk && !givesCheck then var r = SearchParams.lmrReduction(depth, searched) if !pvNode then r += 1 if m == killers(if ply < maxPly then ply else maxPly - 1)(0) then r -= 1 math.max(0, math.min(r, depth - 2)) else 0 val score = if searched == 0 then -negamax(pos, depth - 1, -beta, -alpha, ply + 1) else var trial = -negamax(pos, depth - 1 - reduction, -alpha - 1, -alpha, ply + 1) if !stopped && reduction > 0 && trial > alpha then trial = -negamax(pos, depth - 1, -alpha - 1, -alpha, ply + 1) if !stopped && trial > alpha && trial < beta then trial = -negamax(pos, depth - 1, -beta, -alpha, ply + 1) trial pos.unmake(m, undo) searched += 1 if stopped then return 0 if score > best then best = score bestMove = m if best > alpha then alpha = best if alpha >= beta then cutoff = true if quiet then recordKiller(ply, m) recordHistory(side, m, depth) i += 1 if searched == 0 then if inChk then return -MateValue + ply else return DrawScore if useTt then val flag = if best <= alpha0 then TT.Upper else if cutoff then TT.Lower else TT.Exact tt.store(pos.hash, depth, best, flag, bestMove, ply) best private def makeNull(pos: Position): (Int, Long) = val savedEp = pos.epSquare val savedHash = pos.hash if savedEp >= 0 then pos.hash ^= Zobrist.epFileKeys(Square.fileOf(savedEp)) pos.epSquare = -1 pos.sideToMove = pos.sideToMove ^ 1 pos.hash ^= Zobrist.sideKey (savedEp, savedHash) private def unmakeNull(pos: Position, state: (Int, Long)): Unit = pos.sideToMove = pos.sideToMove ^ 1 pos.epSquare = state._1 pos.hash = state._2 private def hasNonPawnMaterial(pos: Position, color: Int): Boolean = val board = pos.board var sq = 0 while sq < 128 do if Square.onBoard(sq) then val p = board(sq) if p != Piece.Empty && (p >> 3) == color then val pt = p & 7 if pt == Piece.Knight || pt == Piece.Bishop || pt == Piece.Rook || pt == Piece.Queen then return true sq += 1 false private def quiescence(pos: Position, alpha0: Int, beta: Int): Int = nodes += 1 val standPat = Eval.evaluate(pos) if standPat >= beta then return beta var alpha = math.max(alpha0, standPat) val moves = new MoveList() MoveGen.generatePseudo(pos, moves, capturesOnly = true) orderCaptures(pos, moves) val mover = pos.sideToMove var i = 0 while i < moves.size do val m = moves(i) val undo = pos.make(m) if pos.isAttacked(pos.kingSquare(mover), pos.sideToMove) then pos.unmake(m, undo) else val score = -quiescence(pos, -beta, -alpha) pos.unmake(m, undo) if score >= beta then return beta if score > alpha then alpha = score i += 1 alpha private def isRepetition(pos: Position, ply: Int): Boolean = val current = historyLen + ply val stop = math.max(0, current - pos.halfmove) var i = current - 2 while i >= stop do if pathHashes(i) == pos.hash then return true i -= 2 false private def recordKiller(ply: Int, m: Int): Unit = if ply < maxPly && killers(ply)(0) != m then killers(ply)(1) = killers(ply)(0) killers(ply)(0) = m private def recordHistory(side: Int, m: Int, depth: Int): Unit = historyTable(side)(Move.from(m) * 128 + Move.to(m)) += depth * depth private def orderMoves(pos: Position, moves: MoveList, ttMove: Int, ply: Int): Unit = val n = moves.size val scores = new Array[Int](n) val side = pos.sideToMove val kply = if ply < maxPly then ply else maxPly - 1 val k0 = killers(kply)(0) val k1 = killers(kply)(1) var i = 0 while i < n do val m = moves(i) scores(i) = if m == ttMove && ttMove != 0 then 2000000 else if Move.isCapture(m) then 1000000 + mvvLva(pos, m) else if Move.isPromotion(m) then 900000 + Eval.pieceValue(Move.promo(m)) else if m == k0 && k0 != 0 then 800000 else if m == k1 && k1 != 0 then 700000 else historyTable(side)(Move.from(m) * 128 + Move.to(m)) i += 1 sortByScores(moves, scores) private def orderCaptures(pos: Position, moves: MoveList): Unit = val n = moves.size val scores = new Array[Int](n) var i = 0 while i < n do val m = moves(i) scores(i) = if Move.isCapture(m) then mvvLva(pos, m) else if Move.isPromotion(m) then Eval.pieceValue(Move.promo(m)) else 0 i += 1 sortByScores(moves, scores) private def sortByScores(moves: MoveList, scores: Array[Int]): Unit = val n = moves.size var i = 0 while i < n do var bestIdx = i var j = i + 1 while j < n do if scores(j) > scores(bestIdx) then bestIdx = j j += 1 if bestIdx != i then val tmpMove = moves(i) moves(i) = moves(bestIdx) moves(bestIdx) = tmpMove val tmpScore = scores(i) scores(i) = scores(bestIdx) scores(bestIdx) = tmpScore i += 1 private def mvvLva(pos: Position, m: Int): Int = val victim = if Move.isEnPassant(m) then Piece.Pawn else Piece.typeOf(pos.board(Move.to(m))) val attacker = Piece.typeOf(pos.board(Move.from(m))) Eval.pieceValue(victim) * 16 - Eval.pieceValue(attacker) private def extractPv(pos: Position, maxLen: Int): List[Int] = val out = List.newBuilder[Int] val trail = scala.collection.mutable.ArrayDeque[(Int, Undo)]() var steps = 0 var continue = true while continue && steps < maxLen do val m = tt.probeMove(pos.hash) if m == 0 || !isLegal(pos, m) then continue = false else out += m trail.append((m, pos.make(m))) steps += 1 while trail.nonEmpty do val (m, u) = trail.removeLast() pos.unmake(m, u) out.result() private def isLegal(pos: Position, m: Int): Boolean = val legal = new MoveList() MoveGen.generateLegal(pos, legal) var i = 0 while i < legal.size do if legal(i) == m then return true i += 1 false object Searcher: val MateValue = 30000 val Infinity = 1000000 val DrawScore = 0 val AspirationWindow = 25