1use 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#[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}