w_chess/
chess_move.rs

1use crate::{
2    Piece, Square, FILE_A, FILE_B, FILE_C, FILE_D, FILE_E, FILE_F, FILE_G, FILE_H, RANK_1, RANK_2,
3    RANK_3, RANK_4, RANK_5, RANK_6, RANK_7, RANK_8,
4};
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ChessMove {
8    color: bool,
9    before: String,
10    after: String,
11    from: Square,
12    to: Square,
13    piece: Piece,
14    captured: Option<Piece>,
15    promotion: Option<Piece>,
16    san: String,
17    castling: Option<CastlingType>,
18}
19
20impl ChessMove {
21    pub fn new(
22        san: &mut SanMove,
23        color: bool,
24        before: String,
25        after: String,
26        from: u64,
27        captured: Option<Piece>,
28    ) -> Self {
29        Self {
30            color,
31            before,
32            after,
33            from: Square::from(from),
34            to: Square::from(san.to),
35            piece: san.piece,
36            captured,
37            promotion: san.promotion,
38            san: san.san.to_string(),
39            castling: san.castling,
40        }
41    }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SanMove<'a> {
46    pub san: &'a str,
47    pub piece: Piece,
48    pub to: u64,
49    pub from: u64,
50    pub promotion: Option<Piece>,
51    pub castling: Option<CastlingType>,
52}
53
54impl<'a> SanMove<'a> {
55    pub fn parse(san: &'a str) -> Result<Self, &'a str> {
56        let mut chars = san.chars().peekable();
57        let mut to = 0;
58        let mut from = 0;
59        let mut piece = Piece::UNKNOWN;
60        let mut promotion = None;
61        let mut castling = None;
62
63        loop {
64            if let Some(c) = chars.next() {
65                match c {
66                    'x' | '+' | '#' | '!' | '?' | ' ' => {}
67
68                    'O' | '0' => {
69                        if piece == Piece::UNKNOWN {
70                            piece = Piece::KING;
71                        }
72
73                        let target = c;
74
75                        loop {
76                            if let Some(c) = chars.peek() {
77                                match c {
78                                    '-' => {
79                                        chars.next();
80                                        if let Some(c) = chars.next() {
81                                            if c == target {
82                                                if castling.is_none() {
83                                                    castling = Some(CastlingType::KingSide);
84                                                } else {
85                                                    if castling == Some(CastlingType::KingSide) {
86                                                        castling = Some(CastlingType::QueenSide);
87                                                    } else {
88                                                        return Err("Invalid castling move");
89                                                    }
90                                                }
91                                            } else {
92                                                return Err("Invalid castling move");
93                                            }
94                                        }
95                                    }
96                                    _ => {
97                                        if castling.is_none() {
98                                            return Err("Invalid castling move");
99                                        } else {
100                                            break;
101                                        }
102                                    }
103                                }
104                            } else {
105                                if castling.is_none() {
106                                    return Err("Invalid castling move");
107                                } else {
108                                    break;
109                                }
110                            }
111                        }
112                    }
113                    '=' => {
114                        if piece != Piece::PAWN || to == 0 {
115                            return Err("Invalid promotion piece");
116                        }
117
118                        if let Some(potential_rank) = chars.peek() {
119                            promotion = Some(match potential_rank {
120                                'N' => Piece::KNIGHT,
121                                'B' => Piece::BISHOP,
122                                'R' => Piece::ROOK,
123                                'Q' => Piece::QUEEN,
124                                _ => return Err("Invalid promotion piece"),
125                            });
126
127                            chars.next();
128                        }
129                    }
130                    'a'..='h' => {
131                        if c == 'e' && chars.peek() == Some(&'.') {
132                            chars.next();
133                            if chars.peek() == Some(&'p') {
134                                chars.next();
135                                if chars.peek() == Some(&'.') {
136                                    chars.next();
137                                }
138                            }
139                        }
140
141                        if piece == Piece::UNKNOWN {
142                            piece = Piece::PAWN;
143                        }
144
145                        if let Some(potential_rank) = chars.peek() {
146                            if potential_rank.is_digit(10) {
147                                let file: usize = match c {
148                                    'a' => 0,
149                                    'b' => 1,
150                                    'c' => 2,
151                                    'd' => 3,
152                                    'e' => 4,
153                                    'f' => 5,
154                                    'g' => 6,
155                                    'h' => 7,
156                                    _ => unreachable!(),
157                                };
158
159                                let rank: usize = potential_rank.to_digit(10).unwrap() as usize - 1;
160                                if rank > 7 {
161                                    return Err("Invalid rank");
162                                }
163
164                                to = 1 << (file + rank * 8);
165
166                                chars.next();
167                            } else {
168                                from = match c {
169                                    'a' => FILE_A,
170                                    'b' => FILE_B,
171                                    'c' => FILE_C,
172                                    'd' => FILE_D,
173                                    'e' => FILE_E,
174                                    'f' => FILE_F,
175                                    'g' => FILE_G,
176                                    'h' => FILE_H,
177                                    _ => unreachable!(),
178                                };
179                            }
180                        }
181                    }
182                    '1'..='8' => {
183                        from = match c {
184                            '1' => RANK_1,
185                            '2' => RANK_2,
186                            '3' => RANK_3,
187                            '4' => RANK_4,
188                            '5' => RANK_5,
189                            '6' => RANK_6,
190                            '7' => RANK_7,
191                            '8' => RANK_8,
192                            _ => unreachable!(),
193                        }
194                    }
195                    'N' | 'B' | 'R' | 'Q' | 'K' => {
196                        piece = match c {
197                            'N' => Piece::KNIGHT,
198                            'B' => Piece::BISHOP,
199                            'R' => Piece::ROOK,
200                            'Q' => Piece::QUEEN,
201                            'K' => Piece::KING,
202                            _ => unreachable!(),
203                        };
204                    }
205                    _ => return Err("Invalid character"),
206                }
207            } else {
208                break;
209            }
210        }
211
212        if to == 0 {
213            to = std::mem::replace(&mut from, 0);
214        }
215
216        Ok(Self {
217            san,
218            piece,
219            to,
220            from,
221            promotion,
222            castling,
223        })
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use crate::Square;
230
231    use super::*;
232
233    #[test]
234    fn test_san_move_parse() {
235        let san = "Rae1";
236        let san_move = SanMove::parse(san).unwrap();
237        assert_eq!(san_move.san, "Rae1");
238        assert_eq!(san_move.piece, Piece::ROOK);
239        assert_eq!(san_move.to, 1 << Square::E1 as u64);
240        assert_eq!(san_move.from, FILE_A);
241        assert_eq!(san_move.promotion, None);
242
243        let san = "e4";
244        let san_move = SanMove::parse(san).unwrap();
245        assert_eq!(san_move.san, "e4");
246        assert_eq!(san_move.piece, Piece::PAWN);
247        assert_eq!(san_move.to, 1 << Square::E4 as u64);
248        assert_eq!(san_move.from, 0);
249        assert_eq!(san_move.promotion, None);
250
251        let san = "Nf3";
252        let san_move = SanMove::parse(san).unwrap();
253        assert_eq!(san_move.san, "Nf3");
254        assert_eq!(san_move.piece, Piece::KNIGHT);
255        assert_eq!(san_move.to, 1 << Square::F3 as u64);
256        assert_eq!(san_move.from, 0);
257        assert_eq!(san_move.promotion, None);
258
259        let san = "Nf3+";
260        let san_move = SanMove::parse(san).unwrap();
261        assert_eq!(san_move.san, "Nf3+");
262        assert_eq!(san_move.piece, Piece::KNIGHT);
263        assert_eq!(san_move.to, 1 << Square::F3 as u64);
264        assert_eq!(san_move.from, 0);
265        assert_eq!(san_move.promotion, None);
266
267        let san = "e8=Q";
268        let san_move = SanMove::parse(san).unwrap();
269        assert_eq!(san_move.san, "e8=Q");
270        assert_eq!(san_move.piece, Piece::PAWN);
271        assert_eq!(san_move.to, 1 << Square::E8 as u64);
272        assert_eq!(san_move.from, 0);
273        assert_eq!(san_move.promotion, Some(Piece::QUEEN));
274
275        let san = "O-O";
276        let san_move = SanMove::parse(san).unwrap();
277        assert_eq!(san_move.san, "O-O");
278        assert_eq!(san_move.piece, Piece::KING);
279        assert_eq!(san_move.to, 0);
280        assert_eq!(san_move.from, 0);
281        assert_eq!(san_move.promotion, None);
282        assert_eq!(san_move.castling, Some(CastlingType::KingSide));
283
284        let san = "O-O-O";
285        let san_move = SanMove::parse(san).unwrap();
286        assert_eq!(san_move.san, "O-O-O");
287        assert_eq!(san_move.piece, Piece::KING);
288        assert_eq!(san_move.to, 0);
289        assert_eq!(san_move.from, 0);
290        assert_eq!(san_move.promotion, None);
291        assert_eq!(san_move.castling, Some(CastlingType::QueenSide));
292
293        let invalid_castle = "O-O-O-O";
294        assert!(SanMove::parse(invalid_castle).is_err());
295
296        let san = "N3d2";
297        let san_move = SanMove::parse(san).unwrap();
298        assert_eq!(san_move.san, "N3d2");
299        assert_eq!(san_move.piece, Piece::KNIGHT);
300        assert_eq!(san_move.to, 1 << Square::D2 as u64);
301        assert_eq!(san_move.from, RANK_3);
302        assert_eq!(san_move.promotion, None);
303
304        let san = "exd6 e.p.";
305        let san_move = SanMove::parse(san).unwrap();
306        assert_eq!(san_move.san, "exd6 e.p.");
307        assert_eq!(san_move.piece, Piece::PAWN);
308        assert_eq!(san_move.to, 1 << Square::D6 as u64);
309        assert_eq!(san_move.from, FILE_E);
310        assert_eq!(san_move.promotion, None);
311    }
312}
313
314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
315pub enum CastlingType {
316    KingSide,
317    QueenSide,
318}