1use std::collections::HashMap;
2
3use pest::{iterators::Pair, Parser};
4use pest_derive::Parser;
5
6use crate::{
7 core::{Variant, VariantBuilder},
8 errors::PGNError,
9 logic::Game,
10 variants::StandardChess,
11};
12
13#[derive(Parser)]
15#[grammar = "./src/parsing/pgn/pgn.pest"]
16struct PGNParser;
17
18pub fn parse_multiple_pgn<T: Variant + VariantBuilder>(input: &str) -> Result<Vec<T>, PGNError> {
29 let pair = PGNParser::parse(Rule::pgn_file, input)
30 .map_err(|e| PGNError::InvalidPgn(e.to_string()))?
31 .next()
32 .ok_or(PGNError::InvalidPgn(input.to_string()))?;
33
34 let mut games = Vec::new();
35
36 for pgn in pair.into_inner() {
37 if !matches!(pgn.as_rule(), Rule::pgn) {
38 continue;
39 }
40 games.push(parse_single_pgn(pgn)?);
41 }
42
43 let mut variants: Vec<T> = Vec::new();
44
45 for game in games.iter() {
46 if game.get_variant() != T::name()
47 && !(T::name() == StandardChess::name() && game.get_variant() == "From Position")
48 {
49 return Err(PGNError::InvalidVariant(game.get_variant()));
50 }
51 let variant = T::new(game.clone());
52 variants.push(variant);
53 }
54
55 Ok(variants)
56}
57
58pub fn parse_pgn<T: Variant + VariantBuilder>(input: &str) -> Result<T, PGNError> {
70 let pair = PGNParser::parse(Rule::pgn, input)
71 .map_err(|_| PGNError::InvalidPgn(input.to_string()))?
72 .next()
73 .ok_or(PGNError::InvalidPgn(input.to_string()))?;
74
75 let game = parse_single_pgn(pair)?;
76 if game.get_variant() != T::name()
77 && !(T::name() == StandardChess::name() && game.get_variant() == "From Position")
78 {
79 return Err(PGNError::InvalidVariant(game.get_variant()));
80 }
81
82 Ok(T::new(game))
83}
84
85fn parse_sequence(game: &mut Game, sequence: Pair<Rule>) {
92 for subsequence in sequence.into_inner() {
93 match subsequence.as_rule() {
94 Rule::line => {
95 parse_line(game, subsequence);
96 }
97 Rule::white_sequence => {
98 parse_white_sequence(game, subsequence);
99 }
100 Rule::black_sequence => {
101 parse_black_sequence(game, subsequence);
102 }
103 _ => unreachable!(),
104 }
105 }
106}
107
108fn parse_single_pgn(pgn: Pair<Rule>) -> Result<Game, PGNError> {
119 let mut game = Game::default();
120
121 let mut metadata = HashMap::new();
122
123 for record in pgn.into_inner() {
124 match record.as_rule() {
125 Rule::metadata => {
126 let mut pairs = record.into_inner();
127 let key = pairs.next().unwrap().as_span().as_str();
128 let op_value = pairs.next();
129 let value = if op_value.is_some() {
130 op_value.unwrap().as_span().as_str()
131 } else {
132 ""
133 };
134
135 if !metadata.contains_key(key) {
136 metadata.insert(key, value);
137 }
138 if key == "FEN" {
139 game = Game::from_fen(value)?;
140 }
141 }
142 Rule::sequence => {
143 metadata.iter().for_each(|(key, value)| {
144 if let Err(e) = game.history.add_metadata(key, value) {
145 eprintln!("Warning: {}", e);
146 }
147 });
148 parse_sequence(&mut game, record);
149 }
150 Rule::result => (),
151 Rule::COMMENT => (),
152 _ => unreachable!(),
153 }
154 }
155
156 Ok(game)
157}
158
159fn parse_white_sequence(game: &mut Game, white_sequence: Pair<Rule>) {
166 for mov_type in white_sequence.into_inner() {
167 match mov_type.as_rule() {
168 Rule::partial_move => {
169 parse_partial_move(game, mov_type);
170 }
171 Rule::full_move => {
172 parse_full_move(game, mov_type);
173 }
174 Rule::half_sequence => {
175 parse_half_sequence(game, mov_type);
176 }
177 Rule::multi_subsequence => {
178 parse_multi_subsequence(game, mov_type);
179 }
180 Rule::COMMENT => (),
181 _ => unreachable!(),
182 }
183 }
184}
185
186fn parse_black_sequence(game: &mut Game, black_sequence: Pair<Rule>) {
193 for mov_type in black_sequence.into_inner() {
194 match mov_type.as_rule() {
195 Rule::full_move => {
196 parse_full_move(game, mov_type);
197 }
198 Rule::sequence => {
199 parse_sequence(game, mov_type);
200 }
201 Rule::multi_half_subsequence => {
202 parse_multi_half_subsequence(game, mov_type);
203 }
204 Rule::COMMENT => (),
205 _ => unreachable!(),
206 }
207 }
208}
209
210fn parse_multi_subsequence(game: &mut Game, multi_subsequence: Pair<Rule>) {
217 for subsequence in multi_subsequence.into_inner() {
218 match subsequence.as_rule() {
219 Rule::subsequence => {
220 parse_subsequence(game, subsequence.clone());
221 }
222 Rule::COMMENT => (),
223 _ => unreachable!(),
224 }
225 }
226}
227
228fn parse_subsequence(game: &mut Game, subsequence: Pair<Rule>) {
235 let sequence = subsequence.into_inner().next().unwrap();
236
237 game.undo();
238
239 let root_fullmove_number = game.fullmove_number;
240
241 parse_sequence(game, sequence);
242
243 let mut fullmove_number = game.fullmove_number;
244
245 while root_fullmove_number != fullmove_number && game.fen() != game.starting_fen {
246 game.undo();
247 fullmove_number = game.fullmove_number;
248 }
249
250 game.undo();
251 game.redo();
252}
253
254fn parse_multi_half_subsequence(game: &mut Game, multi_half_subsequence: Pair<Rule>) {
261 for half_subsequence in multi_half_subsequence.into_inner() {
262 match half_subsequence.as_rule() {
263 Rule::half_subsequence => {
264 parse_half_subsequence(game, half_subsequence);
265 }
266 Rule::COMMENT => (),
267 _ => unreachable!(),
268 }
269 }
270}
271
272fn parse_half_sequence(game: &mut Game, half_sequence: Pair<Rule>) {
279 for pair in half_sequence.into_inner() {
280 match pair.as_rule() {
281 Rule::r#move => {
282 let mov = pair.as_span().as_str();
283 game.move_piece(mov).unwrap();
284 }
285 Rule::half_move => {
286 parse_half_move(game, pair);
287 }
288 Rule::sequence => {
289 parse_sequence(game, pair);
290 }
291 Rule::COMMENT => (),
292 _ => unreachable!(),
293 }
294 }
295}
296
297fn parse_half_subsequence(game: &mut Game, half_subsequence: Pair<Rule>) {
304 let half_sequence = half_subsequence.into_inner().next().unwrap();
305
306 game.undo();
307 let first_fen = game.fen();
308
309 parse_half_sequence(game, half_sequence);
310
311 game.undo();
312
313 while game.fen() != first_fen {
314 game.undo();
315 }
316
317 game.redo();
318}
319
320fn parse_line(game: &mut Game, line: Pair<Rule>) {
327 for mov_type in line.into_inner() {
328 match mov_type.as_rule() {
329 Rule::partial_move => {
330 parse_partial_move(game, mov_type);
331 }
332 Rule::full_move => {
333 parse_full_move(game, mov_type);
334 }
335 Rule::COMMENT => (),
336 _ => unreachable!(),
337 }
338 }
339}
340
341fn parse_partial_move(game: &mut Game, partial_move: Pair<Rule>) {
348 for part in partial_move.into_inner() {
349 match part.as_rule() {
350 Rule::r#move => {
351 let mov = part.as_span().as_str();
352 game.move_piece(mov).unwrap();
353 }
354 Rule::move_number | Rule::COMMENT => (),
355 _ => {
356 println!("{:?}", part);
357 unreachable!()
358 }
359 }
360 }
361}
362
363fn parse_half_move(game: &mut Game, half_move: Pair<Rule>) {
370 for part in half_move.into_inner() {
371 match part.as_rule() {
372 Rule::r#move => {
373 let mov = part.as_span().as_str();
374 game.move_piece(mov).unwrap();
375 }
376 Rule::second_move_number | Rule::COMMENT => (),
377 _ => unreachable!(),
378 }
379 }
380}
381
382fn parse_full_move(game: &mut Game, full_move: Pair<Rule>) {
389 for part in full_move.into_inner() {
390 match part.as_rule() {
391 Rule::r#move => {
392 let mov = part.as_span().as_str();
393 game.move_piece(mov).unwrap();
394 }
395 Rule::move_number | Rule::COMMENT => (),
396 _ => unreachable!(),
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use crate::{core::Variant, utils::os::read_file, variants::StandardChess};
404
405 use super::*;
406
407 #[test]
408 fn test_parse_pgn() {
409 let input = "[Event \"caro kann: exchange\"]
410 [Site \"https://lichess.org/study/H6cwzXnM/pE8AwLer\"]
411 [Result \"1-0\"]
412 [Variant \"Standard\"]
413 [ECO \"B13\"]
414 [Opening \"Caro-Kann Defense: Exchange Variation\"]
415 [Annotator \"https://lichess.org/@/Santicastrom\"]
416 [UTCDate \"2021.07.20\"]
417 [UTCTime \"16:35:48\"]
418 1. e4 c6 2. d4 d5 3. exd5 cxd5 { [%cal Gc2c4,Gg1f3,Gc2c3,Gc1f4,Yf1d3] } 4. Bd3 (4. c4 Nf6 { [%cal Gg1f3,Gc4d5,Gc4c5,Yb1c3] } 5. Nc3 Nc6 { [%cal Gc4d5,Gc1f4,Gc1e3,Rg1f3,Bc1g5] } 6. Nf3 (6. Bg5 a6 7. Bxf6 (7. Nf3 Be6 { [%cal Yf1e2,Gc4c5] } 8. Be2 (8. c5 g6 9. Bd3 Bg7) 8... g6 { [%cal Gb2b3,Yg5f6,Ge1g1] } 9. Bxf6 exf6 { [%cal Gc4c5,Ye1g1] } 10. O-O Bg7 { [%cal Yd1d2,Gc4c5] } 11. Qd2 h5 { [%cal Rb2b3] }) 7... exf6 8. cxd5 Ne7 { [%cal Rd1a4] } 9. Qa4+ Bd7) 6... g6 { [%cal Gc1g5,Gf1e2,Gc4c5,Yc4d5,Gh2h3]} 7. cxd5 Nxd5 { [%csl Gd1,Gb3][%cal Gd1b3,Gf1c4,Gf1b5,Gf1e2,Gd1d3] } 8. Qb3 e6 { [%csl Gf1,Gb5][%cal Gf1b5,Gc1g5,Gf1c4,Gc3d5,Gf1e2] } 9. Bb5 Bg7 { [%csl Ge1,Gg1][%cal Ge1g1,Gc1g5,Gb5c6,Gc3d5,Gf3e5,Gb3a3] } 10. O-O O-O { [%csl Gb5,Gc6][%cal Gb5c6,Gf1d1,Gc3d5,Gc1g5] } 11. Bxc6 bxc6 { [%csl Gc3,Ga4][%cal Gc3a4,Gf1e1,Gc3e4,Gf1d1,Gc1d2,Gc1g5] } 12. Na4 Qd6 { [%csl Gf1,Ge1][%cal Gf1e1,Gc1d2,Gb3d1,Ga2a3,Ga4c5] } 13. Re1 Rb8 { [%csl Gb3,Gd1][%cal Gb3d1,Gb3c2,Gb3d3] } 14. Qd1 c5 15. Nxc5 Bb7 { [%csl Gc5,Gb7][%cal Gc5b7,Gc5e4] } 16. Nxb7 Rxb7 { [%csl Gb2,Gb3][%cal Gb2b3,Gh2h4,Ge1e2] } 17. b3 Rc8 { [%cal Gf3e5] }) (4. Nf3 Nc6 { [%cal Bc2c3,Rc2c4,Gf3e5,Gf1b5,Gf1e2,Gc1f4,Gh2h3,Gb1d2,Gb1c3,Gb2b3] } 5. c3 (5. c4) (5. Bb5 Qa5+ 6. Nc3 Bg4 { [%cal Rh2h3,Bc1d2] })) 4... Nc6 { [%cal Gg1f3,Yc2c3,Gg1e2,Ga2a3,Gc1f4] } 5. c3 (5. Nf3 Bg4 { [%cal Yc2c3,Gc1e3,Ge1g1,Gb1d2] } 6. c3 Qc7 { [%cal Rb1d2,Be1g1,Gh2h3,Gc1e3,Gc1g5,Gd1b3,Gb1a3] } 7. O-O e6 { [%cal Rb1d2,Bh2h3,Gf1e1,Gc1e3,Gc1g5,Gb1a3] } 8. h3 Bh5 { [%cal Yf1e1,Gc1e3,Gb2b4,Gb1d2,Ga2a4] } 9. Re1 Bd6 { [%cal Yb1d2,Gc1g5,Gb2b4,Gc1e3,Gb1a3] } 10. Nbd2 Nge7 { [%cal Ra2a4,Bd2f1,Gd2b3,Gb2b3] } 11. Nf1 h6 { [%cal Yd3e2] } 12. Be2 Bg6 { [%cal Re2d3,Bf3h4] }) 5... Nf6 { [%cal Gc1g5,Gg1e2,Gh2h3,Rc1f4,Bg1f3] } 6. Nf3 (6. Bf4 Bg4 { [%cal Rd1c2,Bd1b3,Gg1f3,Gg1e2,Gf2f3,Gd1a4,Gd3e2] } 7. Qb3 (7. f3 Bh5 8. g4 Bg6 9. Ne2) 7... Qd7 { [%cal Yb1d2,Gh2h3] } 8. Nd2 e6 { [%cal Yg1f3,Gh2h3] } 9. Ngf3 Bd6 { [%cal Yf4d6,Gf3e5,Gf4e5,Gf4g3,Ge1g1,Gf4g5] } 10. Bxd6 Qxd6 { [%cal Ye1g1,Gb3b7,Gh2h3] } 11. O-O O-O { [%cal Yf1e1,Ga1e1] } 12. Rfe1 Bh5 { [%cal Bf3e5,Rh2h3] } 13. Ne5 Qc7 { [%cal Bf2f4,Rb3c2,Gh2h3] } 14. f4 Ne7 { [%cal Gb3c2,Ra2a3,Gg2g3] }) (6. Bg5 Bg4 { [%cal Bd1b3,Rg1e2,Gg1f3] } 7. Qb3 (7. Ne2 e6 { [%cal Yd1c2,Gd1b3] } 8. Qc2 Qc7 { [%cal Gg5f6,Gg7f6,Gd3h7,Ye2g3] } 9. Ng3 Nh5 { [%cal Yb1d2] }) 7... e5 { [%cal Rb3b7] } 8. Qxb7 Bd7 9. Bxf6 gxf6 10. Bf5 Rb8 { [%cal Rf5d7] }) 6... Bg4 { [%cal Gb1d2,Be1g1,Rh2h3] } 7. O-O Qb8 { [%cal Rh2h3] } 8. h3 Bh5 { [%cal Rg2g3,Gc1g5] } 1-0";
419
420 let variant: StandardChess = parse_pgn(input).unwrap();
421
422 let pgn = variant.pgn();
423
424 assert!(pgn.contains("[Event \"caro kann: exchange\"]"));
425 assert!(pgn.contains("[Site \"https://lichess.org/study/H6cwzXnM/pE8AwLer\"]"));
426 assert!(pgn.contains("[Result \"1-0\"]"));
427 assert!(!pgn.contains("[Variant \"Standard\"]"));
428 assert!(pgn.contains("[ECO \"B13\"]"));
429 assert!(pgn.contains("[Opening \"Caro-Kann Defense: Exchange Variation\"]"));
430 assert!(pgn.contains("[Annotator \"https://lichess.org/@/Santicastrom\"]"));
431 assert!(pgn.contains("[UTCDate \"2021.07.20\"]"));
432 assert!(pgn.contains("[UTCTime \"16:35:48\"]"));
433 assert!(pgn.contains("1. e4 c6 2. d4 d5 3. exd5 cxd5 4. Bd3 (4. c4 Nf6 5. Nc3 Nc6 6. Nf3 (6. Bg5 a6 7. Bxf6 (7. Nf3 Be6 8. Be2 (8. c5 g6 9. Bd3 Bg7) 8... g6 9. Bxf6 exf6 10. O-O Bg7 11. Qd2 h5) 7... exf6 8. cxd5 Ne7 9. Qa4+ Bd7) 6... g6 7. cxd5 Nxd5 8. Qb3 e6 9. Bb5 Bg7 10. O-O O-O 11. Bxc6 bxc6 12. Na4 Qd6 13. Re1 Rb8 14. Qd1 c5 15. Nxc5 Bb7 16. Nxb7 Rxb7 17. b3 Rc8) (4. Nf3 Nc6 5. c3 (5. c4) (5. Bb5 Qa5+ 6. Nc3 Bg4)) 4... Nc6 5. c3 (5. Nf3 Bg4 6. c3 Qc7 7. O-O e6 8. h3 Bh5 9. Re1 Bd6 10. Nbd2 Nge7 11. Nf1 h6 12. Be2 Bg6) 5... Nf6 6. Nf3 (6. Bf4 Bg4 7. Qb3 (7. f3 Bh5 8. g4 Bg6 9. Ne2) 7... Qd7 8. Nd2 e6 9. Ngf3 Bd6 10. Bxd6 Qxd6 11. O-O O-O 12. Rfe1 Bh5 13. Ne5 Qc7 14. f4 Ne7) (6. Bg5 Bg4 7. Qb3 (7. Ne2 e6 8. Qc2 Qc7 9. Ng3 Nh5) 7... e5 8. Qxb7 Bd7 9. Bxf6 gxf6 10. Bf5 Rb8) 6... Bg4 7. O-O Qb8 8. h3 Bh5 1-0"));
434 }
435
436 #[test]
437 fn test_invalid_variant() {
438 let input = "[Variant \"Chess960\"]
439 1. e4 c6";
440 let result: Result<StandardChess, PGNError> = parse_pgn(input);
441 assert!(result.is_err());
442 }
443
444 #[test]
445 fn test_multiple_invalid_variants() {
446 let input = "[Event \"caro kann: exchange\"]
447 [Variant \"Chess960\"]
448 [Site \"\"]
449 1. e4 c6
450 [Event \"another game\"]
451 [Variant \"Standard\"]
452 1. d4 d5";
453 let result: Result<Vec<StandardChess>, PGNError> = parse_multiple_pgn(&input);
454 assert!(result.is_err());
455 }
456
457 #[test]
458 fn test_comments_in_pgn() {
459 let input = "[Event \"game with comments\"]
460 {First comment}
461 1. e4 {A comment} (1. c4 {Another comment}) {Other one} (1. d4) e5 2. {Other} Nf3 Nc6 {And other} (2... {Another one} Nf6 3. Nxe5 {And other} d6) {And other} (2... g6) 3. Bb5 (3. Bc4) 3... {This one} a6
462 {Last one}";
463
464 let variant = parse_pgn::<StandardChess>(&input).unwrap();
465
466 assert_eq!(variant.pgn(), "[Event \"game with comments\"]\n[Site \"\"]\n[Date \"\"]\n[Round \"\"]\n[White \"\"]\n[Black \"\"]\n[Result \"\"]\n1. e4 (1. c4) (1. d4) 1... e5 2. Nf3 Nc6 (2... Nf6 3. Nxe5 d6) (2... g6) 3. Bb5 (3. Bc4) 3... a6")
467 }
468
469 #[test]
470 fn test_multiple_lines_pgn() {
471 let input = "[Event \"game 1\"]
472 1. e4 (1. c4) (1. d4) e5 2. Nf3 Nc6 (2... Nf6 3. Nxe5 d6) (2... g6) 3. Bb5 (3. Bc4) 3... a6";
473
474 let variant = parse_pgn::<StandardChess>(&input).unwrap();
475
476 assert_eq!(variant.pgn(), "[Event \"game 1\"]\n[Site \"\"]\n[Date \"\"]\n[Round \"\"]\n[White \"\"]\n[Black \"\"]\n[Result \"\"]\n1. e4 (1. c4) (1. d4) 1... e5 2. Nf3 Nc6 (2... Nf6 3. Nxe5 d6) (2... g6) 3. Bb5 (3. Bc4) 3... a6")
477 }
478
479 #[test]
480 fn test_from_fen_in_pgn() {
481 let input = "[Event \"game from FEN\"]
482 [FEN \"r1bqkbnr/pppppppp/n7/8/8/5N2/PPPPPPPP/RNBQKB1R w KQkq - 0 1\"]
483 1. e4 e5 2. Nc3 Ne7";
484
485 let variant = parse_pgn::<StandardChess>(&input).unwrap();
486
487 assert_eq!(
488 variant.fen(),
489 "r1bqkb1r/ppppnppp/n7/4p3/4P3/2N2N2/PPPP1PPP/R1BQKB1R w KQkq - 2 3"
490 );
491 }
492
493 #[test]
494 fn test_parse_pgn_file() {
495 let input = read_file("data/standard/ex3.pgn").unwrap();
496
497 let variants: Vec<StandardChess> = parse_multiple_pgn(&input).unwrap();
498
499 assert_eq!(variants.len(), 20);
500
501 for variant in variants {
502 println!("{}\n", variant.pgn());
503 }
504 }
505}