blunders_engine/search/
mod.rs

1//! Search functions.
2
3mod alpha_beta;
4mod history;
5mod ids;
6mod minimax;
7mod negamax;
8mod quiescence;
9
10pub use alpha_beta::*;
11pub use history::*;
12pub use ids::*;
13pub use minimax::*;
14pub use negamax::*;
15pub use quiescence::*;
16
17use std::fmt::{self, Display};
18use std::sync::{atomic::AtomicBool, mpsc, Arc};
19use std::thread;
20use std::time::Duration;
21
22use crate::arrayvec::display;
23use crate::coretypes::{Color, Cp, Move, PlyKind};
24use crate::movelist::Line;
25use crate::timeman::Mode;
26use crate::transposition::TranspositionTable;
27use crate::{Game, Position};
28
29/// The results found from running a search on some root position.
30#[derive(Debug, Clone)]
31pub struct SearchResult {
32    /// The best move to make for a position discovered from search.
33    pub best_move: Move,
34    /// The centipawn score of making the best move, with absolute Cp (+White, -Black).
35    pub score: Cp,
36    /// The principal variation, or a sequence of the best moves that result in an evaluation of at least `score` Cp.
37    pub pv: Line,
38    /// The player to move for the root position that was searched.
39    pub player: Color,
40    /// Depth (aka ply, half move) that was searched to. This depth is only fully searched if the `stopped` flag is false.
41    pub depth: PlyKind,
42    /// Total number of nodes visited in a search, including main search nodes and quiescence nodes.
43    pub nodes: u64,
44    /// Total number of nodes visited in a quiescence search.
45    pub q_nodes: u64,
46    /// Total time elapsed from the start to the end of a search.
47    pub elapsed: Duration,
48    /// Total time elapsed spent in quiescence search, within main search.
49    pub q_elapsed: Duration,
50    /// Flag that indicates this search was aborted.
51    pub stopped: bool,
52
53    /// Number of nodes where a beta-cutoff was performed.
54    pub cut_nodes: u64,
55    /// Number of nodes that improved local alpha value without reaching beta.
56    pub pv_nodes: u64,
57    /// Number of nodes that did not improve alpha or result in a cutoff.
58    pub all_nodes: u64,
59    /// Number of times a position was found in the transposition table.
60    pub tt_hits: u64,
61    /// Number of times a tt hit score could be used and returned immediately.
62    pub tt_cuts: u64,
63}
64
65impl SearchResult {
66    /// Add the following metrics from `other` to this Result:
67    /// nodes, q_nodes, elapsed, q_elapsed, beta_cutoffs, alpha_increases, tt_hits, tt_cuts.
68    pub fn add_metrics(&mut self, other: Self) {
69        self.nodes += other.nodes;
70        self.q_nodes += other.q_nodes;
71        self.elapsed += other.elapsed;
72        self.q_elapsed += other.q_elapsed;
73
74        self.cut_nodes += other.cut_nodes;
75        self.pv_nodes += other.pv_nodes;
76        self.all_nodes += other.all_nodes;
77        self.tt_hits += other.tt_hits;
78        self.tt_cuts += other.tt_cuts;
79    }
80
81    /// Get average nodes per second of search.
82    pub fn nps(&self) -> f64 {
83        (self.nodes as f64 / self.elapsed.as_secs_f64()).round()
84    }
85
86    /// Get average nodes per second of search for only quiescence search.
87    pub fn q_nps(&self) -> f64 {
88        (self.q_nodes as f64 / self.q_elapsed.as_secs_f64()).round()
89    }
90
91    /// Returns the percentage of elapsed time of search that was in quiescence.
92    ///
93    /// Example: elapsed=2.0s, q_elapsed=0.5s, quiescence_ratio=0.25
94    pub fn quiescence_ratio(&self) -> f64 {
95        assert!(
96            self.q_elapsed <= self.elapsed,
97            "logical error for q_elapsed to be greater than elapsed"
98        );
99        self.q_elapsed.as_secs_f64() / self.elapsed.as_secs_f64()
100    }
101
102    /// Returns the percentage of tt hits that result in tt cuts.
103    pub fn tt_cut_ratio(&self) -> f64 {
104        self.tt_cuts as f64 / self.tt_hits as f64
105    }
106
107    /// Converts the score of the search into one that is relative to search's root player.
108    pub fn relative_score(&self) -> Cp {
109        self.score * self.player.sign()
110    }
111
112    /// Converts the score of the search into one that is absolute, with White as + and Black as -.
113    pub fn absolute_score(&self) -> Cp {
114        self.score
115    }
116
117    /// Returns the color who is leading in the search of the root position, or None if drawn.
118    pub fn leading(&self) -> Option<Color> {
119        match self.absolute_score().signum() {
120            1 => Some(Color::White),
121            -1 => Some(Color::Black),
122            _ => None,
123        }
124    }
125}
126
127/// Note that this default is technically illegal and does not represent any actual search.
128impl Default for SearchResult {
129    fn default() -> Self {
130        Self {
131            best_move: Move::illegal(),
132            score: Cp(0),
133            pv: Line::new(),
134            player: Color::White,
135            depth: 0,
136            nodes: 0,
137            q_nodes: 0,
138            elapsed: Duration::ZERO,
139            q_elapsed: Duration::ZERO,
140            stopped: false,
141            cut_nodes: 0,
142            pv_nodes: 0,
143            all_nodes: 0,
144            tt_hits: 0,
145            tt_cuts: 0,
146        }
147    }
148}
149
150impl Display for SearchResult {
151    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
152        let mut displayed = String::new();
153        displayed.push_str("SearchResult {\n");
154        displayed.push_str(&format!("    best_move: {}\n", self.best_move));
155        displayed.push_str(&format!("    abs_score: {}\n", self.absolute_score()));
156        displayed.push_str(&format!("    pv       : {}\n", display(&self.pv)));
157        displayed.push_str(&format!("    player   : {}\n", self.player));
158        displayed.push_str(&format!("    depth    : {}\n", self.depth));
159        displayed.push_str(&format!("    nodes    : {}\n", self.nodes));
160        displayed.push_str(&format!("    nps      : {}\n", self.nps()));
161        displayed.push_str(&format!(
162            "    elapsed  : {}.{}s\n",
163            self.elapsed.as_secs(),
164            self.elapsed.subsec_millis()
165        ));
166        displayed.push_str(&format!("    q_ratio  : {:.2}\n", self.quiescence_ratio()));
167        displayed.push_str(&format!("    stopped  : {}\n", self.stopped));
168        displayed.push_str(&format!("    pv_nodes : {}\n", self.pv_nodes));
169        displayed.push_str(&format!("    cut_nodes: {}\n", self.cut_nodes));
170        displayed.push_str(&format!("    all_nodes: {}\n", self.all_nodes));
171        displayed.push_str(&format!("    tt_cuts  : {}\n", self.tt_cuts));
172        displayed.push_str(&format!("    tt_hits  : {}\n", self.tt_hits));
173        displayed.push_str(&format!("    tt_ratio : {:.2}\n", self.tt_cut_ratio()));
174        displayed.push_str("}\n");
175
176        write!(f, "{}", displayed)
177    }
178}
179
180/// Blunders Engine primary position search function. WIP.
181pub fn search(position: Position, ply: PlyKind, tt: &TranspositionTable) -> SearchResult {
182    assert_ne!(ply, 0);
183    let mode = Mode::depth(ply, None);
184    let history = History::new(&position.into(), tt.zobrist_table());
185    ids(
186        position,
187        mode,
188        history,
189        tt,
190        Arc::new(AtomicBool::new(false)),
191        true,
192    )
193}
194
195/// Blunders Engine non-blocking search function. This runs the search on a separate thread.
196/// When the search has been completed, it returns the value by sending it over the given Sender.
197///
198/// # Arguments
199///
200/// * `game`: State of the current active game
201/// * `mode`: Mode of search determines when the search stops and how deep it searches
202/// * `tt`: Shared Transposition table. This may or may not lock the table for the duration of the search
203/// * `stopper`: Tell search to stop early from an external source
204/// * `debug`: When true prints extra debugging information
205/// * `sender`: Channel to send search result over
206pub fn search_nonblocking<P, T>(
207    game: P,
208    mode: Mode,
209    tt: Arc<TranspositionTable>,
210    stopper: Arc<AtomicBool>,
211    debug: bool,
212    sender: mpsc::Sender<T>,
213) -> thread::JoinHandle<()>
214where
215    T: 'static + Send + From<SearchResult>,
216    P: Into<Game>,
217{
218    let game: Game = game.into();
219    let position = game.position;
220    let history = History::new(&game, tt.zobrist_table());
221
222    thread::spawn(move || {
223        let search_result = ids(position, mode, history, &tt, stopper, debug);
224        sender.send(search_result.into()).unwrap();
225    })
226}