1use anyhow::Result;
2use std::{collections::HashMap, fmt::Display, str::FromStr};
3use thiserror::Error;
4
5#[derive(PartialEq, Debug)]
7pub enum UCI {
8 UciOk,
10
11 ReadyOk,
13
14 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 Option { name: String, opt_type: OptionType },
28}
29
30#[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 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#[derive(Error, Debug)]
113pub enum UCIError {
114 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
127pub 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
140fn 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
161fn 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
179fn 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, };
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 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}