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}