async_uci/
engine.rs

1use crate::parse::{parse_uci, OptionType, UCI};
2use anyhow::{bail, Result};
3use async_trait::async_trait;
4use std::{
5    fmt::Display,
6    process::Stdio,
7    sync::{Arc, Mutex},
8};
9use tokio::{
10    io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
11    process::{Child, ChildStdin, ChildStdout, Command},
12};
13
14/// ChessEngine trait can be implemented for structures that implement the UCI Protocol
15#[async_trait]
16pub trait ChessEngine {
17    /// Start the UCI Protocol
18    async fn start_uci(&mut self) -> Result<()>;
19
20    /// Notify engine of new game start
21    async fn new_game(&mut self) -> Result<()>;
22
23    /// Notify engine of new position to search
24    async fn set_position(&mut self, position: &str) -> Result<()>;
25
26    /// Notify engine to search for best move until explicitly stopped
27    async fn go_infinite(&mut self) -> Result<()>;
28
29    /// Notify engine to search for best move to a certain depth
30    async fn go_depth(&mut self, plies: usize) -> Result<()>;
31
32    /// Notify engine to search for best move for a set time
33    async fn go_time(&mut self, ms: usize) -> Result<()>;
34
35    /// Notify engine to search for a mate in a certain number of moves
36    async fn go_mate(&mut self, mate_in: usize) -> Result<()>;
37
38    /// Notify engine to stop current search
39    async fn stop(&mut self) -> Result<()>;
40
41    /// Retrieve the latest evaluation from the engine
42    async fn get_evaluation(&mut self) -> Option<Evaluation>;
43
44    /// Retrieve the list of available options from the engine
45    async fn get_options(&mut self) -> Result<Vec<EngineOption>>;
46
47    /// Set an option in the engine
48    async fn set_option(&mut self, option: String, value: String) -> Result<()>;
49}
50
51/// Engine can be created to spawn any Chess Engine that implements the UCI Protocol
52pub struct Engine {
53    stdin: ChildStdin,
54    state: EngineState,
55    _proc: Child,
56}
57
58impl Engine {
59    pub async fn new(exe_path: &str) -> Result<Self> {
60        let (proc, stdin, stdout) = spawn_process(exe_path)?;
61        let state = EngineState::new(stdout).await;
62        Ok(Engine {
63            state: state,
64            stdin: stdin,
65            _proc: proc,
66        })
67    }
68    /// Send a command to the engine
69    async fn send_command(&mut self, command: String) -> Result<()> {
70        self.stdin.write_all(command.as_bytes()).await?;
71        self.stdin.flush().await?;
72        Ok(())
73    }
74
75    /// Check if the expected state is the current engine state
76    async fn _expect_state(&mut self, exp_state: &EngineStateEnum) -> Result<()> {
77        let state = self.state.state.lock().expect("couldn't aquire state lock");
78        if *exp_state == *state {
79            return Ok(());
80        }
81        bail!("engine didn't respond with {:?}", exp_state)
82    }
83
84    /// Check if the expected state is the current engine state, retries a couple of times
85    /// waiting between attempts.
86    async fn expect_state(&mut self, exp_state: EngineStateEnum) -> Result<()> {
87        for _ in 0..10 {
88            match self._expect_state(&exp_state).await {
89                Ok(_) => return Ok(()),
90                Err(_) => tokio::time::sleep(std::time::Duration::from_millis(100)).await,
91            };
92        }
93        bail!("engine didn't respond with {:?}", exp_state)
94    }
95
96    /// Check if the engine initialized UCI
97    async fn expect_uciok(&mut self) -> Result<()> {
98        self.expect_state(EngineStateEnum::Initialized).await
99    }
100
101    /// Check if the engine is ready to receive commands
102    async fn expect_readyok(&mut self) -> Result<()> {
103        self.expect_state(EngineStateEnum::Ready).await
104    }
105
106    /// Change current engine state
107    async fn set_state(&mut self, new_state: EngineStateEnum) -> Result<()> {
108        // TODO: Return old state
109        let mut state = self.state.state.lock().expect("couldn't acquire lock");
110        *state = new_state;
111        Ok(())
112    }
113}
114
115/// Spawn a subprocess and return handles for stdin and stdout
116fn spawn_process(exe_path: &str) -> Result<(Child, ChildStdin, ChildStdout)> {
117    let mut cmd = Command::new(exe_path);
118    cmd.stdin(Stdio::piped());
119    cmd.stdout(Stdio::piped());
120    let mut proc = cmd.spawn()?;
121    let stdout = proc.stdout.take().expect("no stdout available");
122    let stdin = proc.stdin.take().expect("no stdin available");
123    Ok((proc, stdin, stdout))
124}
125
126#[async_trait]
127impl ChessEngine for Engine {
128    async fn start_uci(&mut self) -> Result<()> {
129        self.send_command("uci\n".to_string()).await?;
130        self.expect_uciok().await?;
131        self.send_command("isready\n".to_string()).await?;
132        self.expect_readyok().await?;
133        Ok(())
134    }
135
136    async fn new_game(&mut self) -> Result<()> {
137        self.send_command("ucinewgame\n".to_string()).await?;
138        self.set_state(EngineStateEnum::Initialized).await?;
139        self.send_command("isready\n".to_string()).await?;
140        self.expect_readyok().await?;
141        Ok(())
142    }
143
144    async fn set_position(&mut self, fen: &str) -> Result<()> {
145        let cmd = format!("position fen {}\n", fen);
146        self.send_command(cmd.to_string()).await
147    }
148
149    async fn go_infinite(&mut self) -> Result<()> {
150        self.send_command("go infinite\n".to_string()).await?;
151        self.set_state(EngineStateEnum::Thinking).await?;
152        Ok(())
153    }
154
155    async fn go_depth(&mut self, depth: usize) -> Result<()> {
156        self.send_command(format!("go depth {}\n", depth).to_string())
157            .await?;
158        self.set_state(EngineStateEnum::Thinking).await?;
159        Ok(())
160    }
161
162    async fn go_time(&mut self, ms: usize) -> Result<()> {
163        self.send_command(format!("go movetime {}\n", ms).to_string())
164            .await?;
165        self.set_state(EngineStateEnum::Thinking).await?;
166        Ok(())
167    }
168
169    async fn go_mate(&mut self, mate_in: usize) -> Result<()> {
170        self.send_command(format!("go mate {}\n", mate_in).to_string())
171            .await?;
172        self.set_state(EngineStateEnum::Thinking).await?;
173        Ok(())
174    }
175
176    async fn stop(&mut self) -> Result<()> {
177        self.send_command("stop\n".to_string()).await?;
178        self.set_state(EngineStateEnum::Initialized).await?;
179        Ok(())
180    }
181
182    async fn get_evaluation(&mut self) -> Option<Evaluation> {
183        let ev = self.state.evaluation.lock().expect("couldn't acquire lock");
184        return match &*ev {
185            Some(e) => Some(e.clone()),
186            None => None,
187        };
188    }
189
190    async fn get_options(&mut self) -> Result<Vec<EngineOption>> {
191        let options = self.state.options.lock().expect("couldn't acquire lock");
192        Ok(options.clone())
193    }
194
195    async fn set_option(&mut self, option: String, value: String) -> Result<()> {
196        let cmd = format!("setoption name {} value {}\n", option, value);
197        self.send_command(cmd).await
198    }
199}
200
201/// Engine evaluation info
202#[derive(Debug, Clone, PartialEq)]
203pub struct Evaluation {
204    pub score: isize,
205    pub mate: isize,
206    pub depth: isize,
207    pub nodes: isize,
208    pub seldepth: isize,
209    pub multipv: isize,
210    pub pv: Vec<String>,
211    pub time: isize,
212}
213
214impl Default for Evaluation {
215    /// Create evaluation with empty values
216    fn default() -> Self {
217        Evaluation {
218            score: 0,
219            mate: 0,
220            depth: 0,
221            nodes: 0,
222            seldepth: 0,
223            multipv: 0,
224            pv: vec![],
225            time: 0,
226        }
227    }
228}
229
230impl Display for Evaluation {
231    /// The alternate ("{:#}") operator will add the moves in pv to the output
232    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
233        f.write_fmt(format_args!(
234            "score: {} mate: {} depth: {} nodes: {} seldepth: {} multipv: {} time: {}",
235            self.score, self.mate, self.depth, self.nodes, self.seldepth, self.multipv, self.time
236        ))?;
237        if f.alternate() {
238            f.write_fmt(format_args!("\npv: {}", self.pv.join(", ")))?;
239        }
240        Ok(())
241    }
242}
243
244/// Posible engine states
245#[derive(PartialEq, Debug)]
246enum EngineStateEnum {
247    Uninitialized,
248    Initialized,
249    Ready,
250    Thinking,
251}
252
253#[derive(PartialEq, Debug, Clone)]
254pub struct EngineOption {
255    pub name: String,
256    pub opt_type: OptionType,
257}
258
259/// Engine state handler with async stdout parsing
260struct EngineState {
261    state: Arc<Mutex<EngineStateEnum>>,
262    evaluation: Arc<Mutex<Option<Evaluation>>>,
263    options: Arc<Mutex<Vec<EngineOption>>>,
264}
265
266impl EngineState {
267    async fn new(stdout: ChildStdout) -> Self {
268        let ev = Arc::new(Mutex::new(None));
269        let state = Arc::new(Mutex::new(EngineStateEnum::Uninitialized));
270        let options = Arc::new(Mutex::new(Vec::new()));
271        let stdout = BufReader::new(stdout);
272        let engstate = EngineState {
273            state: state.clone(),
274            evaluation: ev.clone(),
275            options: options.clone(),
276        };
277        tokio::spawn(async move {
278            Self::process_stdout(stdout, state.clone(), ev.clone(), options.clone()).await
279        });
280        return engstate;
281    }
282
283    async fn process_stdout(
284        mut stdout: BufReader<ChildStdout>,
285        state: Arc<Mutex<EngineStateEnum>>,
286        ev: Arc<Mutex<Option<Evaluation>>>,
287        options: Arc<Mutex<Vec<EngineOption>>>,
288    ) {
289        loop {
290            let mut str = String::new();
291            stdout.read_line(&mut str).await.unwrap();
292            match parse_uci(str) {
293                Ok(UCI::UciOk) => {
294                    let mut state = state.lock().expect("couldn't aquire state lock");
295                    *state = EngineStateEnum::Initialized;
296                }
297                Ok(UCI::ReadyOk) => {
298                    let mut state = state.lock().expect("couldn't aquire state lock");
299                    *state = EngineStateEnum::Ready;
300                }
301                Ok(UCI::Info {
302                    cp,
303                    mate,
304                    depth,
305                    nodes,
306                    seldepth,
307                    time,
308                    multipv,
309                    pv,
310                }) => {
311                    let mut ev = ev.lock().expect("couldn't aquire ev lock");
312                    let def_ev = Evaluation::default();
313                    let prev_ev = match ev.as_ref() {
314                        Some(ev) => ev,
315                        None => &def_ev,
316                    };
317                    *ev = Some(Evaluation {
318                        score: cp.unwrap_or(prev_ev.score),
319                        mate: mate.unwrap_or(prev_ev.mate),
320                        depth: depth.unwrap_or(prev_ev.depth),
321                        nodes: nodes.unwrap_or(prev_ev.nodes),
322                        seldepth: seldepth.unwrap_or(prev_ev.seldepth),
323                        multipv: multipv.unwrap_or(prev_ev.multipv),
324                        pv: pv.unwrap_or(prev_ev.pv.clone()),
325                        time: time.unwrap_or(prev_ev.time),
326                    });
327                }
328                Ok(UCI::Option { name, opt_type }) => {
329                    let mut options = options.lock().expect("couldn't aquire options lock");
330                    options.push(EngineOption { name, opt_type });
331                }
332                _ => continue,
333            }
334        }
335    }
336}
337
338#[cfg(test)]
339mod test {
340    use anyhow::Result;
341
342    use crate::engine::{ChessEngine, Engine};
343
344    macro_rules! test_file {
345        ($fname:expr) => {
346            concat!(env!("CARGO_MANIFEST_DIR"), "/res/test/", $fname)
347        };
348    }
349
350    #[tokio::test]
351    async fn test_sf() -> Result<()> {
352        let mut sf = Engine::new(test_file!("fakefish.sh")).await?;
353        sf.start_uci().await?;
354        Ok(())
355    }
356}