async_uci/
parse.rs

1use anyhow::Result;
2use std::{collections::HashMap, fmt::Display, str::FromStr};
3use thiserror::Error;
4
5/// Supported UCI commands
6#[derive(PartialEq, Debug)]
7pub enum UCI {
8    /// Sent after the 'uci' command
9    UciOk,
10
11    /// Sent after the 'isready' command
12    ReadyOk,
13
14    /// Engine sending info to GUI
15    Info {
16        cp: Option<isize>,
17        mate: Option<isize>,
18        depth: Option<isize>,
19        seldepth: Option<isize>,
20        nodes: Option<isize>,
21        time: Option<isize>,
22        multipv: Option<isize>,
23        pv: Option<Vec<String>>,
24    },
25
26    /// Options can be set to modify the engine behaviour
27    Option { name: String, opt_type: OptionType },
28}
29
30/// Possible types for Engine Options
31#[derive(PartialEq, Debug, Clone)]
32pub enum OptionType {
33    Check {
34        default: bool,
35    },
36    Spin {
37        default: isize,
38        min: isize,
39        max: isize,
40    },
41    Combo {
42        default: String,
43        options: Vec<String>,
44    },
45    Button,
46    String {
47        default: String,
48    },
49}
50
51impl OptionType {
52    fn new(opt_type: String, line: String) -> Result<Self> {
53        Ok(match opt_type.as_str() {
54            "check" => OptionType::new_check(line)?,
55            "spin" => OptionType::new_spin(line)?,
56            "combo" => OptionType::new_combo(line)?,
57            "button" => OptionType::new_button()?,
58            "string" => OptionType::new_string(line)?,
59            _ => return Err(UCIError::ParseError.into()),
60        })
61    }
62
63    fn new_check(line: String) -> Result<Self> {
64        let words = vec!["default"];
65        let values = parse_line_values(line, words)?;
66        Ok(OptionType::Check {
67            default: values["default"].unwrap(),
68        })
69    }
70
71    fn new_spin(line: String) -> Result<Self> {
72        let words = vec!["default", "min", "max"];
73        let values = parse_line_values(line, words)?;
74        Ok(OptionType::Spin {
75            default: values["default"].unwrap(),
76            min: values["min"].unwrap(),
77            max: values["max"].unwrap(),
78        })
79    }
80
81    fn new_combo(line: String) -> Result<Self> {
82        let words = vec!["default"];
83        let values = parse_line_values(line.clone(), words)?;
84        let line: Vec<&str> = line.split_whitespace().collect();
85        let mut options = Vec::new();
86        // TODO: Check if combo options can have spaces, in which case this will give incorrect results
87        for ix in 0..line.len() {
88            if line[ix] == "var" {
89                options.push(line[ix + 1].to_string());
90            }
91        }
92        Ok(OptionType::Combo {
93            default: values["default"].clone().unwrap(),
94            options: options,
95        })
96    }
97
98    fn new_button() -> Result<Self> {
99        Ok(OptionType::Button)
100    }
101
102    fn new_string(line: String) -> Result<Self> {
103        let words = vec!["default"];
104        let values = parse_line_values(line, words)?;
105        Ok(OptionType::String {
106            default: values["default"].clone().unwrap(),
107        })
108    }
109}
110
111/// Errors produced from UCI parsing
112#[derive(Error, Debug)]
113pub enum UCIError {
114    /// Error parsing a UCI command
115    ParseError,
116}
117
118impl Display for UCIError {
119    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120        let data = match self {
121            UCIError::ParseError => "error parsing uci command",
122        };
123        return f.write_str(data);
124    }
125}
126
127/// Parse an UCI command
128pub fn parse_uci(line: String) -> Result<UCI> {
129    let line = line.trim().to_string();
130    let command = line.split_whitespace().next().unwrap_or("");
131    match command {
132        "info" => parse_info_line(line),
133        "uciok" => Ok(UCI::UciOk),
134        "readyok" => Ok(UCI::ReadyOk),
135        "option" => parse_option_line(line),
136        _ => Err(UCIError::ParseError.into()),
137    }
138}
139
140/// parse_line_values parses the value following each word in the given line.
141fn parse_line_values<T: FromStr + Default>(
142    line: String,
143    words: Vec<&str>,
144) -> Result<HashMap<String, Option<T>>> {
145    let line: Vec<&str> = line.split_whitespace().collect();
146    let mut values = HashMap::with_capacity(words.len());
147    for word in words.iter() {
148        let mut i = line.iter();
149        let value = match i.position(|x: &&str| x == word) {
150            Some(ix) => match line.get(ix + 1) {
151                Some(v) => v.parse::<T>().ok(),
152                None => Some(T::default()),
153            },
154            None => None,
155        };
156        values.insert(word.to_string(), value);
157    }
158    Ok(values)
159}
160
161/// Parse an info line for all supported metadata
162fn parse_info_line(line: String) -> Result<UCI> {
163    let words = vec![
164        "cp", "depth", "nodes", "seldepth", "mate", "time", "multipv",
165    ];
166    let values = parse_line_values(line.clone(), words)?;
167    return Ok(UCI::Info {
168        cp: values["cp"],
169        mate: values["mate"],
170        depth: values["depth"],
171        nodes: values["nodes"],
172        time: values["time"],
173        multipv: values["multipv"],
174        seldepth: values["seldepth"],
175        pv: parse_pv(line),
176    });
177}
178
179/// Parse an info line and return all the moves stated after 'pv'
180fn parse_pv(line: String) -> Option<Vec<String>> {
181    let line: Vec<&str> = line.split_whitespace().collect();
182    let mut pv = Vec::new();
183    let mut i = line.iter();
184    match i.position(|x: &&str| *x == "pv") {
185        Some(_) => {}
186        None => return None, // early return if no pv is found
187    };
188    while let Some(word) = i.next() {
189        pv.push(word.to_string());
190    }
191    Some(pv)
192}
193
194fn parse_option_line(line: String) -> Result<UCI> {
195    // FIXME: handle `name`s with spaces (i.e. `option name Clear Hash type button`)
196    let words = vec!["name", "type"];
197    let values = parse_line_values(line.clone(), words)?;
198    return Ok(UCI::Option {
199        name: values["name"].clone().unwrap(),
200        opt_type: OptionType::new(values["type"].clone().unwrap(), line)?,
201    });
202}
203
204#[cfg(test)]
205mod test {
206
207    use crate::parse::{parse_info_line, UCI};
208    use anyhow::Result;
209
210    macro_rules! test_info_line {
211        ($line:expr, $ev:expr) => {
212            let ev = parse_info_line($line.to_string())?;
213            assert_eq!(ev, $ev);
214        };
215    }
216
217    #[tokio::test]
218    async fn test_parse_info_line() -> Result<()> {
219        test_info_line!("info depth 1 seldepth 1 multipv 1 score cp 59 nodes 56 nps 56000 hashfull 0 tbhits 0 time 1", 
220            UCI::Info {
221                cp: Some(59),
222                mate: None,
223                depth: Some(1),
224                nodes: Some(56),
225                seldepth: Some(1),
226                multipv: Some(1),
227                time: Some(1),
228                pv: None,
229            }
230        );
231        test_info_line!("info depth 1 seldepth 1 multipv 1 score cp 59 nodes 56 nps 56000 hashfull 0 tbhits 0 time 1 pv d6f4 e3f4", 
232            UCI::Info {
233                cp: Some(59),
234                mate: None,
235                depth: Some(1),
236                nodes: Some(56),
237                seldepth: Some(1),
238                multipv: Some(1),
239                time: Some(1),
240                pv: Some(vec!["d6f4".to_string(), "e3f4".to_string()]),
241            }
242        );
243        test_info_line!(
244            "info depth 2 seldepth 2 multipv 1 score cp -27 nodes 227 nps 227000 hashfull 0 tbhits 0 time 1 pv a8b8 f4d6",
245            UCI::Info {
246                cp: Some(-27),
247                mate: None,
248                depth: Some(2),
249                nodes: Some(227),
250                seldepth: Some(2),
251                multipv: Some(1),
252                time: Some(1),
253                pv: Some(vec!["a8b8".to_string(), "f4d6".to_string()]),
254            }
255        );
256        test_info_line!(
257            "info depth 24 seldepth 33 multipv 1 score cp -195 nodes 2499457 nps 642203 hashfull 812 tbhits 0 time 3892 pv d8a5 a4a5 c6a5 f4d6 b7a6 d6c5 f6d7 c5a3 f7f6 e1g1 a8c8 b2b3 e8f7 f1c1 d7b6 f3e1 f5g6 f2f3 h8d8 e3e4 a5c6 e1d3 e6e5 d3c5 d5e4 d2e4 g6e4 c5e4",
258            UCI::Info {
259                cp: Some(-195),
260                mate: None,
261                depth: Some(24),
262                nodes: Some(2499457),
263                seldepth: Some(33),
264                multipv: Some(1),
265                time: Some(3892),
266                pv: Some(vec![
267                    "d8a5".to_string(),
268                    "a4a5".to_string(),
269                    "c6a5".to_string(),
270                    "f4d6".to_string(),
271                    "b7a6".to_string(),
272                    "d6c5".to_string(),
273                    "f6d7".to_string(),
274                    "c5a3".to_string(),
275                    "f7f6".to_string(),
276                    "e1g1".to_string(),
277                    "a8c8".to_string(),
278                    "b2b3".to_string(),
279                    "e8f7".to_string(),
280                    "f1c1".to_string(),
281                    "d7b6".to_string(),
282                    "f3e1".to_string(),
283                    "f5g6".to_string(),
284                    "f2f3".to_string(),
285                    "h8d8".to_string(),
286                    "e3e4".to_string(),
287                    "a5c6".to_string(),
288                    "e1d3".to_string(),
289                    "e6e5".to_string(),
290                    "d3c5".to_string(),
291                    "d5e4".to_string(),
292                    "d2e4".to_string(),
293                    "g6e4".to_string(),
294                    "c5e4".to_string(),
295                ]),
296            }
297        );
298        Ok(())
299    }
300}