use crate::eval::EvalHash;
use crate::time::Instant;
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::atomic::AtomicU64;
use std::sync::atomic::{AtomicBool, Ordering};
use super::time_manager::{
DEFAULT_MAX_MOVES_TO_DRAW, calculate_falling_eval, calculate_time_reduction,
normalize_nodes_effort,
};
use super::{
DEFAULT_DRAW_VALUE_BLACK, DEFAULT_DRAW_VALUE_WHITE, LimitsType, RootMove, SearchTuneParams,
SearchWorker, Skill, SkillOptions, ThreadPool, TimeManagement,
};
use crate::position::Position;
use crate::tt::TranspositionTable;
use crate::types::{Depth, MAX_PLY, Move, Value};
#[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.to_cp())
};
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 delta_raw = 5 + thread_offset + (mean_sq / 9000).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 stats_report: String,
}
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,
increase_depth_shared: Arc<AtomicBool>,
search_again_counter: i32,
max_moves_to_draw: i32,
draw_value_black: i32,
draw_value_white: i32,
search_tune_params: SearchTuneParams,
}
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,
best_move: Move,
pv_len: usize,
}
impl ThreadSummary {
fn from_worker(id: usize, worker: &SearchWorker) -> Option<Self> {
worker.state.root_moves.get(0).map(|rm| {
let best_move = rm.pv.first().copied().unwrap_or_else(|| rm.mv());
Self {
id,
score: rm.score,
completed_depth: worker.state.completed_depth,
best_move,
pv_len: rm.pv.len(),
}
})
}
}
#[inline]
fn thread_voting_value(summary: &ThreadSummary, min_score: Value) -> i64 {
(summary.score.raw() - min_score.raw() + 14) as i64 * summary.completed_depth as i64
}
#[inline]
fn is_proven_loss(score: Value) -> bool {
score != Value::new(-Value::INFINITE.raw()) && score.is_loss()
}
fn select_best_summary_index(summaries: &[ThreadSummary]) -> usize {
if summaries.is_empty() {
return 0;
}
let min_score =
summaries.iter().map(|s| s.score).min_by_key(|s| s.raw()).unwrap_or(Value::ZERO);
let mut votes = HashMap::with_capacity(2 * summaries.len());
for summary in summaries {
*votes.entry(summary.best_move).or_insert(0i64) += thread_voting_value(summary, min_score);
}
let mut best_idx = 0usize;
for (idx, summary) in summaries.iter().enumerate() {
let best = &summaries[best_idx];
let best_vote = *votes.get(&best.best_move).unwrap_or(&0);
let new_vote = *votes.get(&summary.best_move).unwrap_or(&0);
let best_in_proven_win = best.score.is_win();
let new_in_proven_win = summary.score.is_win();
let best_in_proven_loss = is_proven_loss(best.score);
let new_in_proven_loss = is_proven_loss(summary.score);
let better_voting_value = thread_voting_value(summary, min_score)
* (summary.pv_len > 2) as i64
> thread_voting_value(best, min_score) * (best.pv_len > 2) as i64;
if best_in_proven_win {
if summary.score > best.score {
best_idx = idx;
}
} else if best_in_proven_loss {
if new_in_proven_loss && summary.score < best.score {
best_idx = idx;
}
} else if new_in_proven_win
|| new_in_proven_loss
|| (!summary.score.is_loss()
&& (new_vote > best_vote || (new_vote == best_vote && better_voting_value)))
{
best_idx = idx;
}
}
if best_idx != 0 {
let main = &summaries[0];
let selected = &summaries[best_idx];
if selected.best_move != main.best_move && !selected.score.is_mate_score() {
let support_count =
summaries.iter().filter(|s| s.best_move == selected.best_move).count();
if support_count < 2 {
return 0;
}
}
}
best_idx
}
#[inline]
fn best_thread_debug_enabled() -> bool {
std::env::var("RSHOGI_DEBUG_BEST_THREAD")
.map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "on" | "ON"))
.unwrap_or(false)
}
#[inline]
fn helper_search_disabled() -> bool {
std::env::var("RSHOGI_DISABLE_HELPER_SEARCH")
.map(|v| matches!(v.as_str(), "1" | "true" | "TRUE" | "on" | "ON"))
.unwrap_or(false)
}
fn emit_best_thread_debug(
summaries: &[ThreadSummary],
votes: &HashMap<Move, i64>,
selected_id: usize,
applied: bool,
) {
println!(
"info string [best_thread] applied={} selected_id={} threads={}",
applied,
selected_id,
summaries.len()
);
for summary in summaries {
let vote = votes.get(&summary.best_move).copied().unwrap_or(0);
println!(
"info string [best_thread] id={} depth={} score={} move={} pv_len={} vote={}",
summary.id,
summary.completed_depth,
summary.score.raw(),
summary.best_move.to_usi(),
summary.pv_len,
vote
);
}
}
fn collect_thread_summaries(
main_worker: &SearchWorker,
thread_pool: &ThreadPool,
) -> Vec<ThreadSummary> {
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,
best_move: result.best_move,
pv_len: 3,
});
}
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let _ = thread_pool;
summaries
}
fn should_use_best_thread_selection(limits: &LimitsType, skill_enabled: bool) -> bool {
limits.multi_pv == 1 && limits.depth == 0 && limits.mate == 0 && !skill_enabled
}
fn get_best_thread_id(
main_worker: &SearchWorker,
thread_pool: &ThreadPool,
use_best_thread: bool,
debug: bool,
) -> usize {
let summaries = collect_thread_summaries(main_worker, thread_pool);
if summaries.is_empty() {
return 0;
}
let min_score =
summaries.iter().map(|s| s.score).min_by_key(|s| s.raw()).unwrap_or(Value::ZERO);
let mut votes = HashMap::with_capacity(2 * summaries.len());
for summary in &summaries {
*votes.entry(summary.best_move).or_insert(0i64) += thread_voting_value(summary, min_score);
}
let candidate_idx = select_best_summary_index(&summaries);
let candidate_id = summaries[candidate_idx].id;
let selected_id = if use_best_thread { candidate_id } else { 0 };
if debug {
emit_best_thread_debug(&summaries, &votes, selected_id, use_best_thread);
}
selected_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.state.completed_depth;
let nodes = worker.state.nodes;
let best_previous_score = worker.state.root_moves.get(0).map(|rm| rm.score);
let best_previous_average_score = worker.state.root_moves.get(0).map(|rm| {
if rm.average_score.raw() == -Value::INFINITE.raw() {
rm.score
} else {
rm.average_score
}
});
if worker.state.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.state.root_moves.len());
let mut best_move = worker.state.best_move;
if skill_enabled && effective_multi_pv > 0 {
let mut rng = rand::rng();
let best = skill.pick_best(&worker.state.root_moves, effective_multi_pv, &mut rng);
if best != Move::NONE {
best_move = best;
}
}
let best_rm = worker.state.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.state.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
&& (last_ply - ply).abs() & 1 == 1
{
if let Some(prev_score) = self.best_previous_score
&& prev_score != Value::INFINITE
{
self.best_previous_score = Some(Value::new(-prev_score.raw()));
}
if let Some(prev_avg) = self.best_previous_average_score
&& 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.increase_depth_shared.store(true, Ordering::Relaxed);
self.search_again_counter = 0;
}
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 increase_depth_shared = Arc::new(AtomicBool::new(true));
let max_moves_to_draw = DEFAULT_MAX_MOVES_TO_DRAW;
let search_tune_params = SearchTuneParams::default();
let thread_pool = ThreadPool::new(
1,
Arc::clone(&tt),
Arc::clone(&eval_hash),
Arc::clone(&stop),
Arc::clone(&ponderhit_flag),
Arc::clone(&increase_depth_shared),
max_moves_to_draw,
search_tune_params,
);
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,
increase_depth_shared,
search_again_counter: 0,
max_moves_to_draw,
draw_value_black: DEFAULT_DRAW_VALUE_BLACK,
draw_value_white: DEFAULT_DRAW_VALUE_WHITE,
search_tune_params,
}
}
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_draw_value_black(&mut self, v: i32) {
self.draw_value_black = v.clamp(-30000, 30000);
if let Some(worker) = &mut self.worker {
worker.draw_value_black = self.draw_value_black;
}
}
pub fn draw_value_black(&self) -> i32 {
self.draw_value_black
}
pub fn set_draw_value_white(&mut self, v: i32) {
self.draw_value_white = v.clamp(-30000, 30000);
if let Some(worker) = &mut self.worker {
worker.draw_value_white = self.draw_value_white;
}
}
pub fn draw_value_white(&self) -> i32 {
self.draw_value_white
}
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,
self.search_tune_params,
);
}
pub fn num_threads(&self) -> usize {
self.num_threads
}
pub fn search_tune_params(&self) -> SearchTuneParams {
self.search_tune_params
}
pub fn set_search_tune_params(&mut self, params: SearchTuneParams) {
self.search_tune_params = params;
if let Some(worker) = &mut self.worker {
worker.search_tune_params = params;
}
self.thread_pool.update_search_tune_params(params);
}
pub fn set_search_tune_option(
&mut self,
name: &str,
value: i32,
) -> Option<super::SearchTuneSetResult> {
let mut params = self.search_tune_params;
let result = params.set_from_usi_name(name, value)?;
self.set_search_tune_params(params);
Some(result)
}
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 search_tune_params = self.search_tune_params;
let draw_value_black = self.draw_value_black;
let draw_value_white = self.draw_value_white;
let worker = self.worker.get_or_insert_with(|| {
SearchWorker::new(tt_clone, eval_hash_clone, max_moves, 0, search_tune_params)
});
worker.max_moves_to_draw = self.max_moves_to_draw;
worker.search_tune_params = self.search_tune_params;
worker.draw_value_black = self.draw_value_black;
worker.draw_value_white = self.draw_value_white;
worker.prepare_search();
worker.allow_tt_write = true;
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();
let helper_search_enabled = self.num_threads > 1 && !helper_search_disabled();
if helper_search_enabled {
let helper_max_depth = if limits.depth > 0 { MAX_PLY } else { max_depth };
self.thread_pool.start_thinking(
pos,
limits.clone(),
helper_max_depth,
self.time_options,
self.max_moves_to_draw,
draw_value_black,
draw_value_white,
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 helper_search_enabled {
self.stop.store(true, Ordering::SeqCst);
self.thread_pool.wait_for_search_finished();
}
let use_best_thread =
self.num_threads > 1 && should_use_best_thread_selection(&limits, skill_enabled);
let debug_best_thread = best_thread_debug_enabled();
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, use_best_thread, debug_best_thread)
};
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.state.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.state.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);
let stats_report = self.worker.as_ref().map(|w| w.get_stats_report()).unwrap_or_default();
SearchResult {
best_move,
ponder_move,
score,
depth: completed_depth,
nodes: total_nodes,
pv,
stats_report,
}
}
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.increase_depth_shared.store(true, Ordering::Relaxed);
self.search_again_counter = 0;
let mut worker = self.worker.take().expect("worker should be available");
let mut main_state = MainThreadState {
ponderhit_flag: &self.ponderhit_flag,
start_time: self.start_time.unwrap(),
tt: &self.tt,
thread_pool: &self.thread_pool,
increase_depth: self.increase_depth,
search_again_counter: self.search_again_counter,
best_previous_average_score: self.best_previous_average_score,
iter_value: self.iter_value,
iter_idx: self.iter_idx,
last_best_move: self.last_best_move,
last_best_move_depth: self.last_best_move_depth,
tot_best_move_changes: self.tot_best_move_changes,
increase_depth_shared: &self.increase_depth_shared,
};
let mut noop_progress = |_nodes: u64, _bmc: f64| {};
let result = iterative_deepening(
&mut worker,
pos,
limits,
time_manager,
max_depth,
skill_enabled,
&self.increase_depth_shared,
Some(&mut main_state),
&mut on_info,
&mut noop_progress,
);
self.increase_depth = main_state.increase_depth;
self.search_again_counter = main_state.search_again_counter;
self.iter_value = main_state.iter_value;
self.iter_idx = main_state.iter_idx;
self.last_best_move = main_state.last_best_move;
self.last_best_move_depth = main_state.last_best_move_depth;
self.tot_best_move_changes = main_state.tot_best_move_changes;
self.worker = Some(worker);
result
}
}
struct MainThreadState<'a> {
ponderhit_flag: &'a AtomicBool,
start_time: Instant,
tt: &'a TranspositionTable,
thread_pool: &'a ThreadPool,
increase_depth: bool,
search_again_counter: i32,
best_previous_average_score: Option<Value>,
iter_value: [Value; 4],
iter_idx: usize,
last_best_move: Move,
last_best_move_depth: Depth,
tot_best_move_changes: f64,
increase_depth_shared: &'a AtomicBool,
}
impl MainThreadState<'_> {
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;
}
}
fn iterative_deepening<FInfo, FProgress>(
worker: &mut SearchWorker,
pos: &mut Position,
limits: &LimitsType,
time_manager: &mut TimeManagement,
max_depth: Depth,
skill_enabled: bool,
increase_depth_shared: &AtomicBool,
mut main_state: Option<&mut MainThreadState>,
on_info: &mut FInfo,
on_progress: &mut FProgress,
) -> usize
where
FInfo: FnMut(&SearchInfo),
FProgress: FnMut(u64, f64),
{
let is_main = main_state.is_some();
worker.state.root_moves = super::RootMoves::from_legal_moves(pos, &limits.search_moves);
if worker.state.root_moves.is_empty() {
worker.state.best_move = Move::NONE;
#[cfg(debug_assertions)]
if is_main {
eprintln!(
"iterative_deepening: root_moves is empty (search_moves_len={}, side_to_move={:?})",
limits.search_moves.len(),
pos.side_to_move()
);
}
return 0;
}
#[cfg(debug_assertions)]
if is_main {
eprintln!(
"iterative_deepening: root_moves_len={} first_move={}",
worker.state.root_moves.len(),
worker.state.root_moves.get(0).map(|rm| rm.mv().to_usi()).unwrap_or_default()
);
}
if worker.state.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.state.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 mut local_search_again_counter: i32 = 0;
for depth in 1..=max_depth {
if worker.state.abort {
break;
}
if depth > 1 {
let inc_depth = if let Some(ref ms) = main_state {
ms.increase_depth
} else {
increase_depth_shared.load(Ordering::Relaxed)
};
if !inc_depth {
if let Some(ref mut ms) = main_state {
ms.search_again_counter += 1;
} else {
local_search_again_counter += 1;
}
}
}
if let Some(ref ms) = main_state {
if ms.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;
}
}
#[cfg(debug_assertions)]
if is_main && depth <= 2 {
eprintln!(
"iterative_deepening: depth={} nodes={} search_end={} max_time={} stop_requested={}",
depth,
worker.state.nodes,
time_manager.search_end(),
time_manager.maximum(),
time_manager.stop_requested()
);
}
if effective_multi_pv == 1 && depth > 1 && !worker.state.root_moves.is_empty() {
let best_value = worker.state.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.state.root_moves[0].score_lower_bound,
worker.state.root_moves[0].score_upper_bound,
limits.mate,
) {
if is_main {
time_manager.request_stop();
}
break;
}
}
let search_depth = depth;
worker.state.root_depth = search_depth;
worker.state.sel_depth = 0;
let search_again_counter = if let Some(ref ms) = main_state {
ms.search_again_counter
} else {
local_search_again_counter
};
let mut processed_pv = 0;
for pv_idx in 0..effective_multi_pv {
if worker.state.abort {
break;
}
let (mut alpha, mut beta, mut delta) =
compute_aspiration_window(&worker.state.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,
)
};
worker.state.root_moves.stable_sort_range(pv_idx, worker.state.root_moves.len());
if worker.state.abort {
break;
}
if score <= alpha {
beta = alpha;
alpha = Value::new(
score.raw().saturating_sub(delta.raw()).max(-Value::INFINITE.raw()),
);
failed_high_cnt = 0;
if is_main {
time_manager.reset_stop_on_ponderhit();
}
} else if score >= beta {
alpha = Value::new((beta.raw() - delta.raw()).max(alpha.raw()));
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.state.root_moves.stable_sort_range(pv_idx, worker.state.root_moves.len());
worker.state.root_moves.stable_sort_range(0, pv_idx + 1);
processed_pv = pv_idx + 1;
}
if !worker.state.abort && effective_multi_pv > 1 {
worker.state.root_moves.stable_sort_range(0, effective_multi_pv);
}
if let Some(ref ms) = main_state
&& processed_pv > 0
{
let elapsed = ms.start_time.elapsed();
let time_ms = elapsed.as_millis() as u64;
#[cfg(not(target_arch = "wasm32"))]
let helper_nodes = ms
.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 = ms.thread_pool.helper_nodes();
#[cfg(all(target_arch = "wasm32", not(feature = "wasm-threads")))]
let helper_nodes = 0u64;
let total_nodes = worker.state.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.state.root_moves[pv_idx].sel_depth,
score: worker.state.root_moves[pv_idx].score,
nodes: total_nodes,
time_ms,
nps,
hashfull: ms.tt.hashfull(3) as u32,
pv: worker.state.root_moves[pv_idx].pv.clone(),
multi_pv: pv_idx + 1, };
on_info(&info);
}
}
if !worker.state.abort {
worker.state.completed_depth = search_depth;
worker.state.best_move = worker.state.root_moves[0].mv();
for rm in worker.state.root_moves.iter_mut() {
rm.previous_score = rm.score;
}
let best_move_changes = worker.state.best_move_changes;
worker.state.best_move_changes = 0.0;
if let Some(ref mut ms) = main_state {
if worker.state.best_move != ms.last_best_move {
ms.last_best_move = worker.state.best_move;
ms.last_best_move_depth = depth;
}
let best_value = if worker.state.root_moves.is_empty() {
Value::ZERO
} else {
worker.state.root_moves[0].score
};
let completed_depth = worker.state.completed_depth;
let effort = if worker.state.root_moves.is_empty() {
0.0
} else {
worker.state.root_moves[0].effort
};
let nodes = worker.state.nodes;
let root_moves_len = worker.state.root_moves.len();
#[cfg(not(target_arch = "wasm32"))]
let (changes_sum, thread_count) = {
let helper_threads = ms.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 = ms.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 = ms.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) = ms
.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,
);
ms.increase_depth =
time_manager.is_pondering() || elapsed_time <= total_time * 0.503;
ms.increase_depth_shared.store(ms.increase_depth, Ordering::Relaxed);
ms.update_time_factor_state(best_value, tot_best_move_changes);
}
ms.tot_best_move_changes = tot_best_move_changes;
} else {
on_progress(worker.state.nodes, best_move_changes);
}
if !worker.state.root_moves[0].pv.is_empty()
&& worker.state.root_moves[0].pv[0] != last_best_pv[0]
{
last_best_pv = worker.state.root_moves[0].pv.clone();
last_best_score = worker.state.root_moves[0].score;
last_best_move_depth = depth;
}
if effective_multi_pv == 1 && depth > 1 && !worker.state.root_moves.is_empty() {
let best_value = worker.state.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.state.root_moves[0].score_lower_bound,
worker.state.root_moves[0].score_upper_bound,
limits.mate,
) {
if is_main {
time_manager.request_stop();
}
break;
}
}
}
}
if worker.state.abort
&& !worker.state.root_moves.is_empty()
&& worker.state.root_moves[0].score.is_loss()
{
let head = last_best_pv.first().copied().unwrap_or(Move::NONE);
if head != Move::NONE
&& let Some(idx) = worker.state.root_moves.find(head)
{
worker.state.root_moves.move_to_front(idx);
worker.state.root_moves[0].pv = last_best_pv;
worker.state.root_moves[0].score = last_best_score;
worker.state.completed_depth = last_best_move_depth;
}
}
effective_multi_pv
}
#[cfg(any(not(target_arch = "wasm32"), feature = "wasm-threads"))]
fn search_helper_impl<F1, F2>(
worker: &mut SearchWorker,
pos: &mut Position,
limits: &LimitsType,
time_manager: &mut TimeManagement,
max_depth: Depth,
skill_enabled: bool,
increase_depth_shared: &AtomicBool,
on_start: F1,
mut on_depth_complete: F2,
) -> usize
where
F1: FnOnce(),
F2: FnMut(u64, f64),
{
worker.allow_tt_write = true;
on_start();
let mut noop_info = |_info: &SearchInfo| {};
iterative_deepening(
worker,
pos,
limits,
time_manager,
max_depth,
skill_enabled,
increase_depth_shared,
None,
&mut noop_info,
&mut on_depth_complete,
)
}
#[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>,
increase_depth_shared: &AtomicBool,
) -> usize {
search_helper_impl(
worker,
pos,
limits,
time_manager,
max_depth,
skill_enabled,
increase_depth_shared,
|| {
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>,
increase_depth_shared: &AtomicBool,
) -> usize {
search_helper_impl(
worker,
pos,
limits,
time_manager,
max_depth,
skill_enabled,
increase_depth_shared,
|| {
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_should_use_best_thread_selection_yaneuraou_conditions() {
let mut limits = LimitsType::default();
assert!(should_use_best_thread_selection(&limits, false));
limits.depth = 8;
assert!(
!should_use_best_thread_selection(&limits, false),
"go depth ではbest thread選抜を使わない"
);
limits.depth = 0;
limits.mate = 3;
assert!(
!should_use_best_thread_selection(&limits, false),
"go mate ではbest thread選抜を使わない"
);
limits.mate = 0;
limits.multi_pv = 2;
assert!(
!should_use_best_thread_selection(&limits, false),
"MultiPV>1 ではbest thread選抜を使わない"
);
limits.multi_pv = 1;
assert!(
!should_use_best_thread_selection(&limits, true),
"Skill有効時はbest thread選抜を使わない"
);
}
#[test]
fn test_select_best_summary_index_prefers_move_vote_over_single_outlier() {
let m_2g2f = Move::from_usi("2g2f").expect("valid move");
let m_6i7h = Move::from_usi("6i7h").expect("valid move");
let summaries = vec![
ThreadSummary {
id: 0,
score: Value::new(30),
completed_depth: 10,
best_move: m_2g2f,
pv_len: 4,
},
ThreadSummary {
id: 1,
score: Value::new(28),
completed_depth: 9,
best_move: m_2g2f,
pv_len: 4,
},
ThreadSummary {
id: 2,
score: Value::new(90),
completed_depth: 1,
best_move: m_6i7h,
pv_len: 4,
},
];
let idx = select_best_summary_index(&summaries);
assert_eq!(summaries[idx].best_move, m_2g2f);
}
#[test]
fn test_select_best_summary_index_prefers_shorter_win_line() {
let m_2g2f = Move::from_usi("2g2f").expect("valid move");
let m_7g7f = Move::from_usi("7g7f").expect("valid move");
let summaries = vec![
ThreadSummary {
id: 0,
score: Value::mate_in(7),
completed_depth: 12,
best_move: m_2g2f,
pv_len: 4,
},
ThreadSummary {
id: 1,
score: Value::mate_in(5),
completed_depth: 8,
best_move: m_7g7f,
pv_len: 4,
},
];
let idx = select_best_summary_index(&summaries);
assert_eq!(idx, 1, "勝ち筋同士ではより短手数の詰みを優先する");
}
#[test]
fn test_select_best_summary_index_rejects_single_helper_outlier_move() {
let m_2g2f = Move::from_usi("2g2f").expect("valid move");
let m_6i7h = Move::from_usi("6i7h").expect("valid move");
let m_7g7f = Move::from_usi("7g7f").expect("valid move");
let summaries = vec![
ThreadSummary {
id: 0,
score: Value::new(35),
completed_depth: 8,
best_move: m_2g2f,
pv_len: 6,
},
ThreadSummary {
id: 1,
score: Value::new(180),
completed_depth: 8,
best_move: m_6i7h,
pv_len: 6,
},
ThreadSummary {
id: 2,
score: Value::new(34),
completed_depth: 8,
best_move: m_7g7f,
pv_len: 6,
},
];
let idx = select_best_summary_index(&summaries);
assert_eq!(idx, 0, "helper単独の外れ値手ではmain threadを優先する");
}
#[test]
fn test_select_best_summary_index_allows_helper_when_supported_by_multiple_threads() {
let m_2g2f = Move::from_usi("2g2f").expect("valid move");
let m_6i7h = Move::from_usi("6i7h").expect("valid move");
let summaries = vec![
ThreadSummary {
id: 0,
score: Value::new(35),
completed_depth: 8,
best_move: m_2g2f,
pv_len: 6,
},
ThreadSummary {
id: 1,
score: Value::new(120),
completed_depth: 8,
best_move: m_6i7h,
pv_len: 6,
},
ThreadSummary {
id: 2,
score: Value::new(110),
completed_depth: 8,
best_move: m_6i7h,
pv_len: 6,
},
];
let idx = select_best_summary_index(&summaries);
assert_eq!(summaries[idx].best_move, m_6i7h);
}
#[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_set_draw_value_options() {
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
let mut search = Search::new(16);
search.set_draw_value_black(123);
search.set_draw_value_white(-456);
assert_eq!(search.draw_value_black(), 123);
assert_eq!(search.draw_value_white(), -456);
search.set_draw_value_black(40000);
search.set_draw_value_white(-40000);
assert_eq!(search.draw_value_black(), 30000);
assert_eq!(search.draw_value_white(), -30000);
})
.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 136"));
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"));
}
}