chess/
chess_move.rs

1use crate::board::Board;
2use crate::error::Error;
3use crate::file::File;
4use crate::movegen::MoveGen;
5use crate::piece::Piece;
6use crate::rank::Rank;
7use crate::square::Square;
8
9use std::cmp::Ordering;
10use std::fmt;
11use std::str::FromStr;
12
13/// Represent a ChessMove in memory
14#[derive(Clone, Copy, Eq, PartialOrd, PartialEq, Default, Debug, Hash)]
15pub struct ChessMove {
16    source: Square,
17    dest: Square,
18    promotion: Option<Piece>,
19}
20
21impl ChessMove {
22    /// Create a new chess move, given a source `Square`, a destination `Square`, and an optional
23    /// promotion `Piece`
24    #[inline]
25    pub fn new(source: Square, dest: Square, promotion: Option<Piece>) -> ChessMove {
26        ChessMove {
27            source: source,
28            dest: dest,
29            promotion: promotion,
30        }
31    }
32
33    /// Get the source square (square the piece is currently on).
34    #[inline]
35    pub fn get_source(&self) -> Square {
36        self.source
37    }
38
39    /// Get the destination square (square the piece is going to).
40    #[inline]
41    pub fn get_dest(&self) -> Square {
42        self.dest
43    }
44
45    /// Get the promotion piece (maybe).
46    #[inline]
47    pub fn get_promotion(&self) -> Option<Piece> {
48        self.promotion
49    }
50    /// Convert a SAN (Standard Algebraic Notation) move into a `ChessMove`
51    ///
52    /// ```
53    /// use chess::{Board, ChessMove, Square};
54    ///
55    /// let board = Board::default();
56    /// assert_eq!(
57    ///     ChessMove::from_san(&board, "e4").expect("e4 is valid in the initial position"),
58    ///     ChessMove::new(Square::E2, Square::E4, None)
59    /// );
60    /// ```
61    pub fn from_san(board: &Board, move_text: &str) -> Result<ChessMove, Error> {
62        // Castles first...
63        if move_text == "O-O" || move_text == "O-O-O" {
64            let rank = board.side_to_move().to_my_backrank();
65            let source_file = File::E;
66            let dest_file = if move_text == "O-O" { File::G } else { File::C };
67
68            let m = ChessMove::new(
69                Square::make_square(rank, source_file),
70                Square::make_square(rank, dest_file),
71                None,
72            );
73            if MoveGen::new_legal(&board).any(|l| l == m) {
74                return Ok(m);
75            } else {
76                return Err(Error::InvalidSanMove);
77            }
78        }
79
80        // forms of SAN moves
81        // a4 (Pawn moves to a4)
82        // exd4 (Pawn on e file takes on d4)
83        // xd4 (Illegal, source file must be specified)
84        // 1xd4 (Illegal, source file (not rank) must be specified)
85        // Nc3 (Knight (or any piece) on *some square* to c3
86        // Nb1c3 (Knight (or any piece) on b1 to c3
87        // Nbc3 (Knight on b file to c3)
88        // N1c3 (Knight on first rank to c3)
89        // Nb1xc3 (Knight on b1 takes on c3)
90        // Nbxc3 (Knight on b file takes on c3)
91        // N1xc3 (Knight on first rank takes on c3)
92        // Nc3+ (Knight moves to c3 with check)
93        // Nc3# (Knight moves to c3 with checkmate)
94
95        // Because I'm dumb, I'm wondering if a hash table of all possible moves would be stupid.
96        // There are only 186624 possible moves in SAN notation.
97        //
98        // Would this even be faster?  Somehow I doubt it because caching, but maybe, I dunno...
99        // This could take the form of a:
100        // struct CheckOrCheckmate {
101        //      Neither,
102        //      Check,
103        //      CheckMate,
104        // }
105        // struct FromSan {
106        //      piece: Piece,
107        //      source: Vec<Square>, // possible source squares
108        //      // OR
109        //      source_rank: Option<Rank>,
110        //      source_file: Option<File>,
111        //      dest: Square,
112        //      takes: bool,
113        //      check: CheckOrCheckmate
114        // }
115        //
116        // This could be kept internally as well, and never tell the user about such an abomination
117        //
118        // I estimate this table would take around 2 MiB, but I had to approximate some things.  It
119        // may be less
120
121        // This can be described with the following format
122        // [Optional Piece Specifier] ("" | "N" | "B" | "R" | "Q" | "K")
123        // [Optional Source Specifier] ( "" | "a-h" | "1-8" | ("a-h" + "1-8"))
124        // [Optional Takes Specifier] ("" | "x")
125        // [Full Destination Square] ("a-h" + "0-8")
126        // [Optional Promotion Specifier] ("" | "N" | "B" | "R" | "Q")
127        // [Optional Check(mate) Specifier] ("" | "+" | "#")
128        // [Optional En Passant Specifier] ("" | " e.p.")
129
130        let error = Error::InvalidSanMove;
131        let mut cur_index: usize = 0;
132        let moving_piece = match move_text
133            .get(cur_index..(cur_index + 1))
134            .ok_or(error.clone())?
135        {
136            "N" => {
137                cur_index += 1;
138                Piece::Knight
139            }
140            "B" => {
141                cur_index += 1;
142                Piece::Bishop
143            }
144            "Q" => {
145                cur_index += 1;
146                Piece::Queen
147            }
148            "R" => {
149                cur_index += 1;
150                Piece::Rook
151            }
152            "K" => {
153                cur_index += 1;
154                Piece::King
155            }
156            _ => Piece::Pawn,
157        };
158
159        let mut source_file = match move_text
160            .get(cur_index..(cur_index + 1))
161            .ok_or(error.clone())?
162        {
163            "a" => {
164                cur_index += 1;
165                Some(File::A)
166            }
167            "b" => {
168                cur_index += 1;
169                Some(File::B)
170            }
171            "c" => {
172                cur_index += 1;
173                Some(File::C)
174            }
175            "d" => {
176                cur_index += 1;
177                Some(File::D)
178            }
179            "e" => {
180                cur_index += 1;
181                Some(File::E)
182            }
183            "f" => {
184                cur_index += 1;
185                Some(File::F)
186            }
187            "g" => {
188                cur_index += 1;
189                Some(File::G)
190            }
191            "h" => {
192                cur_index += 1;
193                Some(File::H)
194            }
195            _ => None,
196        };
197
198        let mut source_rank = match move_text
199            .get(cur_index..(cur_index + 1))
200            .ok_or(error.clone())?
201        {
202            "1" => {
203                cur_index += 1;
204                Some(Rank::First)
205            }
206            "2" => {
207                cur_index += 1;
208                Some(Rank::Second)
209            }
210            "3" => {
211                cur_index += 1;
212                Some(Rank::Third)
213            }
214            "4" => {
215                cur_index += 1;
216                Some(Rank::Fourth)
217            }
218            "5" => {
219                cur_index += 1;
220                Some(Rank::Fifth)
221            }
222            "6" => {
223                cur_index += 1;
224                Some(Rank::Sixth)
225            }
226            "7" => {
227                cur_index += 1;
228                Some(Rank::Seventh)
229            }
230            "8" => {
231                cur_index += 1;
232                Some(Rank::Eighth)
233            }
234            _ => None,
235        };
236
237        let takes = if let Some(s) = move_text.get(cur_index..(cur_index + 1)) {
238            match s {
239                "x" => {
240                    cur_index += 1;
241                    true
242                }
243                _ => false,
244            }
245        } else {
246            false
247        };
248
249        let dest = if let Some(s) = move_text.get(cur_index..(cur_index + 2)) {
250            if let Ok(q) = Square::from_str(s) {
251                cur_index += 2;
252                q
253            } else {
254                let sq = Square::make_square(
255                    source_rank.ok_or(error.clone())?,
256                    source_file.ok_or(error.clone())?,
257                );
258                source_rank = None;
259                source_file = None;
260                sq
261            }
262        } else {
263            let sq = Square::make_square(
264                source_rank.ok_or(error.clone())?,
265                source_file.ok_or(error.clone())?,
266            );
267            source_rank = None;
268            source_file = None;
269            sq
270        };
271
272        let promotion = if let Some(s) = move_text.get(cur_index..(cur_index + 1)) {
273            match s {
274                "N" => {
275                    cur_index += 1;
276                    Some(Piece::Knight)
277                }
278                "B" => {
279                    cur_index += 1;
280                    Some(Piece::Bishop)
281                }
282                "R" => {
283                    cur_index += 1;
284                    Some(Piece::Rook)
285                }
286                "Q" => {
287                    cur_index += 1;
288                    Some(Piece::Queen)
289                }
290                _ => None,
291            }
292        } else {
293            None
294        };
295
296        if let Some(s) = move_text.get(cur_index..(cur_index + 1)) {
297            let _maybe_check_or_mate = match s {
298                "+" => {
299                    cur_index += 1;
300                    Some(false)
301                }
302                "#" => {
303                    cur_index += 1;
304                    Some(true)
305                }
306                _ => None,
307            };
308        }
309
310        let ep = if let Some(s) = move_text.get(cur_index..) {
311            s == " e.p."
312        } else {
313            false
314        };
315
316        //if ep {
317        //    cur_index += 5;
318        //}
319
320        // Ok, now we have all the data from the SAN move, in the following structures
321        // moveing_piece, source_rank, source_file, taks, dest, promotion, maybe_check_or_mate, and
322        // ep
323
324        let mut found_move: Option<ChessMove> = None;
325        for m in &mut MoveGen::new_legal(board) {
326            // check that the move has the properties specified
327            if board.piece_on(m.get_source()) != Some(moving_piece) {
328                continue;
329            }
330
331            if let Some(rank) = source_rank {
332                if m.get_source().get_rank() != rank {
333                    continue;
334                }
335            }
336
337            if let Some(file) = source_file {
338                if m.get_source().get_file() != file {
339                    continue;
340                }
341            }
342
343            if m.get_dest() != dest {
344                continue;
345            }
346
347            if m.get_promotion() != promotion {
348                continue;
349            }
350
351            if found_move.is_some() {
352                return Err(error);
353            }
354
355            // takes is complicated, because of e.p.
356            if !takes {
357                if board.piece_on(m.get_dest()).is_some() {
358                    continue;
359                }
360            }
361
362            if !ep && takes {
363                if board.piece_on(m.get_dest()).is_none() {
364                    continue;
365                }
366            }
367
368            found_move = Some(m);
369        }
370
371        found_move.ok_or(error.clone())
372    }
373}
374
375impl fmt::Display for ChessMove {
376    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
377        match self.promotion {
378            None => write!(f, "{}{}", self.source, self.dest),
379            Some(x) => write!(f, "{}{}{}", self.source, self.dest, x),
380        }
381    }
382}
383
384impl Ord for ChessMove {
385    fn cmp(&self, other: &ChessMove) -> Ordering {
386        if self.source != other.source {
387            self.source.cmp(&other.source)
388        } else if self.dest != other.dest {
389            self.dest.cmp(&other.dest)
390        } else if self.promotion != other.promotion {
391            match self.promotion {
392                None => Ordering::Less,
393                Some(x) => match other.promotion {
394                    None => Ordering::Greater,
395                    Some(y) => x.cmp(&y),
396                },
397            }
398        } else {
399            Ordering::Equal
400        }
401    }
402}
403
404/// Convert a UCI `String` to a move. If invalid, return `None`
405/// ```
406/// use chess::{ChessMove, Square, Piece};
407/// use std::str::FromStr;
408///
409/// let mv = ChessMove::new(Square::E7, Square::E8, Some(Piece::Queen));
410///
411/// assert_eq!(ChessMove::from_str("e7e8q").expect("Valid Move"), mv);
412/// ```
413impl FromStr for ChessMove {
414    type Err = Error;
415
416    fn from_str(s: &str) -> Result<Self, Self::Err> {
417        let source = Square::from_str(s.get(0..2).ok_or(Error::InvalidUciMove)?)?;
418        let dest = Square::from_str(s.get(2..4).ok_or(Error::InvalidUciMove)?)?;
419
420        let mut promo = None;
421        if s.len() == 5 {
422            promo = Some(match s.chars().last().ok_or(Error::InvalidUciMove)? {
423                'q' => Piece::Queen,
424                'r' => Piece::Rook,
425                'n' => Piece::Knight,
426                'b' => Piece::Bishop,
427                _ => return Err(Error::InvalidUciMove),
428            });
429        }
430
431        Ok(ChessMove::new(source, dest, promo))
432    }
433}
434
435#[test]
436fn test_basic_moves() {
437    let board = Board::default();
438    assert_eq!(
439        ChessMove::from_san(&board, "e4").expect("e4 is valid in the initial position"),
440        ChessMove::new(Square::E2, Square::E4, None)
441    );
442}