Skip to main content

chess_lab/parsing/pgn/
mod.rs

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/// PGN parser struct that uses the pest library to parse PGN strings
14#[derive(Parser)]
15#[grammar = "./src/parsing/pgn/pgn.pest"]
16struct PGNParser;
17
18/// Parse a multiple games PGN string into a vector of [Game] structs
19///
20/// # Arguments
21/// * `input` - A string slice that holds the PGN string to be parsed
22///
23/// # Returns
24/// A `Result<Vec<T>, PGNError>` object
25/// * `Ok(Vec<Game>)` - A vector of [Game] structs with the parsed PGNs
26/// * `Err(PGNError)` - A [PGNError] with the reason why the PGN string could not be parsed
27///
28pub 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
58/// Parse a PGN string into a Game struct
59///
60/// # Arguments
61/// * `input` - A string slice that holds the PGN to be parsed
62///
63/// # Returns
64/// A `Result<T, PGNError>` object
65/// * `Ok(Game)` - A Game struct with the parsed PGN
66/// * `Err(PgnError)` - An error with the reason why the PGN could not be parsed
67///
68/// pub fn parse_pgn<T: Variant + VariantBuilder>(input: &str) -> Result<T, PGNError> {
69pub 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
85/// Plays a sequence of moves in a game
86///
87/// # Arguments
88/// * `game` - A mutable reference to a Game struct
89/// * `sequence` - A Pair<Rule> that holds the sequence of moves to be played
90///
91fn 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
108/// Parse a PGN rule into a Game struct
109///
110/// # Arguments
111/// * `pgn` - A Pair<Rule> that holds the PGN to be parsed
112///
113/// # Returns
114/// A `Result<Game, PGNError>` object
115/// * `Ok(Game)` - A Game struct with the parsed PGN
116/// * `Err(PgnError)` - An error with the reason why the PGN could not be parsed
117///
118fn 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
159/// Plays a white variation of moves in a game
160///
161/// # Arguments
162/// * `game` - A mutable reference to a Game struct
163/// * `white_sequence` - A Pair<Rule> that holds the white sequence of moves to be played
164///
165fn 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
186/// Plays a black variation of moves in a game
187///
188/// # Arguments
189/// * `game` - A mutable reference to a Game struct
190/// * `black_sequence` - A Pair<Rule> that holds the black sequence of moves to be played
191///
192fn 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
210/// Plays a multi subsequence of moves in a game
211///
212/// # Arguments
213/// * `game` - A mutable reference to a Game struct
214/// * `multi_subsequence` - A Pair<Rule> that holds the multi subsequence of moves to be played
215///
216fn 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
228/// Plays a sub variation of moves in a game
229///
230/// # Arguments
231/// * `game` - A mutable reference to a Game struct
232/// * `subsequence` - A Pair<Rule> that holds the subsequence of moves to be played
233///
234fn 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
254/// Plays a multi half subsequence of moves in a game
255///
256/// # Arguments
257/// * `game` - A mutable reference to a Game struct
258/// * `multi_half_subsequence` - A Pair<Rule> that holds the multi half subsequence of moves to be played
259///
260fn 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
272/// Plays a sub variation of moves in a game that starts with a half move
273///
274/// # Arguments
275/// * `game` - A mutable reference to a Game struct
276/// * `half_subsequence` - A Pair<Rule> that holds the half subsequence of moves to be played
277///
278fn 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
297/// Plays a sub variation of moves in a game that starts with a half move
298///
299/// # Arguments
300/// * `game` - A mutable reference to a Game struct
301/// * `half_subsequence` - A Pair<Rule> that holds the half subsequence of moves to be played
302///
303fn 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
320/// Plays a line of moves in a game
321///
322/// # Arguments
323/// * `game` - A mutable reference to a Game struct
324/// * `line` - A Pair<Rule> that holds the line of moves to be played
325///
326fn 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
341/// Plays a partial move in a game
342///
343/// # Arguments
344/// * `game` - A mutable reference to a Game struct
345/// * `partial_move` - A Pair<Rule> that holds the partial move to be played
346///
347fn 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
363/// Plays a half move in a game
364///
365/// # Arguments
366/// * `game` - A mutable reference to a Game struct
367/// * `half_move` - A Pair<Rule> that holds the half move to be played
368///
369fn 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
382/// Plays a full move in a game
383///
384/// # Arguments
385/// * `game` - A mutable reference to a Game struct
386/// * `full_move` - A Pair<Rule> that holds the full move to be played
387///
388fn 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}