use crate::eval::EvalHash;
use crate::time::Instant;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::atomic::AtomicU64;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use super::time_manager::{
calculate_falling_eval, calculate_time_reduction, normalize_nodes_effort,
DEFAULT_MAX_MOVES_TO_DRAW,
};
use super::{LimitsType, RootMove, SearchWorker, Skill, SkillOptions, ThreadPool, TimeManagement};
use crate::position::Position;
use crate::tt::TranspositionTable;
use crate::types::{Depth, Move, Value, MAX_PLY};
#[derive(Debug, Clone)]
pub struct SearchInfo {
pub depth: Depth,
pub sel_depth: i32,
pub score: Value,
pub nodes: u64,
pub time_ms: u64,
pub nps: u64,
pub hashfull: u32,
pub pv: Vec<Move>,
pub multi_pv: usize,
}
impl SearchInfo {
pub fn to_usi_string(&self) -> String {
let score_str =
if self.score.is_mate_score() && self.score.raw().abs() < Value::INFINITE.raw() {
let mate_ply = self.score.mate_ply();
let signed_ply = if self.score.is_loss() {
-mate_ply
} else {
mate_ply
};
format!("mate {signed_ply}")
} else {
format!("cp {}", self.score.raw())
};
let mut s = format!(
"info depth {depth} seldepth {sel_depth} multipv {multi_pv} score {score} nodes {nodes} time {time_ms} nps {nps} hashfull {hashfull}",
depth = self.depth,
sel_depth = self.sel_depth,
multi_pv = self.multi_pv,
score = score_str,
nodes = self.nodes,
time_ms = self.time_ms,
nps = self.nps,
hashfull = self.hashfull
);
if !self.pv.is_empty() {
s.push_str(" pv");
for m in &self.pv {
s.push(' ');
s.push_str(&m.to_usi());
}
}
s
}
}
pub(crate) fn compute_aspiration_window(rm: &RootMove, thread_id: usize) -> (Value, Value, Value) {
let fallback = {
let inf = Value::INFINITE.raw() as i64;
inf * inf
};
let mean_sq = rm.mean_squared_score.unwrap_or(fallback).abs();
let mean_sq = mean_sq.min((Value::INFINITE.raw() as i64) * (Value::INFINITE.raw() as i64));
let thread_offset = (thread_id % 8) as i32;
let score_factor = rm.average_score.raw().abs() / 9000;
let delta_raw =
5 + thread_offset + score_factor + (mean_sq / 11131).min(i32::MAX as i64) as i32;
let delta = Value::new(delta_raw);
let alpha_raw = (rm.average_score.raw() - delta.raw()).max(-Value::INFINITE.raw());
let beta_raw = (rm.average_score.raw() + delta.raw()).min(Value::INFINITE.raw());
(Value::new(alpha_raw), Value::new(beta_raw), delta)
}
#[inline]
fn proven_mate_depth_exceeded(best_value: Value, depth: Depth) -> bool {
if best_value.is_win() || best_value.is_loss() {
let mate_ply = best_value.mate_ply();
return (mate_ply + 2) * 5 / 2 < depth;
}
false
}
#[inline]
fn mate_within_limit(
best_value: Value,
score_lower_bound: bool,
score_upper_bound: bool,
mate_limit_moves: i32,
) -> bool {
if mate_limit_moves <= 0
|| score_lower_bound
|| score_upper_bound
|| !best_value.is_mate_score()
{
return false;
}
let mate_ply = best_value.mate_ply() as i64;
let limit_plies = (mate_limit_moves as i64).saturating_mul(2);
mate_ply <= limit_plies
}
#[derive(Debug, Clone)]
pub struct SearchResult {
pub best_move: Move,
pub ponder_move: Move,
pub score: Value,
pub depth: Depth,
pub nodes: u64,
pub pv: Vec<Move>,
}
pub const DEFAULT_EVAL_HASH_SIZE_MB: usize = 256;
pub struct Search {
tt: Arc<TranspositionTable>,
eval_hash: Arc<EvalHash>,
tt_size_mb: usize,
eval_hash_size_mb: usize,
stop: Arc<AtomicBool>,
ponderhit_flag: Arc<AtomicBool>,
start_time: Option<Instant>,
time_options: super::TimeOptions,
skill_options: SkillOptions,
num_threads: usize,
thread_pool: ThreadPool,
worker: Option<Box<SearchWorker>>,
best_previous_score: Option<Value>,
best_previous_average_score: Option<Value>,
iter_value: [Value; 4],
iter_idx: usize,
last_best_move_depth: Depth,
last_best_move: Move,
tot_best_move_changes: f64,
previous_time_reduction: f64,
last_game_ply: Option<i32>,
increase_depth: bool,
search_again_counter: i32,
max_moves_to_draw: i32,
}
struct WorkerSummary {
best_move_changes: f64,
}
impl From<&SearchWorker> for WorkerSummary {
fn from(w: &SearchWorker) -> Self {
Self {
best_move_changes: w.best_move_changes,
}
}
}
fn aggregate_best_move_changes(changes: &[f64]) -> (f64, usize) {
if changes.is_empty() {
return (0.0, 1);
}
let sum: f64 = changes.iter().copied().sum();
(sum, changes.len())
}
#[cfg(not(target_arch = "wasm32"))]
#[repr(C, align(64))]
pub(crate) struct SearchProgress {
nodes: AtomicU64,
_pad1: [u8; 56], best_move_changes_bits: AtomicU64,
_pad2: [u8; 56], }
#[cfg(not(target_arch = "wasm32"))]
impl SearchProgress {
pub(crate) fn new() -> Self {
Self {
nodes: AtomicU64::new(0),
_pad1: [0; 56],
best_move_changes_bits: AtomicU64::new(0.0f64.to_bits()),
_pad2: [0; 56],
}
}
pub(crate) fn reset(&self) {
self.nodes.store(0, Ordering::Relaxed);
self.best_move_changes_bits.store(0.0f64.to_bits(), Ordering::Relaxed);
}
pub(crate) fn update(&self, nodes: u64, best_move_changes: f64) {
self.nodes.store(nodes, Ordering::Relaxed);
self.best_move_changes_bits
.store(best_move_changes.to_bits(), Ordering::Relaxed);
}
pub(crate) fn nodes(&self) -> u64 {
self.nodes.load(Ordering::Relaxed)
}
pub(crate) fn best_move_changes(&self) -> f64 {
f64::from_bits(self.best_move_changes_bits.load(Ordering::Relaxed))
}
}
struct ThreadSummary {
id: usize,
score: Value,
completed_depth: Depth,
}
impl ThreadSummary {
fn from_worker(id: usize, worker: &SearchWorker) -> Option<Self> {
worker.root_moves.get(0).map(|rm| Self {
id,
score: rm.score,
completed_depth: worker.completed_depth,
})
}
}
fn get_best_thread_id(main_worker: &SearchWorker, thread_pool: &ThreadPool) -> usize {
let mut summaries = Vec::new();
if let Some(summary) = ThreadSummary::from_worker(0, main_worker) {
summaries.push(summary);
}
#[cfg(not(target_arch = "wasm32"))]
for thread in thread_pool.helper_threads() {
if let Some(summary) = thread.with_worker(|worker: &mut SearchWorker| {
ThreadSummary::from_worker(thread.id(), worker)
}) {
summaries.push(summary);
}
}
#[cfg(all(target_arch = "wasm32", feature = "wasm-threads"))]
for result in thread_pool.helper_results() {
summaries.push(ThreadSummary {
id: result.thread_id,
score: result.best_score,
completed_depth: result.completed_depth,
});
}
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let _ = thread_pool;
if summaries.is_empty() {
return 0;
}
if let Some(win) = summaries.iter().find(|s| s.score.is_win()) {
return win.id;
}
let min_score = summaries.iter().map(|s| s.score.raw()).min().unwrap_or(0);
let mut best_id = summaries[0].id;
let mut best_value = i64::MIN;
for summary in summaries {
let vote_value =
(summary.score.raw() - min_score + 14) as i64 * summary.completed_depth as i64;
if vote_value > best_value {
best_value = vote_value;
best_id = summary.id;
}
}
best_id
}
struct BestThreadResult {
best_move: Move,
ponder_move: Move,
score: Value,
completed_depth: Depth,
nodes: u64,
best_previous_score: Option<Value>,
best_previous_average_score: Option<Value>,
pv: Vec<Move>,
}
fn collect_best_thread_result(
worker: &SearchWorker,
limits: &LimitsType,
skill_enabled: bool,
skill: &mut Skill,
) -> BestThreadResult {
let completed_depth = worker.completed_depth;
let nodes = worker.nodes;
let best_previous_score = worker.root_moves.get(0).map(|rm| rm.score);
let best_previous_average_score = worker.root_moves.get(0).map(|rm| {
if rm.average_score.raw() == -Value::INFINITE.raw() {
rm.score
} else {
rm.average_score
}
});
if worker.root_moves.is_empty() {
return BestThreadResult {
best_move: Move::NONE,
ponder_move: Move::NONE,
score: Value::ZERO,
completed_depth,
nodes,
best_previous_score,
best_previous_average_score,
pv: Vec::new(),
};
}
let mut effective_multi_pv = limits.multi_pv;
if skill_enabled {
effective_multi_pv = effective_multi_pv.max(4);
}
effective_multi_pv = effective_multi_pv.min(worker.root_moves.len());
let mut best_move = worker.best_move;
if skill_enabled && effective_multi_pv > 0 {
let mut rng = rand::rng();
let best = skill.pick_best(&worker.root_moves, effective_multi_pv, &mut rng);
if best != Move::NONE {
best_move = best;
}
}
let best_rm = worker.root_moves.iter().find(|rm| rm.mv() == best_move);
let ponder_move = best_rm
.and_then(|rm| {
if rm.pv.len() > 1 {
Some(rm.pv[1])
} else {
None
}
})
.unwrap_or(Move::NONE);
let score = best_rm
.map(|rm| rm.score)
.unwrap_or(worker.root_moves.get(0).map(|rm| rm.score).unwrap_or(Value::ZERO));
let pv = best_rm.map(|rm| rm.pv.clone()).unwrap_or_default();
BestThreadResult {
best_move,
ponder_move,
score,
completed_depth,
nodes,
best_previous_score,
best_previous_average_score,
pv,
}
}
impl Search {
fn prepare_time_metrics(&mut self, ply: i32) {
if let Some(last_ply) = self.last_game_ply {
if (last_ply - ply).abs() & 1 == 1 {
if let Some(prev_score) = self.best_previous_score {
if prev_score != Value::INFINITE {
self.best_previous_score = Some(Value::new(-prev_score.raw()));
}
}
if let Some(prev_avg) = self.best_previous_average_score {
if prev_avg != Value::INFINITE {
self.best_previous_average_score = Some(Value::new(-prev_avg.raw()));
}
}
}
}
if self.best_previous_score == Some(Value::INFINITE) {
self.iter_value = [Value::ZERO; 4];
} else {
let seed = self.best_previous_score.unwrap_or(Value::ZERO);
self.iter_value = [seed; 4];
}
self.iter_idx = 0;
self.last_best_move_depth = 0;
self.last_best_move = Move::NONE;
self.tot_best_move_changes = 0.0;
self.last_game_ply = Some(ply);
self.increase_depth = true;
self.search_again_counter = 0;
}
fn compute_time_factors(
&self,
best_value: Value,
completed_depth: Depth,
tot_best_move_changes: f64,
thread_count: usize,
) -> (f64, f64, f64, usize) {
let prev_avg_raw = self.best_previous_average_score.unwrap_or(Value::INFINITE).raw();
let iter_val = self.iter_value[self.iter_idx];
let falling_eval = calculate_falling_eval(prev_avg_raw, iter_val.raw(), best_value.raw());
let time_reduction = calculate_time_reduction(completed_depth, self.last_best_move_depth);
(falling_eval, time_reduction, tot_best_move_changes, thread_count)
}
fn update_time_factor_state(&mut self, best_value: Value, tot_best_move_changes: f64) {
self.iter_value[self.iter_idx] = best_value;
self.iter_idx = (self.iter_idx + 1) % self.iter_value.len();
self.tot_best_move_changes = tot_best_move_changes;
}
pub fn new(tt_size_mb: usize) -> Self {
let tt = Arc::new(TranspositionTable::new(tt_size_mb));
let eval_hash = Arc::new(EvalHash::new(DEFAULT_EVAL_HASH_SIZE_MB));
let stop = Arc::new(AtomicBool::new(false));
let ponderhit_flag = Arc::new(AtomicBool::new(false));
let max_moves_to_draw = DEFAULT_MAX_MOVES_TO_DRAW;
let thread_pool = ThreadPool::new(
1,
Arc::clone(&tt),
Arc::clone(&eval_hash),
Arc::clone(&stop),
Arc::clone(&ponderhit_flag),
max_moves_to_draw,
);
Self {
tt,
eval_hash,
tt_size_mb,
eval_hash_size_mb: DEFAULT_EVAL_HASH_SIZE_MB,
stop,
ponderhit_flag,
start_time: None,
time_options: super::TimeOptions::default(),
skill_options: SkillOptions::default(),
num_threads: 1,
thread_pool,
worker: None,
best_previous_score: Some(Value::INFINITE),
best_previous_average_score: Some(Value::INFINITE),
iter_value: [Value::ZERO; 4],
iter_idx: 0,
last_best_move_depth: 0,
last_best_move: Move::NONE,
tot_best_move_changes: 0.0,
previous_time_reduction: 0.85,
last_game_ply: None,
increase_depth: true,
search_again_counter: 0,
max_moves_to_draw,
}
}
pub fn resize_tt(&mut self, size_mb: usize) {
self.tt = Arc::new(TranspositionTable::new(size_mb));
self.tt_size_mb = size_mb;
if let Some(worker) = &mut self.worker {
worker.tt = Arc::clone(&self.tt);
}
self.thread_pool.update_tt(Arc::clone(&self.tt));
}
pub fn clear_tt(&mut self) {
self.tt = Arc::new(TranspositionTable::new(self.tt_size_mb));
if let Some(worker) = &mut self.worker {
worker.tt = Arc::clone(&self.tt);
}
self.thread_pool.update_tt(Arc::clone(&self.tt));
}
pub fn tt_uses_large_pages(&self) -> bool {
self.tt.uses_large_pages()
}
pub fn resize_eval_hash(&mut self, size_mb: usize) {
self.eval_hash = Arc::new(EvalHash::new(size_mb));
self.eval_hash_size_mb = size_mb;
if let Some(worker) = &mut self.worker {
worker.eval_hash = Arc::clone(&self.eval_hash);
}
self.thread_pool.update_eval_hash(Arc::clone(&self.eval_hash));
}
pub fn eval_hash(&self) -> Arc<EvalHash> {
Arc::clone(&self.eval_hash)
}
pub fn clear_histories(&mut self) {
if let Some(worker) = &mut self.worker {
worker.clear();
}
self.thread_pool.clear_histories();
}
pub fn stop_flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.stop)
}
pub fn ponderhit_flag(&self) -> Arc<AtomicBool> {
Arc::clone(&self.ponderhit_flag)
}
pub fn request_ponderhit(&self) {
self.ponderhit_flag.store(true, Ordering::SeqCst);
}
pub fn stop(&self) {
self.stop.store(true, Ordering::SeqCst);
}
pub fn set_time_options(&mut self, opts: super::TimeOptions) {
self.time_options = opts;
}
pub fn time_options(&self) -> super::TimeOptions {
self.time_options
}
pub fn set_skill_options(&mut self, opts: SkillOptions) {
self.skill_options = opts;
}
pub fn skill_options(&self) -> SkillOptions {
self.skill_options
}
pub fn set_max_moves_to_draw(&mut self, v: i32) {
self.max_moves_to_draw = if v > 0 { v } else { DEFAULT_MAX_MOVES_TO_DRAW };
}
pub fn max_moves_to_draw(&self) -> i32 {
self.max_moves_to_draw
}
pub fn set_num_threads(&mut self, num: usize) {
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let _ = num; #[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let num = 1;
#[cfg(not(all(target_arch = "wasm32", not(feature = "wasm-threads"))))]
let num = num.clamp(1, 512);
self.num_threads = num;
self.thread_pool.set_num_threads(
num,
Arc::clone(&self.tt),
Arc::clone(&self.eval_hash),
self.max_moves_to_draw,
);
}
pub fn num_threads(&self) -> usize {
self.num_threads
}
pub fn go<F>(
&mut self,
pos: &mut Position,
limits: LimitsType,
on_info: Option<F>,
) -> SearchResult
where
F: FnMut(&SearchInfo),
{
let ply = pos.game_ply();
self.prepare_time_metrics(ply);
self.stop.store(false, Ordering::SeqCst);
self.ponderhit_flag.store(false, Ordering::SeqCst);
self.start_time = Some(Instant::now());
self.tt.new_search();
self.thread_pool.clear_helper_results();
let mut time_manager =
TimeManagement::new(Arc::clone(&self.stop), Arc::clone(&self.ponderhit_flag));
time_manager.set_options(&self.time_options);
time_manager.set_previous_time_reduction(self.previous_time_reduction);
time_manager.init(&limits, pos.side_to_move(), ply, self.max_moves_to_draw);
let tt_clone = Arc::clone(&self.tt);
let eval_hash_clone = Arc::clone(&self.eval_hash);
let max_moves = self.max_moves_to_draw;
let worker = self
.worker
.get_or_insert_with(|| SearchWorker::new(tt_clone, eval_hash_clone, max_moves, 0));
worker.max_moves_to_draw = self.max_moves_to_draw;
worker.prepare_search();
let max_depth = if limits.depth > 0 {
limits.depth
} else {
MAX_PLY };
let mut skill = Skill::from_options(&self.skill_options);
let skill_enabled = skill.enabled();
if self.num_threads > 1 {
self.thread_pool.start_thinking(
pos,
limits.clone(),
max_depth,
self.time_options,
self.max_moves_to_draw,
skill_enabled,
);
}
let _effective_multi_pv = match on_info {
Some(callback) => self.search_with_callback(
pos,
&limits,
&mut time_manager,
max_depth,
callback,
skill_enabled,
),
None => {
let mut noop = |_info: &SearchInfo| {};
self.search_with_callback(
pos,
&limits,
&mut time_manager,
max_depth,
&mut noop,
skill_enabled,
)
}
};
if self.num_threads > 1 {
self.stop.store(true, Ordering::SeqCst);
self.thread_pool.wait_for_search_finished();
}
let best_thread_id = {
let worker = self
.worker
.as_ref()
.expect("worker should be initialized by search_with_callback");
get_best_thread_id(worker, &self.thread_pool)
};
let best_result = if best_thread_id == 0 {
let worker = self
.worker
.as_ref()
.expect("worker should be initialized by search_with_callback");
collect_best_thread_result(worker, &limits, skill_enabled, &mut skill)
} else {
#[cfg(not(target_arch = "wasm32"))]
let result = {
let mut result = None;
for thread in self.thread_pool.helper_threads() {
if thread.id() == best_thread_id {
result = Some(thread.with_worker(|worker: &mut SearchWorker| {
collect_best_thread_result(worker, &limits, skill_enabled, &mut skill)
}));
break;
}
}
result
};
#[cfg(all(target_arch = "wasm32", feature = "wasm-threads"))]
let result = {
let helper_results = self.thread_pool.helper_results();
helper_results.iter().find(|r| r.thread_id == best_thread_id).map(|r| {
let (best_move, score) = if skill_enabled && !r.top_moves.is_empty() {
let mut rng = rand::rng();
let picked = skill.pick_best_from_pairs(&r.top_moves, &mut rng);
if picked != Move::NONE {
let picked_score = r
.top_moves
.iter()
.find(|(mv, _)| *mv == picked)
.map(|(_, score)| *score)
.unwrap_or(r.best_score);
(picked, picked_score)
} else {
(r.best_move, r.best_score)
}
} else {
(r.best_move, r.best_score)
};
BestThreadResult {
best_move,
ponder_move: Move::NONE, score,
completed_depth: r.completed_depth,
nodes: r.nodes,
best_previous_score: Some(r.best_score),
best_previous_average_score: Some(r.best_score),
pv: Vec::new(), }
})
};
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let result: Option<BestThreadResult> = None;
result.unwrap_or_else(|| {
let worker = self
.worker
.as_ref()
.expect("worker should be initialized by search_with_callback");
collect_best_thread_result(worker, &limits, skill_enabled, &mut skill)
})
};
let BestThreadResult {
best_move,
ponder_move,
score,
completed_depth,
nodes: _best_nodes,
best_previous_score,
best_previous_average_score,
pv,
} = best_result;
let total_nodes = {
let main_nodes = self.worker.as_ref().map(|w| w.nodes).unwrap_or(0);
#[cfg(not(target_arch = "wasm32"))]
let helper_nodes =
self.thread_pool.helper_threads().iter().fold(0u64, |acc, thread| {
acc.saturating_add(thread.with_worker(|worker| worker.nodes))
});
#[cfg(all(target_arch = "wasm32", feature = "wasm-threads"))]
let helper_nodes = self.thread_pool.helper_nodes();
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let helper_nodes = 0u64;
main_nodes.saturating_add(helper_nodes)
};
self.previous_time_reduction = time_manager.previous_time_reduction();
self.best_previous_score = best_previous_score;
self.best_previous_average_score = best_previous_average_score;
self.last_game_ply = Some(ply);
SearchResult {
best_move,
ponder_move,
score,
depth: completed_depth,
nodes: total_nodes,
pv,
}
}
fn search_with_callback<F>(
&mut self,
pos: &mut Position,
limits: &LimitsType,
time_manager: &mut TimeManagement,
max_depth: Depth,
mut on_info: F,
skill_enabled: bool,
) -> usize
where
F: FnMut(&SearchInfo),
{
self.increase_depth = true;
self.search_again_counter = 0;
let mut worker = self.worker.take().expect("worker should be available");
worker.root_moves = super::RootMoves::from_legal_moves(pos, &limits.search_moves);
if worker.root_moves.is_empty() {
worker.best_move = Move::NONE;
#[cfg(debug_assertions)]
eprintln!(
"search_with_callback: root_moves is empty (search_moves_len={}, side_to_move={:?})",
limits.search_moves.len(),
pos.side_to_move()
);
self.worker = Some(worker);
return 0;
}
#[cfg(debug_assertions)]
eprintln!(
"search_with_callback: root_moves_len={} first_move={}",
worker.root_moves.len(),
worker.root_moves.get(0).map(|rm| rm.mv().to_usi()).unwrap_or_default()
);
if worker.root_moves.len() == 1 {
time_manager.apply_single_move_limit();
}
let start = self.start_time.unwrap();
let mut effective_multi_pv = limits.multi_pv;
if skill_enabled {
effective_multi_pv = effective_multi_pv.max(4);
}
effective_multi_pv = effective_multi_pv.min(worker.root_moves.len());
let mut last_best_pv = vec![Move::NONE];
let mut last_best_score = Value::new(-Value::INFINITE.raw());
let mut last_best_move_depth = 0;
for depth in 1..=max_depth {
#[cfg(debug_assertions)]
if depth <= 2 {
eprintln!(
"search_with_callback: depth={} nodes={} search_end={} max_time={} stop_requested={}",
depth,
worker.nodes,
time_manager.search_end(),
time_manager.maximum(),
time_manager.stop_requested()
);
}
if depth > 1 && !self.increase_depth {
self.search_again_counter += 1;
}
if worker.abort {
break;
}
if self.ponderhit_flag.swap(false, Ordering::Relaxed) {
time_manager.on_ponderhit();
}
let is_pondering = time_manager.is_pondering();
if depth > 1 && !is_pondering && time_manager.should_stop(depth) {
break;
}
if effective_multi_pv == 1 && depth > 1 && !worker.root_moves.is_empty() {
let best_value = worker.root_moves[0].score;
if limits.mate == 0 {
if proven_mate_depth_exceeded(best_value, depth) {
break;
}
} else if mate_within_limit(
best_value,
worker.root_moves[0].score_lower_bound,
worker.root_moves[0].score_upper_bound,
limits.mate,
) {
time_manager.request_stop();
break;
}
}
let search_depth = depth;
worker.root_depth = search_depth;
worker.sel_depth = 0;
let mut processed_pv = 0;
for pv_idx in 0..effective_multi_pv {
if worker.abort {
break;
}
let (mut alpha, mut beta, mut delta) =
compute_aspiration_window(&worker.root_moves[pv_idx], worker.thread_id);
let mut failed_high_cnt = 0;
loop {
let adjusted_depth = (search_depth
- failed_high_cnt
- (3 * (self.search_again_counter + 1) / 4))
.max(1);
let score = if pv_idx == 0 {
worker.search_root(pos, adjusted_depth, alpha, beta, limits, time_manager)
} else {
worker.search_root_for_pv(
pos,
search_depth,
alpha,
beta,
pv_idx,
limits,
time_manager,
)
};
if worker.abort {
break;
}
if score <= alpha {
beta = alpha;
alpha = Value::new(
score.raw().saturating_sub(delta.raw()).max(-Value::INFINITE.raw()),
);
failed_high_cnt = 0;
time_manager.reset_stop_on_ponderhit();
} else if score >= beta {
beta = Value::new(
score.raw().saturating_add(delta.raw()).min(Value::INFINITE.raw()),
);
failed_high_cnt += 1;
} else {
break;
}
delta = Value::new(delta.raw().saturating_add(delta.raw() / 3));
}
worker.root_moves.stable_sort_range(pv_idx, worker.root_moves.len());
worker.root_moves.stable_sort_range(0, pv_idx + 1);
processed_pv = pv_idx + 1;
}
if !worker.abort && effective_multi_pv > 1 {
worker.root_moves.stable_sort_range(0, effective_multi_pv);
}
if processed_pv > 0 {
let elapsed = start.elapsed();
let time_ms = elapsed.as_millis() as u64;
#[cfg(not(target_arch = "wasm32"))]
let helper_nodes = self
.thread_pool
.helper_threads()
.iter()
.fold(0u64, |acc, thread| acc.saturating_add(thread.nodes()));
#[cfg(all(target_arch = "wasm32", feature = "wasm-threads"))]
let helper_nodes = self.thread_pool.helper_nodes();
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let helper_nodes = 0u64;
let total_nodes = worker.nodes.saturating_add(helper_nodes);
let nps = if time_ms > 0 {
total_nodes.saturating_mul(1000) / time_ms
} else {
0
};
for pv_idx in 0..processed_pv {
let info = SearchInfo {
depth,
sel_depth: worker.root_moves[pv_idx].sel_depth,
score: worker.root_moves[pv_idx].score,
nodes: total_nodes,
time_ms,
nps,
hashfull: self.tt.hashfull(3) as u32,
pv: worker.root_moves[pv_idx].pv.clone(),
multi_pv: pv_idx + 1, };
on_info(&info);
}
}
if !worker.abort {
worker.completed_depth = search_depth;
worker.best_move = worker.root_moves[0].mv();
if worker.best_move != self.last_best_move {
self.last_best_move = worker.best_move;
self.last_best_move_depth = depth;
}
for rm in worker.root_moves.iter_mut() {
rm.previous_score = rm.score;
}
let summary = WorkerSummary::from(&*worker);
let best_value = if worker.root_moves.is_empty() {
Value::ZERO
} else {
worker.root_moves[0].score
};
let completed_depth = worker.completed_depth;
let effort = if worker.root_moves.is_empty() {
0.0
} else {
worker.root_moves[0].effort
};
let nodes = worker.nodes;
let root_moves_len = worker.root_moves.len();
let best_move_changes = summary.best_move_changes;
worker.best_move_changes = 0.0;
#[cfg(not(target_arch = "wasm32"))]
let (changes_sum, thread_count) = {
let helper_threads = self.thread_pool.helper_threads();
let mut changes = Vec::with_capacity(helper_threads.len() + 1);
changes.push(best_move_changes);
for thread in helper_threads {
changes.push(thread.best_move_changes());
}
aggregate_best_move_changes(&changes)
};
#[cfg(all(target_arch = "wasm32", feature = "wasm-threads"))]
let (changes_sum, thread_count) = {
let helper_changes = self.thread_pool.helper_best_move_changes();
let mut changes = Vec::with_capacity(helper_changes.len() + 1);
changes.push(best_move_changes);
changes.extend(helper_changes);
aggregate_best_move_changes(&changes)
};
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let (changes_sum, thread_count) = (best_move_changes, 1);
let tot_best_move_changes = self.tot_best_move_changes / 2.0 + changes_sum;
if limits.use_time_management()
&& !time_manager.stop_on_ponderhit()
&& time_manager.search_end() == 0
{
let (falling_eval, time_reduction, tot_changes, threads) = self
.compute_time_factors(
best_value,
completed_depth,
tot_best_move_changes,
thread_count,
);
let total_time = time_manager.total_time_for_iteration(
falling_eval,
time_reduction,
tot_changes,
threads,
);
let nodes_effort = normalize_nodes_effort(effort, nodes);
let total_time = if root_moves_len == 1 {
total_time.min(500.0)
} else {
total_time
};
let elapsed_time = time_manager.elapsed_from_ponderhit() as f64;
time_manager.apply_iteration_timing(
time_manager.elapsed(),
total_time,
nodes_effort,
completed_depth,
);
self.increase_depth =
time_manager.is_pondering() || elapsed_time <= total_time * 0.5138;
self.update_time_factor_state(best_value, tot_best_move_changes);
}
self.tot_best_move_changes = tot_best_move_changes;
if !worker.root_moves[0].pv.is_empty()
&& worker.root_moves[0].pv[0] != last_best_pv[0]
{
last_best_pv = worker.root_moves[0].pv.clone();
last_best_score = worker.root_moves[0].score;
last_best_move_depth = depth;
}
if effective_multi_pv == 1 && depth > 1 && !worker.root_moves.is_empty() {
let best_value = worker.root_moves[0].score;
if limits.mate == 0 {
if proven_mate_depth_exceeded(best_value, depth) {
break;
}
} else if mate_within_limit(
best_value,
worker.root_moves[0].score_lower_bound,
worker.root_moves[0].score_upper_bound,
limits.mate,
) {
time_manager.request_stop();
break;
}
}
}
}
if worker.abort && !worker.root_moves.is_empty() && worker.root_moves[0].score.is_loss() {
let head = last_best_pv.first().copied().unwrap_or(Move::NONE);
if head != Move::NONE {
if let Some(idx) = worker.root_moves.find(head) {
worker.root_moves.move_to_front(idx);
worker.root_moves[0].pv = last_best_pv;
worker.root_moves[0].score = last_best_score;
worker.completed_depth = last_best_move_depth;
}
}
}
self.worker = Some(worker);
effective_multi_pv
}
}
#[cfg(any(not(target_arch = "wasm32"), feature = "wasm-threads"))]
#[inline(always)]
fn search_helper_impl<F1, F2>(
worker: &mut SearchWorker,
pos: &mut Position,
limits: &LimitsType,
time_manager: &mut TimeManagement,
max_depth: Depth,
skill_enabled: bool,
on_start: F1,
mut on_depth_complete: F2,
) -> usize
where
F1: FnOnce(),
F2: FnMut(u64, f64),
{
on_start();
worker.root_moves = super::RootMoves::from_legal_moves(pos, &limits.search_moves);
if worker.root_moves.is_empty() {
worker.best_move = Move::NONE;
return 0;
}
if worker.root_moves.len() == 1 {
time_manager.apply_single_move_limit();
}
let mut effective_multi_pv = limits.multi_pv;
if skill_enabled {
effective_multi_pv = effective_multi_pv.max(4);
}
effective_multi_pv = effective_multi_pv.min(worker.root_moves.len());
let mut last_best_pv = vec![Move::NONE];
let mut last_best_score = Value::new(-Value::INFINITE.raw());
let mut last_best_move_depth = 0;
let search_again_counter = 0;
for depth in 1..=max_depth {
if worker.abort {
break;
}
if effective_multi_pv == 1 && depth > 1 && !worker.root_moves.is_empty() {
let best_value = worker.root_moves[0].score;
if limits.mate == 0 {
if proven_mate_depth_exceeded(best_value, depth) {
break;
}
} else if mate_within_limit(
best_value,
worker.root_moves[0].score_lower_bound,
worker.root_moves[0].score_upper_bound,
limits.mate,
) {
break;
}
}
let search_depth = depth;
worker.root_depth = search_depth;
worker.sel_depth = 0;
for pv_idx in 0..effective_multi_pv {
if worker.abort {
break;
}
let (mut alpha, mut beta, mut delta) =
compute_aspiration_window(&worker.root_moves[pv_idx], worker.thread_id);
let mut failed_high_cnt = 0;
loop {
let adjusted_depth =
(search_depth - failed_high_cnt - (3 * (search_again_counter + 1) / 4)).max(1);
let score = if pv_idx == 0 {
worker.search_root(pos, adjusted_depth, alpha, beta, limits, time_manager)
} else {
worker.search_root_for_pv(
pos,
search_depth,
alpha,
beta,
pv_idx,
limits,
time_manager,
)
};
if worker.abort {
break;
}
if score <= alpha {
beta = alpha;
alpha = Value::new(
score.raw().saturating_sub(delta.raw()).max(-Value::INFINITE.raw()),
);
failed_high_cnt = 0;
} else if score >= beta {
beta = Value::new(
score.raw().saturating_add(delta.raw()).min(Value::INFINITE.raw()),
);
failed_high_cnt += 1;
} else {
break;
}
delta = Value::new(
delta.raw().saturating_add(delta.raw() / 3).min(Value::INFINITE.raw()),
);
}
worker.root_moves.stable_sort_range(pv_idx, worker.root_moves.len());
worker.root_moves.stable_sort_range(0, pv_idx + 1);
}
if !worker.abort && effective_multi_pv > 1 {
worker.root_moves.stable_sort_range(0, effective_multi_pv);
}
if !worker.abort {
worker.completed_depth = search_depth;
worker.best_move = worker.root_moves[0].mv();
for rm in worker.root_moves.iter_mut() {
rm.previous_score = rm.score;
}
let best_move_changes = worker.best_move_changes;
worker.best_move_changes = 0.0;
on_depth_complete(worker.nodes, best_move_changes);
if !worker.root_moves[0].pv.is_empty() && worker.root_moves[0].pv[0] != last_best_pv[0]
{
last_best_pv = worker.root_moves[0].pv.clone();
last_best_score = worker.root_moves[0].score;
last_best_move_depth = search_depth;
}
if effective_multi_pv == 1 && depth > 1 && !worker.root_moves.is_empty() {
let best_value = worker.root_moves[0].score;
if limits.mate == 0 {
if proven_mate_depth_exceeded(best_value, depth) {
break;
}
} else if mate_within_limit(
best_value,
worker.root_moves[0].score_lower_bound,
worker.root_moves[0].score_upper_bound,
limits.mate,
) {
break;
}
}
}
}
if worker.abort && !worker.root_moves.is_empty() && worker.root_moves[0].score.is_loss() {
let head = last_best_pv.first().copied().unwrap_or(Move::NONE);
if head != Move::NONE {
if let Some(idx) = worker.root_moves.find(head) {
worker.root_moves.move_to_front(idx);
worker.root_moves[0].pv = last_best_pv;
worker.root_moves[0].score = last_best_score;
worker.completed_depth = last_best_move_depth;
}
}
}
effective_multi_pv
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn search_helper(
worker: &mut SearchWorker,
pos: &mut Position,
limits: &LimitsType,
time_manager: &mut TimeManagement,
max_depth: Depth,
skill_enabled: bool,
progress: Option<&SearchProgress>,
) -> usize {
search_helper_impl(
worker,
pos,
limits,
time_manager,
max_depth,
skill_enabled,
|| {
if let Some(p) = progress {
p.reset();
}
},
|nodes, bmc| {
if let Some(p) = progress {
p.update(nodes, bmc);
}
},
)
}
#[cfg(all(target_arch = "wasm32", feature = "wasm-threads"))]
pub(crate) fn search_helper(
worker: &mut SearchWorker,
pos: &mut Position,
limits: &LimitsType,
time_manager: &mut TimeManagement,
max_depth: Depth,
skill_enabled: bool,
progress: Option<&super::thread::HelperProgress>,
) -> usize {
search_helper_impl(
worker,
pos,
limits,
time_manager,
max_depth,
skill_enabled,
|| {
if let Some(p) = progress {
p.reset();
}
},
|nodes, bmc| {
if let Some(p) = progress {
p.update(nodes, bmc);
}
},
)
}
#[cfg(test)]
mod tests {
use super::*;
const STACK_SIZE: usize = 64 * 1024 * 1024;
#[test]
fn test_aggregate_best_move_changes_empty() {
let (sum, threads) = aggregate_best_move_changes(&[]);
assert_eq!(sum, 0.0);
assert_eq!(threads, 1);
}
#[test]
fn test_aggregate_best_move_changes_multi() {
let (sum, threads) = aggregate_best_move_changes(&[1.0, 2.0, 3.0]);
assert!((sum - 6.0).abs() < 1e-9, "sum should be 6.0, got {sum}");
assert_eq!(threads, 3);
}
#[test]
fn test_worker_summary_from_worker() {
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
let tt = Arc::new(TranspositionTable::new(16));
let eval_hash = Arc::new(EvalHash::new(1));
let mut worker = SearchWorker::new(tt, eval_hash, DEFAULT_MAX_MOVES_TO_DRAW, 0);
worker.best_move_changes = 3.5;
let summary = WorkerSummary::from(&*worker);
assert!(
(summary.best_move_changes - 3.5).abs() < 1e-9,
"best_move_changes should match"
);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_prepare_time_metrics_resets_iter_state() {
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
let mut search = Search::new(16);
search.best_previous_score = Some(Value::new(200));
search.best_previous_average_score = Some(Value::new(123));
search.last_game_ply = Some(5);
search.iter_value = [Value::new(1), Value::new(2), Value::new(3), Value::new(4)];
search.iter_idx = 2;
search.last_best_move_depth = 5;
search.tot_best_move_changes = 7.5;
search.prepare_time_metrics(6);
assert_eq!(search.best_previous_score, Some(Value::new(-200)));
assert_eq!(search.best_previous_average_score, Some(Value::new(-123)));
assert_eq!(search.iter_value, [Value::new(-200); 4]);
assert_eq!(search.iter_idx, 0);
assert_eq!(search.last_best_move_depth, 0);
assert_eq!(search.tot_best_move_changes, 0.0);
assert_eq!(search.last_game_ply, Some(6));
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_prepare_time_metrics_seeds_zero_for_infinite() {
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
let mut search = Search::new(16);
search.best_previous_score = Some(Value::INFINITE);
search.best_previous_average_score = Some(Value::INFINITE);
search.prepare_time_metrics(1);
assert_eq!(search.iter_value, [Value::ZERO; 4]);
assert_eq!(search.iter_idx, 0);
assert_eq!(search.best_previous_score, Some(Value::INFINITE));
assert_eq!(search.best_previous_average_score, Some(Value::INFINITE));
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_set_max_moves_to_draw_option() {
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
let mut search = Search::new(16);
search.set_max_moves_to_draw(512);
assert_eq!(search.max_moves_to_draw(), 512);
search.set_max_moves_to_draw(0);
assert_eq!(search.max_moves_to_draw(), DEFAULT_MAX_MOVES_TO_DRAW);
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_mate_within_limit_converts_moves_to_plies() {
assert!(mate_within_limit(Value::mate_in(9), false, false, 5));
assert!(!mate_within_limit(Value::mate_in(11), false, false, 5));
}
#[test]
fn test_mate_within_limit_handles_mated_scores() {
assert!(mate_within_limit(Value::mated_in(7), false, false, 4));
}
#[test]
fn test_mate_within_limit_requires_exact_score() {
assert!(!mate_within_limit(Value::mate_in(7), true, false, 4));
assert!(!mate_within_limit(Value::mate_in(7), false, true, 4));
}
#[test]
fn test_search_basic() {
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
let mut search = Search::new(16);
let mut pos = Position::new();
pos.set_hirate();
let limits = LimitsType {
depth: 3,
..Default::default()
};
let result = search.go(&mut pos, limits, None::<fn(&SearchInfo)>);
assert_ne!(result.best_move, Move::NONE, "Should find a best move");
assert!(result.depth >= 1, "Should complete at least depth 1");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_search_with_callback() {
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
let mut search = Search::new(16);
let mut pos = Position::new();
pos.set_hirate();
let limits = LimitsType {
depth: 2,
..Default::default()
};
let mut info_count = 0;
let result = search.go(
&mut pos,
limits,
Some(|_info: &SearchInfo| {
info_count += 1;
}),
);
assert_ne!(result.best_move, Move::NONE, "Should find a best move");
assert!(info_count >= 1, "Should have called info callback at least once");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_search_info_to_usi() {
let info = SearchInfo {
depth: 5,
sel_depth: 7,
score: Value::new(123),
nodes: 10000,
time_ms: 500,
nps: 20000,
hashfull: 100,
pv: vec![],
multi_pv: 1,
};
let usi = info.to_usi_string();
assert!(usi.contains("depth 5"));
assert!(usi.contains("seldepth 7"));
assert!(usi.contains("multipv 1"));
assert!(usi.contains("score cp 123"));
assert!(usi.contains("nodes 10000"));
}
#[test]
fn test_search_info_to_usi_formats_mate_score() {
let info = SearchInfo {
depth: 9,
sel_depth: 9,
score: Value::mate_in(5),
nodes: 42,
time_ms: 10,
nps: 4200,
hashfull: 0,
pv: vec![],
multi_pv: 1,
};
let usi = info.to_usi_string();
assert!(usi.contains("score mate 5"));
}
#[test]
fn test_search_info_to_usi_formats_mated_score_with_negative_sign() {
let info = SearchInfo {
depth: 9,
sel_depth: 9,
score: Value::mated_in(4),
nodes: 42,
time_ms: 10,
nps: 4200,
hashfull: 0,
pv: vec![],
multi_pv: 1,
};
let usi = info.to_usi_string();
assert!(usi.contains("score mate -4"));
}
}