Skip to main content

chess/formats/
pgn.rs

1//! PGN format
2
3use std::{fmt, str};
4
5use winnow::Parser as _;
6
7use crate::{
8    Position, Scharnagl,
9    game::{Command, Mode, Nag, Outcome, Slot, Tag as OtherTag, Text},
10    position::Parts,
11};
12
13use super::san;
14
15pub mod convert;
16pub mod parse;
17pub mod stream;
18
19pub use parse::game;
20
21// https://www.chessprogramming.org/Portable_Game_Notation
22// https://www.saremba.de/chessgml/standards/pgn/pgn-complete.htm
23// https://github.com/mliebelt/pgn-spec-commented
24// https://github.com/mliebelt/pgn-spec-commented/blob/main/pgn-spec-supplement.md
25//
26// Arrows and coloured squares
27// [%cal Gc2c3,Rc3d4] green arrow c2-c3, red arrow c3-d4
28// [%csl Ra3,Ga4] a3 red, a4 green
29// # insert mini board in move list
30// https://chesstempo.com/manual/en/manual.html#pgnviewercommentannotations
31//
32
33#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Game {
35    pub tags: Vec<Tag>,
36    pub start: Parts,
37    pub intro: Option<Comment>,
38    pub moves: Vec<Move>,
39    pub outcome: Outcome,
40}
41
42#[derive(Clone, Debug, PartialEq, Eq)]
43pub enum Tag {
44    Event(String),
45    Site(String),
46    Date(String),
47    Round(String),
48    White(String),
49    Black(String),
50    Outcome(Outcome),
51    Fen(Parts),
52    SetUp(bool),
53    Variant(String),
54    Chess960Id(Scharnagl),
55    Other(OtherTag),
56}
57
58impl Tag {
59    pub fn freestyle() -> Self {
60        Tag::Variant("Chess960".to_string())
61    }
62
63    pub fn variant(&self) -> Option<&str> {
64        if let Tag::Variant(value) = self { Some(value) } else { None }
65    }
66}
67
68#[derive(Debug, thiserror::Error)]
69pub enum Error {
70    #[error(transparent)]
71    Parse(#[from] parse::Error),
72    #[error(transparent)]
73    Convert(#[from] convert::Error),
74}
75
76pub type Result<T, E = Error> = core::result::Result<T, E>;
77
78impl fmt::Display for Game {
79    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80        let shredder = self.mode().is_freestyle();
81        for tag in &self.tags {
82            if let Tag::Fen(position) = tag {
83                let fen = if shredder { position.shredder_fen() } else { position.fen() };
84                write_tag(f, "FEN", &fen)?;
85                writeln!(f)?;
86            } else {
87                writeln!(f, "{tag}")?;
88            }
89        }
90        if !self.tags.is_empty() {
91            writeln!(f)?;
92        }
93        let mut wrap = Wrap::<_, 80>::new(f);
94        if let Some(intro) = &self.intro {
95            wrap.token(intro)?;
96        }
97        write_moves(&self.moves, &mut wrap, self.start.ply(), Notation::San)?;
98        wrap.token(self.outcome)
99    }
100}
101
102impl str::FromStr for Game {
103    type Err = Error;
104
105    fn from_str(text: &str) -> Result<Self> {
106        parse::game.parse(text).map_err(|error| parse::Error::from(text, 1, error).into())
107    }
108}
109
110#[derive(Clone, Debug, PartialEq, Eq)]
111pub struct Variation {
112    pub intro: Option<Comment>,
113    pub moves: Vec<Move>,
114    pub outro: Option<Comment>,
115}
116
117#[derive(Clone, Debug, PartialEq, Eq)]
118pub struct Move {
119    pub san: san::San,
120    pub comment: Option<Comment>,
121    pub annotations: Vec<Annotation>,
122    pub variations: Vec<Variation>,
123}
124
125#[derive(Clone, Debug, PartialEq, Eq)]
126pub struct Comment(Text);
127
128#[derive(Clone, Debug, PartialEq, Eq)]
129pub enum Annotation {
130    Nag(Nag),
131    Command(Command),
132}
133
134impl Game {
135    pub fn movetext(&self) -> String {
136        self.write_movetext(Notation::San)
137    }
138
139    pub fn figurine_movetext(&self) -> String {
140        self.write_movetext(Notation::Figurine)
141    }
142
143    fn write_movetext(&self, notation: Notation) -> String {
144        let mut movetext = String::new();
145        let mut wrap = Wrap::<_, 0>::new(&mut movetext);
146        if let Some(intro) = &self.intro {
147            wrap.token(intro).expect("writing PGN movetext to string");
148        }
149        write_moves(&self.moves, &mut wrap, self.start.ply(), notation)
150            .expect("writing PGN movetext to string");
151        movetext
152    }
153
154    pub fn mode(&self) -> Mode {
155        for tag in &self.tags {
156            let Some(variant) = tag.variant() else { continue };
157            if Mode::from_tag(variant) == Some(Mode::Freestyle) {
158                return Mode::Freestyle;
159            }
160        }
161
162        if self.start.castles.chess_compatible() {
163            Mode::Chess
164        } else {
165            Mode::Freestyle
166        }
167    }
168}
169
170impl fmt::Display for Tag {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        match self {
173            Tag::Event(value) => write_tag(f, "Event", value),
174            Tag::Site(value) => write_tag(f, "Site", value),
175            Tag::Date(value) => write_tag(f, "Date", value),
176            Tag::Round(value) => write_tag(f, "Round", value),
177            Tag::White(value) => write_tag(f, "White", value),
178            Tag::Black(value) => write_tag(f, "Black", value),
179            Tag::Outcome(outcome) => write_tag(f, "Result", &outcome.to_string()),
180            Tag::Fen(position) => write_tag(f, "FEN", &position.fen()),
181            Tag::SetUp(setup) => write_tag(f, "SetUp", if *setup { "1" } else { "0" }),
182            Tag::Variant(variant) => write_tag(f, "Variant", variant),
183            Tag::Chess960Id(id) => write_tag(f, "Chess960Id", &id.to_string()),
184            Tag::Other(tag) => write_tag(f, tag.key.as_ref(), &tag.value),
185        }
186    }
187}
188
189impl Mode {
190    fn from_tag(value: &str) -> Option<Self> {
191        match value.to_ascii_lowercase().as_str() {
192            "chess" | "standard" => Some(Self::Chess),
193            "chess960" | "fischerandom" | "fischer random" | "freestyle" => {
194                Some(Self::Freestyle)
195            }
196            _ => None,
197        }
198    }
199}
200
201fn write_tag(f: &mut fmt::Formatter<'_>, key: &str, value: &str) -> fmt::Result {
202    fn escape_tag_value(value: &str) -> String {
203        value.replace('\\', "\\\\").replace('"', "\\\"")
204    }
205
206    write!(f, "[{} \"{}\"]", key, escape_tag_value(value))
207}
208
209fn start_position(tags: &[Tag]) -> Parts {
210    tags.iter()
211        .rev()
212        .find_map(|tag| match tag {
213            Tag::Fen(position) => Some(*position),
214            _ => None,
215        })
216        .unwrap_or_else(|| Position::start().parts())
217}
218
219impl fmt::Display for Annotation {
220    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
221        match self {
222            Annotation::Nag(nag) => write!(f, "{nag}"),
223            Annotation::Command(command) => write!(f, "{command}"),
224        }
225    }
226}
227
228impl fmt::Display for Command {
229    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
230        write!(f, "[%{}", self.command)?;
231        for (index, parameter) in self.parameters.iter().enumerate() {
232            if index == 0 {
233                write!(f, " ")?;
234            } else {
235                write!(f, ",")?;
236            }
237            write!(f, "{parameter}")?;
238        }
239        write!(f, "]")
240    }
241}
242
243impl fmt::Display for Nag {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        match self {
246            Nag::Numeric(nag) => write!(f, "${nag}"),
247            Nag::Symbol(nag) => write!(f, "{nag}"),
248        }
249    }
250}
251
252impl fmt::Display for Outcome {
253    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
254        match self {
255            Outcome::White => write!(f, "1-0"),
256            Outcome::Black => write!(f, "0-1"),
257            Outcome::Draw => write!(f, "1/2-1/2"),
258            Outcome::Unknown => write!(f, "*"),
259        }
260    }
261}
262
263fn write_moves<W: fmt::Write + ?Sized, const WIDTH: usize>(
264    moves: &[Move],
265    wrap: &mut Wrap<'_, W, WIDTH>,
266    first_ply: usize,
267    notation: Notation,
268) -> fmt::Result {
269    for (ply, (index, play)) in (first_ply..).zip(moves.iter().enumerate()) {
270        if should_write_move_number(ply, index, moves) {
271            wrap.token(MoveNumber(ply))?;
272        }
273        match notation {
274            Notation::San => wrap.token(play.san)?,
275            Notation::Figurine => wrap.token(play.san.figurine())?,
276        }
277        play.write_tail(wrap, ply, notation)?;
278    }
279    Ok(())
280}
281
282impl Move {
283    fn write_tail<W: fmt::Write + ?Sized, const WIDTH: usize>(
284        &self,
285        wrap: &mut Wrap<'_, W, WIDTH>,
286        ply: usize,
287        notation: Notation,
288    ) -> fmt::Result {
289        let mut commands = Vec::new();
290        for annotation in &self.annotations {
291            match annotation {
292                Annotation::Nag(Nag::Symbol(_)) => wrap.suffix(annotation)?,
293                Annotation::Nag(Nag::Numeric(_)) => wrap.token(annotation)?,
294                Annotation::Command(command) => commands.push(command.clone()),
295            }
296        }
297        if !commands.is_empty() || self.comment.is_some() {
298            wrap.token(MoveComment { commands, text: self.comment.clone() })?;
299        }
300        write_variations(&self.variations, wrap, ply, notation)?;
301        Ok(())
302    }
303}
304
305fn write_variations<W: fmt::Write + ?Sized, const WIDTH: usize>(
306    variations: &[Variation],
307    wrap: &mut Wrap<'_, W, WIDTH>,
308    ply: usize,
309    notation: Notation,
310) -> fmt::Result {
311    for variation in variations {
312        wrap.open("(")?;
313        if let Some(intro) = &variation.intro {
314            wrap.token(intro)?;
315        }
316        write_moves(&variation.moves, wrap, ply, notation)?;
317        wrap.close(")")?;
318        if let Some(outro) = &variation.outro {
319            wrap.token(outro)?;
320        }
321    }
322    Ok(())
323}
324
325#[derive(Clone, Copy)]
326enum Notation {
327    San,
328    Figurine,
329}
330
331#[derive(Debug)]
332struct MoveNumber(usize);
333
334struct MoveComment {
335    commands: Vec<Command>,
336    text: Option<Comment>,
337}
338
339impl Comment {
340    fn merge(&mut self, comment: &Comment) {
341        self.0.merge(&comment.0);
342    }
343}
344
345impl From<Comment> for Text {
346    fn from(comment: Comment) -> Self {
347        comment.0
348    }
349}
350
351impl fmt::Display for Comment {
352    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353        write!(f, "{{{}}}", self.0)
354    }
355}
356
357impl fmt::Display for MoveComment {
358    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
359        write!(f, "{{")?;
360        for (index, command) in self.commands.iter().enumerate() {
361            if index > 0 {
362                write!(f, " ")?;
363            }
364            write!(f, "{command}")?;
365        }
366        if let Some(text) = &self.text {
367            if !self.commands.is_empty() {
368                write!(f, " ")?;
369            }
370            write!(f, "{}", text.0)?;
371        }
372        write!(f, "}}")
373    }
374}
375
376impl fmt::Display for MoveNumber {
377    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
378        let ply = self.0;
379        let number = ply / 2 + 1;
380        if ply.is_multiple_of(2) { write!(f, "{number}.") } else { write!(f, "{number}...") }
381    }
382}
383
384struct Wrap<'a, W: fmt::Write + ?Sized, const WIDTH: usize = 80> {
385    f: &'a mut W,
386    line: usize,
387    glued: bool,
388}
389
390impl<'a, W: fmt::Write + ?Sized, const WIDTH: usize> Wrap<'a, W, WIDTH> {
391    fn new(f: &'a mut W) -> Self {
392        Self { f, line: 0, glued: false }
393    }
394
395    fn token(&mut self, token: impl fmt::Display) -> fmt::Result {
396        let token = token.to_string();
397        let separator = usize::from(self.line > 0 && !self.glued);
398        if WIDTH > 0 && self.line > 0 && self.line + separator + token.len() > WIDTH {
399            writeln!(self.f)?;
400            self.line = 0;
401            self.glued = false;
402        } else if self.line > 0 && !self.glued {
403            write!(self.f, " ")?;
404            self.line += 1;
405        }
406        write!(self.f, "{token}")?;
407        self.line += token.len();
408        self.glued = false;
409        Ok(())
410    }
411
412    fn suffix(&mut self, token: impl fmt::Display) -> fmt::Result {
413        let token = token.to_string();
414        write!(self.f, "{token}")?;
415        self.line += token.len();
416        self.glued = false;
417        Ok(())
418    }
419
420    fn open(&mut self, token: &str) -> fmt::Result {
421        self.token(token)?;
422        self.glued = true;
423        Ok(())
424    }
425
426    fn close(&mut self, token: &str) -> fmt::Result {
427        write!(self.f, "{token}")?;
428        self.line += token.len();
429        self.glued = false;
430        Ok(())
431    }
432}
433
434fn should_write_move_number(ply: usize, index: usize, moves: &[Move]) -> bool {
435    ply.is_multiple_of(2)
436        || index == 0
437        || moves[index - 1].has_intervening_annotation_or_variation()
438}
439
440impl Move {
441    fn has_intervening_annotation_or_variation(&self) -> bool {
442        !self.annotations.is_empty() || self.comment.is_some() || !self.variations.is_empty()
443    }
444}
445
446#[cfg(test)]
447mod tests {
448    use crate::{
449        Position,
450        board::{Role::*, Scharnagl},
451        formats::san,
452        game::Mode,
453        square::{File::*, Square::*},
454    };
455
456    use super::*;
457
458    fn text(text: &str) -> Text {
459        Text::new(text).unwrap()
460    }
461
462    fn comment(comment: &str) -> Comment {
463        Comment(text(comment))
464    }
465
466    #[test]
467    fn parses_game() {
468        let pgn = r#"
469            [Event "Casual Game"]
470            [Site "Berlin GER"]
471            [Date "1852.??.??"]
472            [Round "?"]
473            [White "Adolf Anderssen"]
474            [Black "Jean Dufresne"]
475            [Result "1-0"]
476
477            1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 {Italian Game} 1-0
478        "#;
479
480        let game = game.parse(pgn).unwrap();
481        assert_eq!(game.tags.len(), 7);
482        assert_eq!(game.tags[0], Tag::Event("Casual Game".to_string()));
483        assert_eq!(game.moves.len(), 6);
484        assert_eq!(game.outcome, Outcome::White);
485        assert_eq!(game.moves[5].comment, Some(comment("Italian Game")));
486    }
487
488    #[test]
489    fn parses_variation_and_nags() {
490        let pgn = r#"[Event "x"]
491
4921. e4! e5 $1 (1... c5 {Sicilian}) 2. Nf3 *
493"#;
494
495        let game = game.parse(pgn).unwrap();
496        let e4 = &game.moves[0];
497        assert_eq!(
498            e4.san.play,
499            san::Move::Normal {
500                role: Pawn,
501                file: None,
502                rank: None,
503                capture: false,
504                to: E4,
505                promotion: None,
506            }
507        );
508        assert_eq!(e4.annotations, vec![Annotation::Nag(Nag::Symbol("!".to_string()))]);
509
510        let e5 = &game.moves[1];
511        assert_eq!(e5.annotations, vec![Annotation::Nag(Nag::Numeric(1))]);
512        assert_eq!(e5.variations.len(), 1);
513        assert_eq!(e5.variations[0].moves[0].comment, Some(comment("Sicilian")));
514        assert_eq!(game.outcome, Outcome::Unknown);
515    }
516
517    #[test]
518    fn converts_to_game() {
519        let pgn = game
520            .parse(
521                r#"[Event "x"]
522
5231. e4! e5 $1 (1... c5 {Sicilian}) 2. Nf3 *
524"#,
525            )
526            .unwrap();
527        let game = crate::Game::from_pgn(pgn).unwrap();
528
529        assert_eq!(game.roster.event, Some(text("x")));
530        assert_eq!(game.tags.len(), 0);
531        assert_eq!(game.start_options().len(), 1);
532
533        let e4_id = game.start_options().first().unwrap().slot();
534        let e4 = game.play(e4_id).unwrap();
535        assert_eq!(e4.play().to, E4);
536        assert_eq!(e4.meta.nags, vec![Nag::Symbol("!".to_string())]);
537        let e4_options = e4.options();
538        assert_eq!(e4_options.len(), 2);
539
540        let (e5, variations) = e4_options.split_first().unwrap();
541        assert_eq!(e5.play().to, E5);
542        assert_eq!(e5.meta.nags, vec![Nag::Numeric(1)]);
543
544        let c5 = variations.first().unwrap();
545        assert_eq!(c5.play().to, C5);
546        assert_eq!(c5.meta.comment, Some(text("Sicilian")));
547    }
548
549    #[test]
550    fn converts_fen_game() {
551        let pgn = game
552            .parse(
553                r#"[FEN "4k3/8/8/8/8/8/4P3/4K3 w - - 0 1"]
554
5551. e4 *
556"#,
557            )
558            .unwrap();
559        let game = crate::Game::from_pgn(pgn).unwrap();
560
561        let options = game.start_options();
562        let e4 = options.first().unwrap();
563        assert_eq!(e4.play().from, E2);
564        assert_eq!(e4.play().to, E4);
565    }
566
567    #[test]
568    fn start_uses_last_fen_tag() {
569        let pgn = game
570            .parse(
571                r#"[FEN "4k3/8/8/8/8/8/8/4K3 w - - 0 1"]
572[FEN "4k3/8/8/8/8/8/4P3/4K3 w - - 0 1"]
573
5741. e4 *
575"#,
576            )
577            .unwrap();
578
579        assert_eq!(pgn.start.fen(), "4k3/8/8/8/8/8/4P3/4K3 w - - 0 1");
580    }
581
582    #[test]
583    fn parses_variant_tag() {
584        let pgn = r#"
585            [Variant "Fischer Random"]
586            *
587        "#;
588        let pgn = game.parse(pgn).unwrap();
589
590        assert_eq!(pgn.tags, vec![Tag::Variant("Fischer Random".to_string())]);
591        assert_eq!(pgn.mode(), Mode::Freestyle);
592        assert!(pgn.to_string().contains("[Variant \"Fischer Random\"]"));
593    }
594
595    #[test]
596    fn freestyle_variant_wins() {
597        let pgn = r#"
598            [Variant "Chess960"]
599            [Variant "Standard"]
600            *
601        "#;
602        let pgn = game.parse(pgn).unwrap();
603
604        assert_eq!(pgn.mode(), Mode::Freestyle);
605    }
606
607    #[test]
608    fn freestyle_variant_overrides_unsupported() {
609        let pgn = r#"
610            [Variant "Antichess"]
611            [Variant "Chess960"]
612            *
613        "#;
614        let pgn = game.parse(pgn).unwrap();
615
616        assert_eq!(pgn.mode(), Mode::Freestyle);
617
618        let pgn = r#"
619            [Variant "Chess960"]
620            [Variant "Antichess"]
621            *
622        "#;
623        let pgn = game.parse(pgn).unwrap();
624
625        assert_eq!(pgn.mode(), Mode::Freestyle);
626    }
627
628    #[test]
629    fn mode_ignores_unsupported_tags() {
630        let pgn = r#"
631            [Variant "Antichess"]
632            *
633        "#;
634        let pgn = game.parse(pgn).unwrap();
635
636        assert_eq!(pgn.mode(), Mode::Chess);
637    }
638
639    #[test]
640    fn mode_falls_back_to_position_castling() {
641        let fen = r#"
642            [FEN "8/8/8/8/8/8/8/8 w - - 0 1"]
643            *
644        "#;
645        let pgn = game.parse(fen).unwrap();
646
647        assert_eq!(pgn.mode(), Mode::Chess);
648
649        let fen = r#"
650            [Variant "Standard"]
651            [FEN "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf - 2 9"]
652            *
653        "#;
654        let pgn = game.parse(fen).unwrap();
655
656        assert_eq!(pgn.mode(), Mode::Freestyle);
657        assert!(pgn.to_string().contains(" w HFhf - 2 9\"]"));
658    }
659
660    #[test]
661    fn converts_to_game_mode() {
662        let pgn = game.parse(r#"[Event "x"] 1. e4 *"#).unwrap();
663        let chess = crate::Game::try_from(pgn).unwrap();
664        assert_eq!(chess.mode(), Mode::Chess);
665
666        let pgn = game
667            .parse(
668                r#"[Variant "Chess960"]
669[FEN "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf - 2 9"]
6709. e4 *"#,
671            )
672            .unwrap();
673        let freestyle = crate::Game::try_from(pgn).unwrap();
674        assert_eq!(freestyle.mode(), Mode::Freestyle);
675    }
676
677    #[test]
678    fn converts_unsupported_variant_as_chess() {
679        let pgn = game.parse(r#"[Variant "Antichess"] *"#).unwrap();
680        let game = crate::Game::try_from(pgn).unwrap();
681
682        assert_eq!(game.mode(), Mode::Chess);
683    }
684
685    #[test]
686    fn rejects_invalid_start() {
687        let pgn =
688            game.parse(r#"[Variant "Standard"] [FEN "4k3/8/8/8/8/8/8/4K2P w - - 0 1"] *"#).unwrap();
689        let error = crate::Game::try_from(pgn).err().unwrap();
690
691        assert!(matches!(
692            error,
693            convert::Error::Start { mode: Mode::Chess, .. }
694        ));
695    }
696
697    #[test]
698    fn displays_fen_game_from_its_start_ply() {
699        let fen = "4k3/8/8/8/8/8/4P3/4K3 b - - 0 17";
700        let position = Position::from_fen(fen).unwrap();
701        let mut game = crate::Game::chess(position).unwrap();
702        game.start_options_mut().push(crate::Move::normal(King, E8, D8)).unwrap();
703
704        let pgn = Game::from(game);
705        assert!(pgn.to_string().contains("[SetUp \"1\"]"), "{}", pgn);
706        assert!(pgn.to_string().contains(&format!("[FEN \"{fen}\"]")), "{}", pgn);
707        assert!(pgn.to_string().contains("\n17... Kd8 *"), "{}", pgn);
708        assert_eq!(pgn.movetext(), "17... Kd8");
709    }
710
711    #[test]
712    fn displays_figurine_movetext() {
713        let mut game = crate::Game::chess(Position::start()).unwrap();
714        let e4 = game.start_options_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
715        game.play_mut(e4).unwrap().options_mut().push(crate::Move::normal(Knight, G8, F6)).unwrap();
716
717        let pgn = Game::from(game);
718        assert_eq!(pgn.movetext(), "1. e4 Nf6");
719        assert_eq!(pgn.figurine_movetext(), "1. e4 ♘f6");
720    }
721
722    #[test]
723    fn converts_from_game() {
724        let mut game = crate::Game::chess(Position::start()).unwrap();
725        game.roster.event = Some(text("x"));
726
727        let e4 = game.start_options_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
728        game.start_options_mut().push(crate::Move::normal(Pawn, D2, D4)).unwrap();
729
730        {
731            let mut e4 = game.play_mut(e4).unwrap();
732            e4.meta.nags.push(Nag::Symbol("!".to_string()));
733            e4.options_mut().push(crate::Move::normal(Pawn, E7, E5)).unwrap();
734            let c5 = e4.options_mut().push(crate::Move::normal(Pawn, C7, C5)).unwrap().slot();
735            game.play_mut(c5).unwrap().meta.comment = Some(text("Sicilian"));
736        }
737
738        let pgn = Game::from(game);
739        assert_eq!(
740            pgn.to_string(),
741            r#"[Event "x"]
742[Result "*"]
743
7441. e4! (1. d4) 1... e5 (1... c5 {Sicilian}) *"#
745        );
746    }
747
748    #[test]
749    fn converts_from_freestyle_game() {
750        let game = crate::Game::freestyle(Position::freestyle(Scharnagl::CHESS));
751
752        let pgn = Game::from(game);
753
754        assert!(pgn.to_string().contains("[Variant \"Chess960\"]"), "{}", pgn);
755        assert!(pgn.to_string().contains("[SetUp \"1\"]"), "{}", pgn);
756        assert!(pgn.to_string().contains("[Chess960Id \"518\"]"), "{}", pgn);
757        assert!(
758            pgn.to_string()
759                .contains("[FEN \"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w HAha - 0 1\"]"),
760            "{}",
761            pgn
762        );
763    }
764
765    #[test]
766    fn roundtrips_kiwipete_game_tree() {
767        let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
768        let position = Position::from_fen(fen).unwrap();
769        let mut game = crate::Game::chess(position).unwrap();
770
771        for play in position.legal_moves() {
772            let id = game.start_options_mut().push(play).unwrap().slot();
773            let replies = game.play(id).unwrap().legal().to_vec();
774            let mut play = game.play_mut(id).unwrap();
775            for reply in replies {
776                play.options_mut().push(reply).unwrap();
777            }
778        }
779
780        let pgn = Game::from(game);
781        let game = crate::Game::from_pgn(pgn.clone()).unwrap();
782        let roundtrip = Game::from(game);
783
784        assert_eq!(roundtrip, pgn);
785    }
786
787    #[test]
788    fn parses_annotations_between_variations() {
789        let pgn = r#"[Event "x"]
790
7911. e4 e5 {A} (1... c5) {B} (1... e6) {C} 2. Nf3 *
792"#;
793
794        let game = game.parse(pgn).unwrap();
795        let e5 = &game.moves[1];
796        assert_eq!(e5.comment, Some(comment("A")));
797        assert_eq!(e5.variations.len(), 2);
798        assert_eq!(e5.variations[0].outro, Some(comment("B")));
799        assert_eq!(e5.variations[1].outro, Some(comment("C")));
800        assert_eq!(
801            game.to_string(),
802            r#"[Event "x"]
803
8041. e4 e5 {A} (1... c5) {B} (1... e6) {C} 2. Nf3 *"#
805        );
806    }
807
808    #[test]
809    fn extracts_commands_from_move_comments() {
810        let game =
811            game.parse(r#"[Event "x"] 1. e4 {[%cal Ge2e4] [%clk 0:14:49] Good move.} *"#).unwrap();
812        let e4 = &game.moves[0];
813        assert_eq!(
814            e4.annotations,
815            vec![
816                Annotation::Command(Command {
817                    command: text("cal"),
818                    parameters: vec!["Ge2e4".to_string()],
819                }),
820                Annotation::Command(Command {
821                    command: text("clk"),
822                    parameters: vec!["0:14:49".to_string()],
823                }),
824            ]
825        );
826        assert_eq!(e4.comment, Some(comment("Good move.")));
827        assert_eq!(
828            game.to_string(),
829            r#"[Event "x"]
830
8311. e4 {[%cal Ge2e4] [%clk 0:14:49] Good move.} *"#
832        );
833    }
834
835    #[test]
836    fn extracts_multi_parameter_commands() {
837        let game = game
838            .parse(r#"[Event "x"] 1. e4 {[%tqu "En","find the move","","","e2e4","",10]} *"#)
839            .unwrap();
840        let e4 = &game.moves[0];
841        assert_eq!(
842            e4.annotations,
843            vec![Annotation::Command(Command {
844                command: text("tqu"),
845                parameters: vec![
846                    r#""En""#.to_string(),
847                    r#""find the move""#.to_string(),
848                    r#""""#.to_string(),
849                    r#""""#.to_string(),
850                    r#""e2e4""#.to_string(),
851                    r#""""#.to_string(),
852                    "10".to_string(),
853                ],
854            })]
855        );
856        assert_eq!(
857            game.to_string(),
858            r#"[Event "x"]
859
8601. e4 {[%tqu "En","find the move","","","e2e4","",10]} *"#
861        );
862    }
863
864    #[test]
865    fn extracts_commands_with_trailing_whitespace() {
866        let game = game.parse(r#"[Event "x"] 1. e4 {[%foo ]} *"#).unwrap();
867        assert_eq!(
868            game.moves[0].annotations,
869            vec![Annotation::Command(Command { command: text("foo"), parameters: vec![] })]
870        );
871        assert_eq!(
872            game.to_string(),
873            r#"[Event "x"]
874
8751. e4 {[%foo]} *"#
876        );
877    }
878
879    #[test]
880    fn ignores_empty_comments() {
881        let game = game.parse(r#"[Event "x"] 1. e4 {} {   } e5 *"#).unwrap();
882        assert_eq!(game.moves[0].comment, None);
883        assert_eq!(game.moves[1].comment, None);
884    }
885
886    #[test]
887    fn parses_san_in_moves() {
888        let game = game.parse(r#"[Event "x"] 1. exd8=Q# *"#).unwrap();
889        assert_eq!(
890            game.moves[0].san,
891            san::San {
892                play: san::Move::Normal {
893                    role: Pawn,
894                    file: Some(E),
895                    rank: None,
896                    capture: true,
897                    to: D8,
898                    promotion: Some(Queen),
899                },
900                check: Some(san::Check::Checkmate),
901            }
902        );
903    }
904
905    #[test]
906    fn displays_game() {
907        let game = game.parse(r#"[Event "x"] 1. e4! e5 $1 (1... c5 {Sicilian}) 2. Nf3 *"#).unwrap();
908        assert_eq!(
909            game.to_string(),
910            "[Event \"x\"]\n\n1. e4! 1... e5 $1 (1... c5 {Sicilian}) 2. Nf3 *"
911        );
912    }
913
914    #[test]
915    fn reads_games_one_at_a_time() {
916        let input = b"[Event \"a\"]\n1. e4 *\n\n[Event \"b\"]\n1. d4 *\n";
917        let games =
918            stream::games(&input[..]).map(|game| game.unwrap().unwrap()).collect::<Vec<_>>();
919        assert_eq!(games.len(), 2);
920        assert_eq!(games[0].tags[0], Tag::Event("a".to_string()));
921        assert_eq!(games[1].tags[0], Tag::Event("b".to_string()));
922    }
923
924    #[test]
925    fn reader_does_not_split_inside_comment() {
926        let input = b"[Event \"a\"]\n1. e4 {\n[not a tag]\n} *\n\n[Event \"b\"]\n1. d4 *\n";
927        let games =
928            stream::games(&input[..]).map(|game| game.unwrap().unwrap()).collect::<Vec<_>>();
929        assert_eq!(games.len(), 2);
930        assert_eq!(games[0].moves[0].comment, Some(comment("[not a tag]")));
931    }
932
933    #[test]
934    fn reader_handles_movetext_after_tag_on_same_line() {
935        let input = b"[Event \"a\"] 1. e4 *\n\n[Event \"b\"] 1. d4 *\n";
936        let games =
937            stream::games(&input[..]).map(|game| game.unwrap().unwrap()).collect::<Vec<_>>();
938        assert_eq!(games.len(), 2);
939        assert_eq!(games[0].tags[0], Tag::Event("a".to_string()));
940        assert_eq!(games[1].tags[0], Tag::Event("b".to_string()));
941    }
942
943    #[test]
944    fn parses_line_wrapped_black_move_without_number() {
945        game.parse("[Event \"x\"]\n\n14. f3 b6\n15. Be2 *").unwrap();
946    }
947}