1use anyhow::{anyhow, Result};
2use regex::Regex;
3
4use myopic_core::{CastleZone, Piece, Square};
5
6use crate::{ChessBoard, Move, MoveComputeType};
7use crate::parse::patterns::*;
8
9pub fn moves<B: ChessBoard>(start: &B, encoded: &str) -> Result<Vec<Move>> {
12 let mut mutator_board = start.clone();
13 let mut dest: Vec<Move> = Vec::new();
14 for evolve in pgn_move().find_iter(encoded) {
15 match parse_single_move(&mut mutator_board, evolve.as_str()) {
16 Ok(result) => {
17 dest.push(result.clone());
18 mutator_board.make(result)?;
19 }
20 Err(_) => return Err(anyhow!("Failed at {} in: {}", evolve.as_str(), encoded)),
21 };
22 }
23 Ok(dest)
24}
25
26fn parse_single_move<B: ChessBoard>(start: &mut B, pgn_move: &str) -> Result<Move> {
27 let legal = start.compute_moves(MoveComputeType::All);
28 if pgn_move == "O-O" {
30 return legal
31 .iter()
32 .find(|&m| match m {
33 Move::Castle { zone, .. } => *zone == CastleZone::kingside(start.active()),
34 _ => false,
35 })
36 .cloned()
37 .ok_or(anyhow!("Kingside castling not available!"));
38 } else if pgn_move == "O-O-O" {
39 return legal
40 .iter()
41 .find(|&m| match m {
42 Move::Castle { zone, .. } => *zone == CastleZone::queenside(start.active()),
43 _ => false,
44 })
45 .cloned()
46 .ok_or(anyhow!("Queenside castling not available!"));
47 }
48 let target = square()
51 .find_iter(pgn_move)
52 .map(|m| m.as_str().parse::<Square>().unwrap())
53 .last()
54 .map(|mv| mv.clone());
55
56 let (move_piece_ordinal, promote_piece_ordinal) = piece_ordinals(pgn_move);
58 let move_piece_matches = |p: Piece| move_piece_ordinal == (p as usize % 6);
59 let promote_piece_matches = |p: Piece| promote_piece_ordinal == (p as usize % 6);
60 let move_matches_pawn = move_piece_matches(Piece::WP);
61
62 let file = find_differentiating_rank_or_file(pgn_move, file());
64 let rank = find_differentiating_rank_or_file(pgn_move, rank());
65 let matches_start = |sq: Square| matches_square(file, rank, sq);
66
67 let matching = legal
70 .into_iter()
71 .filter(|mv| match mv {
72 &Move::Standard {
73 moving, from, dest, ..
74 } => move_piece_matches(moving) && target == Some(dest) && matches_start(from),
75 &Move::Enpassant { from, .. } => {
76 move_matches_pawn && target == start.enpassant() && matches_start(from)
77 }
78 &Move::Promotion {
79 from,
80 dest,
81 promoted,
82 ..
83 } => {
84 move_matches_pawn
85 && target == Some(dest)
86 && matches_start(from)
87 && promote_piece_matches(promoted)
88 }
89 &Move::Castle { .. } => false,
90 })
91 .map(|mv| mv.clone())
92 .collect::<Vec<_>>();
93
94 if matching.len() == 1 {
95 Ok((&matching[0]).clone())
96 } else {
97 Err(anyhow!("Found no move matching {}", pgn_move))
98 }
99}
100
101fn matches_square(file: Option<char>, rank: Option<char>, sq: Square) -> bool {
102 let sq_str = sq.to_string();
103 let matches_file = |f: char| char_at(&sq_str, 0) == f;
104 let matches_rank = |r: char| char_at(&sq_str, 1) == r;
105 match (file, rank) {
106 (Some(f), Some(r)) => matches_file(f) && matches_rank(r),
107 (None, Some(r)) => matches_rank(r),
108 (Some(f), None) => matches_file(f),
109 _ => true,
110 }
111}
112
113fn char_at(string: &String, index: usize) -> char {
114 string.chars().nth(index).unwrap()
115}
116
117fn find_differentiating_rank_or_file(pgn_move: &str, re: &Regex) -> Option<char> {
118 let all_matches: Vec<_> = re
119 .find_iter(pgn_move)
120 .map(|m| m.as_str().to_owned())
121 .collect();
122 if all_matches.len() == 1 {
123 None
124 } else {
125 Some(char_at(&all_matches[0], 0))
126 }
127}
128
129fn piece_ordinals(pgn_move: &str) -> (usize, usize) {
130 let matches: Vec<_> = pgn_piece()
131 .find_iter(pgn_move)
132 .map(|m| m.as_str().to_owned())
133 .collect();
134 let is_promotion = pgn_move.contains("=");
135 let (move_piece, promote_piece) = if matches.is_empty() {
136 (None, None)
137 } else if matches.len() == 1 && is_promotion {
138 (None, Some(char_at(&matches[0], 0)))
139 } else {
140 (Some(char_at(&matches[0], 0)), None)
141 };
142 let ord = |piece: Option<char>| match piece {
143 None => 0,
144 Some('N') => 1,
145 Some('B') => 2,
146 Some('R') => 3,
147 Some('Q') => 4,
148 Some('K') => 5,
149 _ => panic!(),
150 };
151 (ord(move_piece), ord(promote_piece))
152}
153
154#[cfg(test)]
155mod test {
156 use anyhow::Result;
157
158 use crate::{Board, ChessBoard};
159
160 fn execute_success_test(expected_finish: &'static str, pgn: &'static str) -> Result<()> {
161 let finish = expected_finish.parse::<Board>()?;
162 let mut board = crate::STARTPOS_FEN.parse::<Board>()?;
163 for evolve in super::moves(&board, &String::from(pgn))? {
164 board.make(evolve)?;
165 }
166 assert_eq!(finish, board);
167 Ok(())
168 }
169
170 #[test]
171 fn case_zero() -> Result<()> {
172 execute_success_test(
173 "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",
174 "",
175 )
176 }
177
178 #[test]
179 fn case_one() -> Result<()> {
180 execute_success_test(
181 "8/1P4pk/P1N2pp1/8/P3q2P/6P1/5PK1/8 w - - 6 56",
182 "1. d4 d5 2. c4 c6 3. Nf3 Nf6 4. e3 Bf5 5. Nc3 e6 6. Nh4 Bg6
183 7. Nxg6 hxg6 8. g3 Nbd7 9. Bg2 dxc4 10. Qe2 Nb6 11. O-O Bb4
184 12. Bd2 O-O 13. Ne4 Qe7 14. Bxb4 Qxb4 15. Nc5 Rab8 16. Rfc1
185 Rfd8 17. Qc2 Nfd7 18. Ne4 e5 19. a3 Qe7 20. Re1 Nf6 21. Ng5
186 exd4 22. exd4 Qd6 23. Nf3 Re8 24. Re5 Nfd7 25. Ra5 a6 26. Rd1
187 Rbd8 27. Bf1 Re7 28. Rg5 Qf6 29. Kg2 Rde8 30. h4 Qe6 31. a4
188 Qe4 32. Qc1 f6 33. Ra5 Qe6 34. Qc2 Qe4 35. Qc1 Kh8 36. Re1 Qg4
189 37. Rxe7 Rxe7 38. Bxc4 Nxc4 39. Qxc4 Qe4 40. Qb3 c5 41. dxc5
190 Qc6 42. Qc3 Re2 43. b4 Ne5 44. b5 Qe4 45. c6 Nd3 46. Qxd3 Qxd3
191 47. cxb7 Re8 48. bxa6 Qb3 49. Rc5 Kh7 50. Rc8 Rg8 51. Nd4 Qb6
192 52. Rxg8 Kxg8 53. Kg1 Kh7 54. Nc6 Qb1+ 55. Kg2 Qe4+ 1/2-1/2",
193 )
194 }
195 #[test]
196 fn case_two() -> Result<()> {
197 execute_success_test(
198 "5rk1/pp2p3/3p2pb/2pP4/2q5/3b1B1P/PPn2Q2/R1NK2R1 w - - 0 28",
199 "
200 [Event \"F/S Return Match\"]
201 [Site \"Belgrade, Serbia JUG\"]
202 [Date \"1992.11.04\"]
203 [Round \"29\"]
204 [White \"Fischer, Robert J.\"]
205 [Black \"Spassky, Boris V.\"]
206 [Result \"1/2-1/2\"]
207
208 1.d4 Nf6 2.c4 g6 3.Nc3 Bg7 4.e4 d6 5.f3 O-O 6.Be3 Nbd7 7.Qd2
209 c5 8.d5 Ne5 9.h3 Nh5 10.Bf2 f5 11.exf5 Rxf5 12.g4 Rxf3 13.gxh5
210 Qf8 14.Ne4 Bh6 15.Qc2 Qf4 16.Ne2 Rxf2 17.Nxf2 Nf3+ 18.Kd1 Qh4
211 19.Nd3 Bf5 20.Nec1 Nd2 21.hxg6 hxg6 22.Bg2 Nxc4 23.Qf2 Ne3+
212 24.Ke2 Qc4 25.Bf3 Rf8 26.Rg1 Nc2 27.Kd1 Bxd3 0-1
213 ",
214 )
215 }
216}
217
218#[cfg(test)]
219mod test_single_move {
220 use crate::Board;
221
222 use super::*;
223
224 fn execute_success_test(
225 expected: &'static str,
226 start_fen: &'static str,
227 pgn: &'static str,
228 ) -> Result<()> {
229 let mut board = start_fen.parse::<Board>()?;
230 let parsed_expected = Move::from(expected, board.hash())?;
231 let pgn_parse = parse_single_move(&mut board, pgn)?;
232 assert_eq!(parsed_expected, pgn_parse);
233 Ok(())
234 }
235
236 #[test]
237 fn case_one() -> Result<()> {
238 execute_success_test(
239 "sbbg4f3wn",
240 "rn1qkbnr/pp2pppp/2p5/3p4/4P1b1/2N2N1P/PPPP1PP1/R1BQKB1R b KQkq - 0 4",
241 "Bxf3",
242 )
243 }
244
245 #[test]
246 fn case_two() -> Result<()> {
247 execute_success_test(
248 "ewe5f6f5",
249 "r2qkbnr/pp1np1pp/2p5/3pPp2/8/2N2Q1P/PPPP1PP1/R1B1KB1R w KQkq f6 0 7",
250 "exf6",
251 )
252 }
253
254 #[test]
255 fn case_three() -> Result<()> {
256 execute_success_test(
257 "pf7g8wnbn",
258 "r2q1bnr/pp1nkPpp/2p1p3/3p4/8/2N2Q1P/PPPP1PP1/R1B1KB1R w KQ - 1 9",
259 "fxg8=N",
260 )
261 }
262
263 #[test]
264 fn case_four() -> Result<()> {
265 execute_success_test(
266 "pf7g8wqbn",
267 "r2q1bnr/pp1nkPpp/2p1p3/3p4/8/2N2Q1P/PPPP1PP1/R1B1KB1R w KQ - 1 9",
268 "fxg8=Q",
269 )
270 }
271
272 #[test]
273 fn case_five() -> Result<()> {
274 execute_success_test(
275 "sbra8e8-",
276 "r5r1/ppqkb1pp/2p1pn2/3p2B1/3P4/2NB1Q1P/PPP2PP1/4RRK1 b - - 8 14",
277 "Rae8",
278 )
279 }
280
281 #[test]
282 fn case_six() -> Result<()> {
283 execute_success_test(
284 "swre1e2-",
285 "4rr2/ppqkb1p1/2p1p2p/3p4/3Pn2B/2NBRQ1P/PPP2PP1/4R1K1 w - - 2 18",
286 "R1e2",
287 )
288 }
289
290 #[test]
291 fn case_seven() -> Result<()> {
292 execute_success_test(
293 "sbrf3f6wb",
294 "5r2/ppqkb1p1/2p1pB1p/3p4/3Pn2P/2NBRr2/PPP1RPP1/6K1 b - - 0 20",
295 "R3xf6",
296 )
297 }
298
299 #[test]
300 fn case_eight() -> Result<()> {
301 execute_success_test(
302 "sbne4f2wp",
303 "5r2/ppqkb1p1/2p1pr1p/3p4/3Pn2P/2NBR3/PPP1RPP1/7K b - - 1 21",
304 "Nxf2+",
305 )
306 }
307
308 #[test]
309 fn case_nine() -> Result<()> {
310 execute_success_test(
311 "sbrf8f1wb",
312 "5r2/ppqkb1p1/2p1p2p/3p4/P2P3P/2N1R3/1PP3P1/5B1K b - - 0 24",
313 "Rf8xf1#",
314 )
315 }
316
317 #[test]
318 fn case_ten() -> Result<()> {
319 execute_success_test(
320 "cwk",
321 "r3k2r/pp1q1ppp/n1p2n2/4p3/3pP2P/3P1QP1/PPPN1PB1/R3K2R w KQkq - 1 13",
322 "O-O",
323 )
324 }
325 #[test]
326 fn case_eleven() -> Result<()> {
327 execute_success_test(
328 "cbq",
329 "r3k2r/pp1q1ppp/n1p2n2/4p3/3pP2P/3P1QP1/PPPN1PB1/R4RK1 b kq - 2 13",
330 "O-O-O",
331 )
332 }
333}