1use core::{fmt, str::FromStr};
4
5use crate::{
6 Game, Role, game,
7 game::Mode,
8 square::{File, Rank, Square},
9};
10
11use super::{StrInput as Input, prelude::*};
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
14#[cfg_attr(feature = "serde", derive(SerializeDisplay, DeserializeFromStr))]
15pub struct Move {
16 pub from: Square,
17 pub to: Square,
18 pub promotion: Option<Role>,
19}
20
21impl crate::Move {
22 pub fn uci(self, mode: Mode) -> Move {
23 match mode {
24 Mode::Chess => self.uci_chess(),
25 Mode::Freestyle => self.uci_960(),
26 }
27 }
28
29 pub fn uci_chess(self) -> Move {
30 Move { from: self.from, to: self.to, promotion: self.promotes() }
31 }
32
33 pub fn uci_960(self) -> Move {
34 let to = match self.castle_rook_file() {
35 Some(file) => Square::new(file, self.from.rank()),
36 None => self.to,
37 };
38 Move { from: self.from, to, promotion: self.promotes() }
39 }
40}
41
42impl Move {
43 pub fn resolve(self, mode: Mode, legal: &[crate::Move]) -> Option<crate::Move> {
44 match mode {
45 Mode::Chess => self.resolve_chess(legal),
46 Mode::Freestyle => self.resolve_960(legal),
47 }
48 }
49
50 pub fn resolve_chess(self, legal: &[crate::Move]) -> Option<crate::Move> {
51 legal.iter().copied().find(|play| play.uci_chess() == self)
52 }
53
54 pub fn resolve_960(self, legal: &[crate::Move]) -> Option<crate::Move> {
55 legal.iter().copied().find(|play| play.uci_960() == self)
56 }
57}
58
59impl<'g> game::OptionsMut<'g> {
60 pub fn push_uci(&mut self, uci: Move) -> Result<game::PlayMut<'_>, game::Error> {
61 let options = self.as_ref();
62 let legal = options.legal();
63 let Some(play) = uci.resolve(options.mode(), legal) else {
64 return Err(game::Error::Illegal);
65 };
66 self.push(play)
67 }
68 pub fn into_push_uci(self, uci: Move) -> Result<game::PlayMut<'g>, game::Error> {
69 let options = self.as_ref();
70 let legal = options.legal();
71 let Some(play) = uci.resolve(options.mode(), legal) else {
72 return Err(game::Error::Illegal);
73 };
74 self.into_push(play)
75 }
76}
77
78impl crate::Position {
79 pub fn resolve_uci(self, mode: Mode, moves: &[Move]) -> Vec<crate::Move> {
80 let mut game = Game::new(self, mode);
81 let mut options = game.start_options_mut();
82 let mut resolved = Vec::with_capacity(moves.len());
83
84 for uci in moves {
85 let Ok(play) = options.into_push_uci(*uci) else {
86 break;
87 };
88 resolved.push(play.play());
89 options = play.into_options_mut();
90 }
91
92 resolved
93 }
94
95 pub fn resolve_uci_san(self, mode: Mode, moves: &[Move]) -> Vec<String> {
96 let mut game = Game::new(self, mode);
97 let mut options = game.start_options_mut();
98 let mut resolved = Vec::with_capacity(moves.len());
99
100 for uci in moves {
101 let Ok(play) = options.into_push_uci(*uci) else {
102 break;
103 };
104 resolved.push(play.san().to_string());
105 options = play.into_options_mut();
106 }
107
108 resolved
109 }
110}
111
112impl fmt::Display for Move {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 self.from.fmt(f)?;
115 self.to.fmt(f)?;
116 if let Some(promotion) = self.promotion {
117 promotion.lower().fmt(f)?;
118 }
119 Ok(())
120 }
121}
122
123impl FromStr for Move {
124 type Err = Error;
125
126 fn from_str(s: &str) -> Result<Self, Self::Err> {
127 let mut input = s.trim();
128 let play = uci_move(&mut input).map_err(|_| Error::InvalidMove)?;
129 input.is_empty().then_some(play).ok_or(Error::InvalidMove)
130 }
131}
132
133#[derive(Debug, thiserror::Error)]
134pub enum Error {
135 #[error("invalid UCI move")]
136 InvalidMove,
137 #[error("invalid UCI info")]
138 InvalidInfo,
139}
140
141#[derive(Clone, Debug, Eq, PartialEq)]
142pub struct Pv {
143 pub moves: Vec<Move>,
144}
145
146#[derive(Clone, Debug, Eq, PartialEq)]
147pub struct Info {
148 pub depth: Option<u32>,
149 pub seldepth: Option<u32>,
150 pub multipv: Option<usize>,
151 pub nodes: Option<u32>,
152 pub score: Option<Score>,
153 pub bound: Option<Bound>,
154 pub pv: Option<Pv>,
155}
156
157impl FromStr for Info {
158 type Err = Error;
159
160 fn from_str(s: &str) -> Result<Self, Self::Err> {
161 let mut input = s.trim();
162 let info = info(&mut input).map_err(|_| Error::InvalidInfo)?;
163 input.is_empty().then_some(info).ok_or(Error::InvalidInfo)
164 }
165}
166
167#[derive(Clone, Copy, Debug, Eq, PartialEq)]
168pub enum Score {
169 Centipawns(i32),
170 Mate(i32),
171}
172
173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
174pub enum Bound {
175 Lower,
176 Upper,
177}
178
179pub fn parse_move(text: &str, legal: &[crate::Move]) -> Option<crate::Move> {
181 parse_move_as(Mode::Chess, text, legal)
182}
183
184pub fn parse_move_as(mode: Mode, text: &str, legal: &[crate::Move]) -> Option<crate::Move> {
185 let mut input = text.trim();
186 let play = uci_move(&mut input).ok()?;
187 input.is_empty().then_some(())?;
188
189 play.resolve(mode, legal)
190}
191
192pub fn uci_move(input: &mut Input<'_>) -> ModalResult<Move> {
193 (square, square, opt(promotion))
194 .map(|(from, to, promotion)| Move { from, to, promotion })
195 .parse_next(input)
196}
197
198pub fn info(input: &mut Input<'_>) -> ModalResult<Info> {
199 "info".parse_next(input)?;
200
201 let mut info = Info {
202 depth: None,
203 seldepth: None,
204 multipv: None,
205 nodes: None,
206 score: None,
207 bound: None,
208 pv: None,
209 };
210
211 while preceded(space1, |input: &mut Input<'_>| info_field(input, &mut info))
212 .parse_next(input)
213 .is_ok()
214 {}
215
216 Ok(info)
217}
218
219fn info_field(input: &mut Input<'_>, info: &mut Info) -> ModalResult<()> {
220 alt((
221 "depth".value(Field::Depth),
222 "seldepth".value(Field::Seldepth),
223 "multipv".value(Field::Multipv),
224 "nodes".value(Field::Nodes),
225 "score".value(Field::Score),
226 "lowerbound".value(Field::Lowerbound),
227 "upperbound".value(Field::Upperbound),
228 "pv".value(Field::Pv),
229 token.value(Field::Skip),
230 ))
231 .parse_next(input)?
232 .parse(info, input)
233}
234
235#[derive(Clone, Copy)]
236enum Field {
237 Depth,
238 Seldepth,
239 Multipv,
240 Nodes,
241 Score,
242 Lowerbound,
243 Upperbound,
244 Pv,
245 Skip,
246}
247
248impl Field {
249 fn parse(self, info: &mut Info, input: &mut Input<'_>) -> ModalResult<()> {
250 match self {
251 Field::Depth => info.depth = Some(preceded(space1, dec_uint).parse_next(input)?),
252 Field::Seldepth => info.seldepth = Some(preceded(space1, dec_uint).parse_next(input)?),
253 Field::Multipv => info.multipv = Some(preceded(space1, dec_uint).parse_next(input)?),
254 Field::Nodes => info.nodes = Some(preceded(space1, dec_uint).parse_next(input)?),
255 Field::Score => info.score = Some(preceded(space1, score).parse_next(input)?),
256 Field::Lowerbound => info.bound = Some(Bound::Lower),
257 Field::Upperbound => info.bound = Some(Bound::Upper),
258 Field::Pv => info.pv = Some(preceded(space1, pv).parse_next(input)?),
259 Field::Skip => {}
260 }
261
262 Ok(())
263 }
264}
265
266fn token(input: &mut Input<'_>) -> ModalResult<()> {
267 take_till(1.., char::is_whitespace).void().parse_next(input)
268}
269
270fn score(input: &mut Input<'_>) -> ModalResult<Score> {
271 alt((
272 preceded(("cp", space1), signed).map(Score::Centipawns),
273 preceded(("mate", space1), signed).map(Score::Mate),
274 ))
275 .parse_next(input)
276}
277
278fn signed(input: &mut Input<'_>) -> ModalResult<i32> {
279 (opt(alt(('+'.value(1), '-'.value(-1)))), dec_uint)
280 .verify_map(|(sign, value): (Option<i32>, u32)| {
281 i32::try_from(value).ok().map(|value| sign.unwrap_or(1) * value)
282 })
283 .parse_next(input)
284}
285
286fn pv(input: &mut Input<'_>) -> ModalResult<Pv> {
287 separated(1.., uci_move, space1).map(|moves| Pv { moves }).parse_next(input)
288}
289
290fn square(input: &mut Input<'_>) -> ModalResult<Square> {
291 (file, rank).map(|(file, rank)| Square::new(file, rank)).parse_next(input)
292}
293
294fn file(input: &mut Input<'_>) -> ModalResult<File> {
295 one_of(|c| "abcdefgh".contains(c)).map(File::panicky_from_char).parse_next(input)
296}
297
298fn rank(input: &mut Input<'_>) -> ModalResult<Rank> {
299 one_of(|c| "12345678".contains(c)).map(Rank::panicky_from_char).parse_next(input)
300}
301
302fn promotion(input: &mut Input<'_>) -> ModalResult<Role> {
303 one_of(|c| "nbrqNBRQ".contains(c)).map(Role::panicky_from_char).parse_next(input)
304}
305
306#[cfg(test)]
307mod tests {
308 use crate::{Position, board::Role::*, square::Square::*};
309
310 use super::*;
311
312 #[test]
313 fn parses_normal_move() {
314 let legal = Position::start().legal_moves();
315
316 assert_eq!(parse_move("e2e4", &legal).unwrap().to, E4);
317 assert!(parse_move("e2e5", &legal).is_none());
318 }
319
320 #[test]
321 fn parses_promotion_move() {
322 let position = Position::from_fen("8/P7/8/8/8/8/8/k6K w - - 0 1").unwrap();
323 let legal = position.legal_moves();
324
325 assert_eq!(parse_move("a7a8q", &legal).unwrap().promotes(), Some(Queen));
326 assert_eq!(parse_move("a7a8Q", &legal).unwrap().promotes(), Some(Queen));
327 assert!(parse_move("a7a8", &legal).is_none());
328 }
329
330 #[test]
331 fn resolves_special_moves_from_legal_moves() {
332 let castle = Position::from_fen("4k3/8/8/8/8/8/8/R3K2R w KQ - 0 1").unwrap();
333 assert!(parse_move("e1g1", &castle.legal_moves()).is_some_and(crate::Move::is_castle));
334
335 let en_passant = Position::from_fen("4k3/8/8/3pP3/8/8/8/4K3 w - d6 0 1").unwrap();
336 assert!(
337 parse_move("e5d6", &en_passant.legal_moves()).is_some_and(crate::Move::is_en_passant)
338 );
339 }
340
341 #[test]
342 fn parses_info() {
343 let parsed = info
344 .parse(
345 "info depth 24 seldepth 47 multipv 1 score cp 30 nodes 46777504 nps 15592501 hashfull 1000 tbhits 0 time 3000 pv d2d4 g8f6",
346 )
347 .unwrap();
348
349 assert_eq!(parsed.depth, Some(24));
350 assert_eq!(parsed.seldepth, Some(47));
351 assert_eq!(parsed.multipv, Some(1));
352 assert_eq!(parsed.nodes, Some(46777504));
353 assert_eq!(parsed.score, Some(Score::Centipawns(30)));
354 assert_eq!(parsed.bound, None);
355 assert_eq!(
356 parsed.pv,
357 Some(Pv {
358 moves: vec![
359 Move { from: D2, to: D4, promotion: None },
360 Move { from: G8, to: F6, promotion: None },
361 ],
362 })
363 );
364 }
365
366 #[test]
367 fn parses_info_mate_and_bound() {
368 let parsed = info
369 .parse("info depth 6 score mate -3 upperbound nodes 974 pv c8c1 d2f1 c1f1")
370 .unwrap();
371
372 assert_eq!(parsed.depth, Some(6));
373 assert_eq!(parsed.score, Some(Score::Mate(-3)));
374 assert_eq!(parsed.bound, Some(Bound::Upper));
375 assert_eq!(parsed.nodes, Some(974));
376 assert_eq!(parsed.pv.unwrap().moves.len(), 3);
377 }
378}