myopic_board/parse/
pgn.rs

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
9/// Extracts the moves encoded in standard pgn format starting at
10/// a custom board position.
11pub 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 a castle move we can retrieve straight away
29    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    // Otherwise we need to get more involved.
49    // The target square of the move.
50    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    // Functionality for checking if a piece type matches the pgn move.
57    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    // Functionality for differentiating ambiguous moves.
63    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    // Retrieve the unique move which matches target square, piece type and
68    // any differentiating information.
69    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}