blunders_engine/
engine.rs

1//! Engine struct acts as a simplified API for the various parts of the Blunders engine.
2
3use std::sync::atomic::{AtomicBool, Ordering};
4use std::sync::mpsc::{self, Sender};
5use std::sync::Arc;
6use std::thread::JoinHandle;
7
8use crate::error::{self, ErrorKind};
9use crate::position::{Game, Position};
10use crate::search::{self, SearchResult};
11use crate::timeman::Mode;
12use crate::TranspositionTable;
13
14/// EngineBuilder allows for parameters of an Engine to be set and built once,
15/// avoiding repeating costly initialization steps of making then changing an Engine.
16///
17/// Default values:
18///
19/// * `game`: Starting chess position
20/// * `transpositions_mb`: 1 megabytes
21/// * `num_threads`: 1,
22/// * `debug`: true
23#[derive(Debug, Clone, Eq, PartialEq)]
24pub struct EngineBuilder {
25    game: Game,
26    transpositions_mb: usize,
27    num_threads: usize,
28    debug: bool,
29}
30
31impl EngineBuilder {
32    /// Create a new default EngineBuilder.
33    pub fn new() -> Self {
34        Self {
35            game: Game::start_position(),
36            transpositions_mb: 1,
37            num_threads: 1,
38            debug: true,
39        }
40    }
41
42    /// Create and return a new Engine.
43    pub fn build(&self) -> Engine {
44        let tt = Arc::new(TranspositionTable::with_mb(self.transpositions_mb));
45        let stopper = Arc::new(AtomicBool::new(false));
46
47        Engine {
48            game: self.game.clone(),
49            tt,
50            stopper,
51            debug: self.debug,
52            search_handle: None,
53        }
54    }
55
56    /// Set the Engine's initial game state.
57    pub fn game(mut self, game: Game) -> Self {
58        self.game = game;
59        self
60    }
61
62    /// Set the Engine's initial game state from a position with no history.
63    pub fn position(mut self, position: Position) -> Self {
64        self.game = Game::from(position);
65        self
66    }
67
68    /// Set the engine's initial search thread pool size.
69    pub fn threads(mut self, num_threads: usize) -> Self {
70        self.num_threads = num_threads;
71        self
72    }
73
74    /// Set the engine's initial transposition table size in megabytes.
75    pub fn transpositions_mb(mut self, transpositions_mb: usize) -> Self {
76        self.transpositions_mb = transpositions_mb;
77        self
78    }
79
80    /// Set whether the engine begins in debug mode.
81    pub fn debug(mut self, debug: bool) -> Self {
82        self.debug = debug;
83        self
84    }
85}
86
87/// Engine wraps up all parameters required for running any kind of search.
88/// It is stateful because to properly evaluate a chess position the history of
89/// moves for the current game need to be tracked.
90///
91/// If a new game is going to be started, the engine needs to be told so.
92pub struct Engine {
93    // Search fields
94    game: Game,
95    tt: Arc<TranspositionTable>,
96    stopper: Arc<AtomicBool>,
97    debug: bool,
98
99    // Meta fields
100    search_handle: Option<JoinHandle<()>>,
101}
102
103impl Engine {
104    pub fn new() -> Self {
105        Self {
106            game: Game::from(Position::start_position()),
107            tt: Arc::new(TranspositionTable::new()),
108            stopper: Arc::new(AtomicBool::new(false)),
109            debug: true,
110            search_handle: None,
111        }
112    }
113
114    /// Returns reference to current game of engine.
115    pub fn game(&self) -> &Game {
116        &self.game
117    }
118
119    /// Returns reference to current debug flag of engine.
120    pub fn debug(&self) -> &bool {
121        &self.debug
122    }
123
124    /// Returns reference to engine's transposition table.
125    pub fn transposition_table(&self) -> &TranspositionTable {
126        &self.tt
127    }
128
129    /// Set the game or position for evaluation.
130    pub fn set_game<T: Into<Game>>(&mut self, game: T) {
131        self.game = game.into();
132    }
133
134    /// Update the engine's debug parameter.
135    pub fn set_debug(&mut self, new_debug: bool) {
136        self.debug = new_debug;
137    }
138
139    /// Informs engine that next search will be from a new game.
140    /// Returns Ok if engine succeeded in changing state for a new game, Err otherwise.
141    pub fn new_game(&mut self) -> error::Result<()> {
142        self.try_clear_transpositions()
143    }
144
145    /// Attempt to set a new size for the transposition table in Megabytes.
146    /// Table is set only if there is exactly one reference to the table (not used in search).
147    /// Returns Ok(new capacity) on success or Err if no change was made.
148    pub fn try_set_transpositions_mb(&mut self, new_mb: usize) -> error::Result<usize> {
149        Arc::get_mut(&mut self.tt)
150            .map(|inner_tt| inner_tt.set_mb(new_mb))
151            .ok_or(ErrorKind::EngineTranspositionTableInUse.into())
152    }
153
154    /// Attempt to clear the transposition table. Table is cleared only if there
155    /// are no other Arcs to the table.
156    /// Returns Ok on success or Err if the table was not cleared.
157    pub fn try_clear_transpositions(&mut self) -> error::Result<()> {
158        Arc::get_mut(&mut self.tt)
159            .map(|inner_tt| inner_tt.clear())
160            .ok_or(ErrorKind::EngineTranspositionTableInUse.into())
161    }
162
163    /// Run a blocking search.
164    pub fn search_sync(&mut self, mode: Mode) -> SearchResult {
165        // Block until a search is ready to run.
166        self.stop();
167        self.wait();
168        self.unstop();
169
170        let (sender, receiver) = mpsc::channel();
171        self.search(mode, sender).unwrap();
172        self.wait();
173        receiver.recv().unwrap()
174    }
175
176    /// Run a non-blocking search.
177    /// The engine only runs one search at a time, so if it is not ready, it fails to begin.
178    /// If the engine is available for searching, it ensures its stopper is unset.
179    pub fn search<T>(&mut self, mode: Mode, sender: Sender<T>) -> error::Result<()>
180    where
181        T: From<SearchResult> + Send + 'static,
182    {
183        if self.search_handle.is_none() {
184            self.unstop();
185
186            let handle = search::search_nonblocking(
187                self.game.clone(),
188                mode,
189                Arc::clone(&self.tt),
190                Arc::clone(&self.stopper),
191                self.debug,
192                sender,
193            );
194            self.search_handle = Some(handle);
195
196            Ok(())
197        } else {
198            Err((ErrorKind::EngineAlreadySearching, "failed to begin search").into())
199        }
200    }
201
202    pub fn ponder(&self) {
203        todo!()
204    }
205
206    /// Informs the active search to stop searching as soon as possible.
207    pub fn stop(&self) {
208        self.stopper.store(true, Ordering::Relaxed);
209    }
210
211    /// Resets stopper flag.
212    pub fn unstop(&self) {
213        self.stopper.store(false, Ordering::Relaxed);
214    }
215
216    /// Engine blocks thread until search is completed.
217    pub fn wait(&mut self) {
218        let handle_opt = self.search_handle.take();
219
220        if let Some(handle) = handle_opt {
221            handle.join().unwrap();
222        }
223    }
224
225    /// Returns true if the engine is ready to start a search.
226    /// Only one search may run at a time, so if a search is in progress, engine is not ready.
227    pub fn ready(&self) -> bool {
228        self.search_handle.is_none()
229    }
230
231    /// Consumes and shuts down the Engine. Signals any threads to stop searching
232    /// and waits for internal resources to close first.
233    /// The engine will normally close up properly when dropped,
234    /// however this function provides a way to do it explicitly
235    /// directly from the API.
236    pub fn shutdown(self) {}
237}
238
239impl Default for Engine {
240    fn default() -> Self {
241        Self::new()
242    }
243}
244
245impl Drop for Engine {
246    fn drop(&mut self) {
247        self.stop();
248        self.wait();
249    }
250}