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