#[cfg(feature = "search-stats")]
pub(super) const STATS_MAX_DEPTH: usize = 32;
#[cfg(feature = "search-stats")]
#[derive(Debug, Clone)]
pub struct SearchStats {
pub nodes_searched: u64,
pub lmr_applied: u64,
pub lmr_research: u64,
pub move_loop_pruned: u64,
pub futility_pruned: u64,
pub nmp_attempted: u64,
pub nmp_cutoff: u64,
pub nmp_skip_not_cut_node: u64,
pub nmp_skip_in_check: u64,
pub nmp_skip_eval_low: u64,
pub nmp_skip_excluded: u64,
pub nmp_skip_prev_null: u64,
pub nmp_candidate_nodes: u64,
pub razoring_applied: u64,
pub probcut_attempted: u64,
pub probcut_cutoff: u64,
pub singular_extension: u64,
pub multi_cut: u64,
pub tt_cutoff: u64,
pub nodes_by_depth: [u64; STATS_MAX_DEPTH],
pub tt_cutoff_by_depth: [u64; STATS_MAX_DEPTH],
pub tt_probe_by_depth: [u64; STATS_MAX_DEPTH],
pub tt_hit_by_depth: [u64; STATS_MAX_DEPTH],
pub tt_fail_depth_by_depth: [u64; STATS_MAX_DEPTH],
pub tt_fail_bound_by_depth: [u64; STATS_MAX_DEPTH],
pub lmr_to_depth1_from: [u64; STATS_MAX_DEPTH],
pub depth1_children_total: u64,
pub depth1_children_tt_cut: u64,
pub tt_write_by_depth: [u64; STATS_MAX_DEPTH],
pub razoring_by_depth: [u64; STATS_MAX_DEPTH],
pub futility_by_depth: [u64; STATS_MAX_DEPTH],
pub nmp_cutoff_by_depth: [u64; STATS_MAX_DEPTH],
pub first_move_cutoff_by_depth: [u64; STATS_MAX_DEPTH],
pub cutoff_by_depth: [u64; STATS_MAX_DEPTH],
pub move_count_sum_by_depth: [u64; STATS_MAX_DEPTH],
pub lmr_reduction_histogram: [u64; 16],
pub lmr_new_depth_histogram: [u64; STATS_MAX_DEPTH],
pub qs_nodes: u64,
pub qs_tt_hit: u64,
pub qs_tt_cutoff: u64,
pub qs_stand_pat_cutoff: u64,
pub qs_moves_generated: u64,
pub qs_moves_searched: u64,
pub qs_see_pruned: u64,
pub qs_futility_pruned: u64,
pub qs_history_pruned: u64,
pub qs_see_margin_pruned: u64,
pub qs_in_check_nodes: u64,
pub lmr_cut_node_applied: u64,
pub lmr_cut_node_to_depth1: u64,
pub lmr_non_cut_node_applied: u64,
pub lmr_non_cut_node_to_depth1: u64,
}
#[cfg(feature = "search-stats")]
impl Default for SearchStats {
fn default() -> Self {
Self {
nodes_searched: 0,
lmr_applied: 0,
lmr_research: 0,
move_loop_pruned: 0,
futility_pruned: 0,
nmp_attempted: 0,
nmp_cutoff: 0,
nmp_skip_not_cut_node: 0,
nmp_skip_in_check: 0,
nmp_skip_eval_low: 0,
nmp_skip_excluded: 0,
nmp_skip_prev_null: 0,
nmp_candidate_nodes: 0,
razoring_applied: 0,
probcut_attempted: 0,
probcut_cutoff: 0,
singular_extension: 0,
multi_cut: 0,
tt_cutoff: 0,
nodes_by_depth: [0; STATS_MAX_DEPTH],
tt_cutoff_by_depth: [0; STATS_MAX_DEPTH],
tt_probe_by_depth: [0; STATS_MAX_DEPTH],
tt_hit_by_depth: [0; STATS_MAX_DEPTH],
tt_fail_depth_by_depth: [0; STATS_MAX_DEPTH],
tt_fail_bound_by_depth: [0; STATS_MAX_DEPTH],
lmr_to_depth1_from: [0; STATS_MAX_DEPTH],
depth1_children_total: 0,
depth1_children_tt_cut: 0,
tt_write_by_depth: [0; STATS_MAX_DEPTH],
razoring_by_depth: [0; STATS_MAX_DEPTH],
futility_by_depth: [0; STATS_MAX_DEPTH],
nmp_cutoff_by_depth: [0; STATS_MAX_DEPTH],
first_move_cutoff_by_depth: [0; STATS_MAX_DEPTH],
cutoff_by_depth: [0; STATS_MAX_DEPTH],
move_count_sum_by_depth: [0; STATS_MAX_DEPTH],
lmr_reduction_histogram: [0; 16],
lmr_new_depth_histogram: [0; STATS_MAX_DEPTH],
qs_nodes: 0,
qs_tt_hit: 0,
qs_tt_cutoff: 0,
qs_stand_pat_cutoff: 0,
qs_moves_generated: 0,
qs_moves_searched: 0,
qs_see_pruned: 0,
qs_futility_pruned: 0,
qs_history_pruned: 0,
qs_see_margin_pruned: 0,
qs_in_check_nodes: 0,
lmr_cut_node_applied: 0,
lmr_cut_node_to_depth1: 0,
lmr_non_cut_node_applied: 0,
lmr_non_cut_node_to_depth1: 0,
}
}
}
#[cfg(feature = "search-stats")]
impl SearchStats {
pub fn reset(&mut self) {
*self = Self::default();
}
pub fn format_report(&self) -> String {
let mut report = String::new();
report.push_str("=== Search Statistics ===\n");
report.push_str(&format!("Nodes searched: {:>12}\n", self.nodes_searched));
report.push_str(&format!("TT cutoffs: {:>12}\n", self.tt_cutoff));
report.push_str("--- Pre-Move Pruning ---\n");
report.push_str(&format!("NMP attempted: {:>12}\n", self.nmp_attempted));
report.push_str(&format!("NMP cutoffs: {:>12}\n", self.nmp_cutoff));
if self.nmp_candidate_nodes > 0 {
let success_rate = self.nmp_cutoff as f64 / self.nmp_attempted.max(1) as f64 * 100.0;
report.push_str(&format!("NMP success rate: {:>11.1}%\n", success_rate));
report.push_str(" --- NMP Skip Reasons ---\n");
report.push_str(&format!(" Candidate nodes: {:>12}\n", self.nmp_candidate_nodes));
report.push_str(&format!(" Skip (not cut): {:>12}\n", self.nmp_skip_not_cut_node));
report.push_str(&format!(" Skip (in check): {:>12}\n", self.nmp_skip_in_check));
report.push_str(&format!(" Skip (eval low): {:>12}\n", self.nmp_skip_eval_low));
report.push_str(&format!(" Skip (excluded): {:>12}\n", self.nmp_skip_excluded));
report.push_str(&format!(" Skip (prev null): {:>12}\n", self.nmp_skip_prev_null));
}
report.push_str(&format!("Razoring: {:>12}\n", self.razoring_applied));
report.push_str(&format!("Futility (static): {:>12}\n", self.futility_pruned));
report.push_str(&format!("ProbCut attempted: {:>12}\n", self.probcut_attempted));
report.push_str(&format!("ProbCut cutoffs: {:>12}\n", self.probcut_cutoff));
report.push_str("--- Move Loop ---\n");
report.push_str(&format!("Move loop pruned: {:>12}\n", self.move_loop_pruned));
report.push_str(&format!("LMR applied: {:>12}\n", self.lmr_applied));
report.push_str(&format!("LMR re-search: {:>12}\n", self.lmr_research));
report.push_str("--- Extensions ---\n");
report.push_str(&format!("Singular extension: {:>12}\n", self.singular_extension));
report.push_str(&format!("Multi-cut: {:>12}\n", self.multi_cut));
report.push_str("--- Nodes by Depth ---\n");
for (d, &count) in self.nodes_by_depth.iter().enumerate() {
if count > 0 {
let tt_cut = self.tt_cutoff_by_depth[d];
let tt_rate = if count > 0 {
(tt_cut as f64 / count as f64 * 100.0) as u32
} else {
0
};
report.push_str(&format!(
" depth {:>2}: {:>10} nodes, {:>8} TT cuts ({:>2}%)\n",
d, count, tt_cut, tt_rate
));
}
}
report.push_str("--- TT Details (depth 1) ---\n");
let probe = self.tt_probe_by_depth[1];
let hit = self.tt_hit_by_depth[1];
let cut = self.tt_cutoff_by_depth[1];
let fail_depth = self.tt_fail_depth_by_depth[1];
let fail_bound = self.tt_fail_bound_by_depth[1];
if probe > 0 {
report.push_str(&format!(
" Probes: {}, Hits: {} ({:.1}%), Cuts: {} ({:.1}%)\n",
probe,
hit,
hit as f64 / probe as f64 * 100.0,
cut,
cut as f64 / probe as f64 * 100.0
));
report
.push_str(&format!(" Fail reasons: depth={}, bound={}\n", fail_depth, fail_bound));
}
report.push_str("--- LMR to Depth 1 Sources ---\n");
for (d, &count) in self.lmr_to_depth1_from.iter().enumerate() {
if count > 0 {
report.push_str(&format!(" from depth {:>2}: {:>8} nodes\n", d, count));
}
}
report.push_str("--- TT Writes by Depth ---\n");
for (d, &count) in self.tt_write_by_depth.iter().enumerate() {
if count > 0 {
let probe = self.tt_probe_by_depth[d];
let ratio = if probe > 0 {
format!("{:.1}x", count as f64 / probe as f64)
} else {
"-".to_string()
};
report.push_str(&format!(
" depth {:>2}: {:>8} writes (probe ratio: {})\n",
d, count, ratio
));
}
}
report.push_str("--- Early Return by Depth ---\n");
for d in 0..STATS_MAX_DEPTH {
let razoring = self.razoring_by_depth[d];
let futility = self.futility_by_depth[d];
let nmp = self.nmp_cutoff_by_depth[d];
let nodes = self.nodes_by_depth[d];
if razoring > 0 || futility > 0 || nmp > 0 {
report.push_str(&format!(
" depth {:>2}: razoring={:>6}, futility={:>6}, nmp={:>6} (nodes={})\n",
d, razoring, futility, nmp, nodes
));
}
}
report.push_str("--- Move Ordering Quality (First Move Cutoff Rate) ---\n");
for d in 0..STATS_MAX_DEPTH {
let first_cut = self.first_move_cutoff_by_depth[d];
let total_cut = self.cutoff_by_depth[d];
if total_cut > 0 {
let rate = first_cut as f64 / total_cut as f64 * 100.0;
report.push_str(&format!(
" depth {:>2}: {:>6}/{:>6} ({:>5.1}%)\n",
d, first_cut, total_cut, rate
));
}
}
report.push_str("--- Average Move Count at Cutoff ---\n");
for d in 0..STATS_MAX_DEPTH {
let total_cut = self.cutoff_by_depth[d];
let move_count_sum = self.move_count_sum_by_depth[d];
if total_cut > 0 {
let avg = move_count_sum as f64 / total_cut as f64;
report.push_str(&format!(
" depth {:>2}: {:>6.2} avg ({} cutoffs)\n",
d, avg, total_cut
));
}
}
report.push_str("--- LMR Reduction Histogram (r/1024) ---\n");
for (r, &count) in self.lmr_reduction_histogram.iter().enumerate() {
if count > 0 {
let label = if r == 15 {
"15+".to_string()
} else {
format!("{:>2}", r)
};
report.push_str(&format!(
" r={}: {:>8} ({:>5.1}%)\n",
label,
count,
count as f64 / self.lmr_applied as f64 * 100.0
));
}
}
report.push_str("--- LMR New Depth Distribution ---\n");
for d in 0..STATS_MAX_DEPTH {
let count = self.lmr_new_depth_histogram[d];
if count > 0 {
report.push_str(&format!(
" new_depth {:>2}: {:>8} ({:>5.1}%)\n",
d,
count,
count as f64 / self.lmr_applied as f64 * 100.0
));
}
}
report.push_str("--- Quiescence Search Statistics ---\n");
report.push_str(&format!("QS nodes: {:>12}\n", self.qs_nodes));
if self.qs_nodes > 0 {
let qs_nodes = self.qs_nodes as f64;
report.push_str(&format!(
" In-check nodes: {:>12} ({:.1}%)\n",
self.qs_in_check_nodes,
self.qs_in_check_nodes as f64 / qs_nodes * 100.0
));
report.push_str(&format!(
" TT hit: {:>12} ({:.1}%)\n",
self.qs_tt_hit,
self.qs_tt_hit as f64 / qs_nodes * 100.0
));
report.push_str(&format!(
" TT cutoff: {:>12} ({:.1}%)\n",
self.qs_tt_cutoff,
self.qs_tt_cutoff as f64 / qs_nodes * 100.0
));
report.push_str(&format!(
" Stand-pat cutoff: {:>12} ({:.1}%)\n",
self.qs_stand_pat_cutoff,
self.qs_stand_pat_cutoff as f64 / qs_nodes * 100.0
));
report.push_str(&format!(
" Moves generated: {:>12} ({:.1} avg/node)\n",
self.qs_moves_generated,
self.qs_moves_generated as f64 / qs_nodes
));
report.push_str(&format!(
" Moves searched: {:>12} ({:.1} avg/node)\n",
self.qs_moves_searched,
self.qs_moves_searched as f64 / qs_nodes
));
}
let qs_total_pruned = self.qs_see_pruned
+ self.qs_futility_pruned
+ self.qs_history_pruned
+ self.qs_see_margin_pruned;
if qs_total_pruned > 0 {
report.push_str(" --- QS Pruning ---\n");
report.push_str(&format!(" SEE (capture): {:>12}\n", self.qs_see_pruned));
report.push_str(&format!(" Futility: {:>12}\n", self.qs_futility_pruned));
report.push_str(&format!(" History: {:>12}\n", self.qs_history_pruned));
report.push_str(&format!(" SEE margin: {:>12}\n", self.qs_see_margin_pruned));
}
report.push_str("--- LMR Cut Node Analysis ---\n");
if self.lmr_cut_node_applied > 0 {
let cut_rate =
self.lmr_cut_node_to_depth1 as f64 / self.lmr_cut_node_applied as f64 * 100.0;
report.push_str(&format!(
" cut_node: {:>8} LMR, {:>8} to d1 ({:.1}%)\n",
self.lmr_cut_node_applied, self.lmr_cut_node_to_depth1, cut_rate
));
}
if self.lmr_non_cut_node_applied > 0 {
let non_cut_rate = self.lmr_non_cut_node_to_depth1 as f64
/ self.lmr_non_cut_node_applied as f64
* 100.0;
report.push_str(&format!(
" non_cut_node: {:>8} LMR, {:>8} to d1 ({:.1}%)\n",
self.lmr_non_cut_node_applied, self.lmr_non_cut_node_to_depth1, non_cut_rate
));
}
report
}
}
#[cfg(feature = "search-stats")]
macro_rules! inc_stat {
($st:expr, $field:ident) => {
$st.stats.$field += 1;
};
}
#[cfg(not(feature = "search-stats"))]
macro_rules! inc_stat {
($self:expr, $field:ident) => {};
}
#[cfg(feature = "search-stats")]
macro_rules! inc_stat_by_depth {
($st:expr, $field:ident, $depth:expr) => {
let d = ($depth as usize).min($crate::search::stats::STATS_MAX_DEPTH - 1);
$st.stats.$field[d] += 1;
};
}
#[cfg(not(feature = "search-stats"))]
macro_rules! inc_stat_by_depth {
($self:expr, $field:ident, $depth:expr) => {};
}
pub(super) use inc_stat;
pub(super) use inc_stat_by_depth;