1use core::{fmt, str};
4
5use winnow::Parser as _;
6
7use crate::{
8 Player, Position, Scharnagl,
9 game::{Command, Mode, Nag, Outcome, Tag as OtherTag},
10 position::{self, Parts},
11};
12
13use super::{Text, san};
14
15pub mod convert;
16pub mod parse;
17pub mod stream;
18
19pub use parse::pgn;
20
21use Mode::*;
22
23#[derive(Clone, Debug, DeserializeFromStr, PartialEq, Eq, SerializeDisplay)]
36pub struct Pgn {
37 pub tags: Vec<Tag>,
38 pub start: Parts,
39 pub intro: Option<Comment>,
40 pub moves: Vec<Move>,
41 pub outcome: Outcome,
42}
43
44#[derive(Clone, Debug, Default, Eq, PartialEq)]
46pub struct Import {
47 pub eval: import::Eval,
48}
49
50pub mod import {
51 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
52 pub enum Eval {
53 Drop,
54 #[default]
55 Passthrough,
56 }
57}
58
59#[derive(Clone, Debug, Default, Eq, PartialEq)]
61pub struct Export {
62 pub eval: export::Eval,
63}
64
65pub mod export {
66 #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
67 pub enum Eval {
68 #[default]
69 Passthrough,
70 Overwrite,
71 }
72}
73
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub enum Tag {
76 Title(String),
77 Event(String),
78 Site(String),
79 Date(String),
80 Round(String),
81 White(String),
82 Black(String),
83 Outcome(Outcome),
84 Fen(Parts),
85 SetUp(bool),
86 Variant(String),
87 Chess960Id(Scharnagl),
88 Orientation(Player),
89 Valuation(Eval),
90 Other(OtherTag),
91}
92
93impl Tag {
94 pub fn freestyle() -> Self {
95 Tag::Variant("Fischerandom".to_string())
96 }
97
98 pub fn variant(&self) -> Option<&str> {
99 if let Tag::Variant(value) = self { Some(value) } else { None }
100 }
101}
102
103#[derive(Debug, thiserror::Error)]
104pub enum Error {
105 #[error(transparent)]
106 Parse(#[from] parse::Error),
107 #[error(transparent)]
108 Convert(#[from] convert::Error),
109}
110
111pub type Result<T, E = Error> = core::result::Result<T, E>;
112
113impl fmt::Display for Pgn {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 let shredder = self.mode().is_freestyle();
116 for tag in &self.tags {
117 if let Tag::Fen(position) = tag {
118 let fen = if shredder { position.shredder_fen() } else { position.fen() };
119 write_tag(f, "FEN", &fen)?;
120 writeln!(f)?;
121 } else {
122 writeln!(f, "{tag}")?;
123 }
124 }
125 if !self.tags.is_empty() {
126 writeln!(f)?;
127 }
128 let mut wrap = Wrap::<_, 80>::new(f);
129 if let Some(intro) = &self.intro {
130 wrap.token(intro)?;
131 }
132 write_plays(&self.moves, &mut wrap, self.start.ply(), Notation::San)?;
133 wrap.token(self.outcome)
134 }
135}
136
137impl str::FromStr for Pgn {
138 type Err = Error;
139
140 fn from_str(text: &str) -> Result<Self> {
141 parse::pgn.parse(text).map_err(|error| parse::Error::from(text, 1, error).into())
142 }
143}
144
145#[derive(Clone, Debug, PartialEq, Eq)]
146pub struct Variation {
147 pub intro: Option<Comment>,
148 pub moves: Vec<Move>,
149 pub outro: Option<Comment>,
150}
151
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct Move {
154 pub san: san::San,
155 pub comment: Option<Comment>,
156 pub annotations: Vec<Annotation>,
157 pub variations: Vec<Variation>,
158}
159
160#[derive(Clone, Debug, PartialEq, Eq)]
161pub struct Comment(Text);
162
163#[derive(Clone, Copy, Debug, Eq, PartialEq)]
164pub enum Score {
165 Centipawns(i32),
166 Mate(i32),
167}
168
169#[derive(Clone, Copy, Debug, Eq, PartialEq)]
170pub struct Eval {
171 pub depth: Option<u16>,
172 pub score: Score,
173}
174
175#[derive(Clone, Debug, PartialEq, Eq)]
176pub enum Annotation {
177 Nag(Nag),
178 Valuation(Eval),
179 Command(Command),
180}
181
182impl fmt::Display for Eval {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 match self.score {
185 Score::Centipawns(centipawns) => {
186 write!(f, "{:.2}", f64::from(centipawns) / 100.0)?;
187 }
188 Score::Mate(mate) => write!(f, "#{mate}")?,
189 }
190 if let Some(depth) = self.depth {
191 write!(f, ",{depth}")?;
192 }
193 Ok(())
194 }
195}
196
197impl Eval {
198 fn command(self) -> Command {
199 Command {
200 command: Text::new("eval").expect("command name is non-empty"),
201 parameters: vec![self.to_string()],
202 }
203 }
204}
205
206impl str::FromStr for Eval {
207 type Err = &'static str;
208
209 fn from_str(value: &str) -> Result<Self, Self::Err> {
210 let (score, depth) = match value.trim().split_once(',') {
211 Some((score, depth)) => {
212 let depth = depth.trim().parse().map_err(|_| "invalid evaluation depth")?;
213 (score, Some(depth))
214 }
215 None => (value, None),
216 };
217 let score = score.trim();
218 let score = if let Some(mate) = score.strip_prefix('#') {
219 Score::Mate(mate.parse().map_err(|_| "invalid mate score")?)
220 } else {
221 let pawns: f64 = score.parse().map_err(|_| "invalid centipawn score")?;
222 let centipawns = (pawns * 100.0).round();
223 if !centipawns.is_finite()
224 || centipawns < f64::from(i32::MIN)
225 || centipawns > f64::from(i32::MAX)
226 {
227 return Err("centipawn score out of range");
228 }
229 Score::Centipawns(centipawns as i32)
230 };
231 Ok(Self { score, depth })
232 }
233}
234
235impl From<position::Score> for Score {
236 fn from(score: position::Score) -> Self {
237 use position::Score::*;
238
239 match score {
240 Centipawns(value) => Score::Centipawns(value),
241 Mate(value) => Score::Mate(value.get()),
242 }
243 }
244}
245
246impl From<&position::Evaluation> for Option<Eval> {
247 fn from(evaluation: &position::Evaluation) -> Self {
248 let position::Evaluation::Lines(lines) = evaluation else { return None };
249 Some(Eval { depth: Some(lines.depth), score: lines.lines.first().score.into() })
250 }
251}
252
253impl Pgn {
254 pub fn movetext(&self) -> String {
255 self.write_movetext(Notation::San)
256 }
257
258 pub fn figurine_movetext(&self) -> String {
259 self.write_movetext(Notation::Figurine)
260 }
261
262 fn write_movetext(&self, notation: Notation) -> String {
263 let mut movetext = String::new();
264 let mut wrap = Wrap::<_, 0>::new(&mut movetext);
265 if let Some(intro) = &self.intro {
266 wrap.token(intro).expect("writing PGN movetext to string");
267 }
268 write_plays(&self.moves, &mut wrap, self.start.ply(), notation)
269 .expect("writing PGN movetext to string");
270 movetext
271 }
272
273 pub fn mode(&self) -> Mode {
274 for tag in &self.tags {
275 let Some(variant) = tag.variant() else { continue };
276 if Mode::from_tag(variant) == Some(Freestyle) {
277 return Freestyle;
278 }
279 }
280
281 if self.start.castles.chess_compatible() { Chess } else { Freestyle }
282 }
283}
284
285impl fmt::Display for Tag {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 match self {
288 Tag::Title(value) => write_tag(f, "Title", value),
289 Tag::Event(value) => write_tag(f, "Event", value),
290 Tag::Site(value) => write_tag(f, "Site", value),
291 Tag::Date(value) => write_tag(f, "Date", value),
292 Tag::Round(value) => write_tag(f, "Round", value),
293 Tag::White(value) => write_tag(f, "White", value),
294 Tag::Black(value) => write_tag(f, "Black", value),
295 Tag::Outcome(outcome) => write_tag(f, "Result", &outcome.to_string()),
296 Tag::Fen(position) => write_tag(f, "FEN", &position.fen()),
297 Tag::SetUp(setup) => write_tag(f, "SetUp", if *setup { "1" } else { "0" }),
298 Tag::Variant(variant) => write_tag(f, "Variant", variant),
299 Tag::Chess960Id(id) => write_tag(f, "Chess960Id", &id.to_string()),
300 Tag::Orientation(player) => write_tag(f, "Orientation", player.name()),
301 Tag::Valuation(valuation) => write_tag(f, "Valuation", &valuation.to_string()),
302 Tag::Other(tag) => write_tag(f, tag.key.as_ref(), &tag.value),
303 }
304 }
305}
306
307impl Mode {
308 fn from_tag(value: &str) -> Option<Self> {
309 match value.to_ascii_lowercase().as_str() {
310 "chess" | "standard" => Some(Chess),
311 "chess960" | "fischerandom" | "fischer random" | "freestyle" => Some(Freestyle),
312 _ => None,
313 }
314 }
315}
316
317fn write_tag(f: &mut fmt::Formatter<'_>, key: &str, value: &str) -> fmt::Result {
318 fn escape_tag_value(value: &str) -> String {
319 value.replace('\\', "\\\\").replace('"', "\\\"")
320 }
321
322 write!(f, "[{} \"{}\"]", key, escape_tag_value(value))
323}
324
325fn start_position(tags: &[Tag]) -> Parts {
326 tags.iter()
327 .rev()
328 .find_map(|tag| match tag {
329 Tag::Fen(position) => Some(*position),
330 _ => None,
331 })
332 .unwrap_or_else(|| Position::start().parts())
333}
334
335impl fmt::Display for Annotation {
336 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
337 match self {
338 Annotation::Nag(nag) => write!(f, "{nag}"),
339 Annotation::Valuation(valuation) => valuation.command().fmt(f),
340 Annotation::Command(command) => write!(f, "{command}"),
341 }
342 }
343}
344
345impl fmt::Display for Command {
346 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347 write!(f, "[%{}", self.command)?;
348 for (index, parameter) in self.parameters.iter().enumerate() {
349 if index == 0 {
350 write!(f, " ")?;
351 } else {
352 write!(f, ",")?;
353 }
354 write!(f, "{parameter}")?;
355 }
356 write!(f, "]")
357 }
358}
359
360impl fmt::Display for Nag {
361 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
362 match self {
363 Nag::Numeric(nag) => write!(f, "${nag}"),
364 Nag::Symbol(nag) => write!(f, "{nag}"),
365 }
366 }
367}
368
369impl fmt::Display for Outcome {
370 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
371 match self {
372 Outcome::White => write!(f, "1-0"),
373 Outcome::Black => write!(f, "0-1"),
374 Outcome::Draw => write!(f, "1/2-1/2"),
375 Outcome::Unknown => write!(f, "*"),
376 }
377 }
378}
379
380fn write_plays<W: fmt::Write + ?Sized, const WIDTH: usize>(
381 plays: &[Move],
382 wrap: &mut Wrap<'_, W, WIDTH>,
383 first_ply: usize,
384 notation: Notation,
385) -> fmt::Result {
386 for (ply, (index, play)) in (first_ply..).zip(plays.iter().enumerate()) {
387 if should_write_move_number(ply, index, plays) {
388 wrap.token(MoveNumber(ply))?;
389 }
390 match notation {
391 Notation::San => wrap.token(play.san)?,
392 Notation::Figurine => wrap.token(play.san.figurine())?,
393 }
394 play.write_tail(wrap, ply, notation)?;
395 }
396 Ok(())
397}
398
399impl Move {
400 fn write_tail<W: fmt::Write + ?Sized, const WIDTH: usize>(
401 &self,
402 wrap: &mut Wrap<'_, W, WIDTH>,
403 ply: usize,
404 notation: Notation,
405 ) -> fmt::Result {
406 let mut commands = Vec::new();
407 for annotation in &self.annotations {
408 match annotation {
409 Annotation::Nag(Nag::Symbol(_)) => wrap.suffix(annotation)?,
410 Annotation::Nag(Nag::Numeric(_)) => wrap.token(annotation)?,
411 Annotation::Valuation(valuation) => commands.push(valuation.command()),
412 Annotation::Command(command) => commands.push(command.clone()),
413 }
414 }
415 if !commands.is_empty() || self.comment.is_some() {
416 wrap.token(MoveComment { commands, text: self.comment.clone() })?;
417 }
418 write_variations(&self.variations, wrap, ply, notation)?;
419 Ok(())
420 }
421}
422
423fn write_variations<W: fmt::Write + ?Sized, const WIDTH: usize>(
424 variations: &[Variation],
425 wrap: &mut Wrap<'_, W, WIDTH>,
426 ply: usize,
427 notation: Notation,
428) -> fmt::Result {
429 for variation in variations {
430 wrap.open("(")?;
431 if let Some(intro) = &variation.intro {
432 wrap.token(intro)?;
433 }
434 write_plays(&variation.moves, wrap, ply, notation)?;
435 wrap.close(")")?;
436 if let Some(outro) = &variation.outro {
437 wrap.token(outro)?;
438 }
439 }
440 Ok(())
441}
442
443#[derive(Clone, Copy)]
444enum Notation {
445 San,
446 Figurine,
447}
448
449#[derive(Debug)]
450struct MoveNumber(usize);
451
452struct MoveComment {
453 commands: Vec<Command>,
454 text: Option<Comment>,
455}
456
457impl Comment {
458 fn merge(&mut self, comment: &Comment) {
459 self.0.merge(&comment.0);
460 }
461}
462
463impl From<Comment> for Text {
464 fn from(comment: Comment) -> Self {
465 comment.0
466 }
467}
468
469impl fmt::Display for Comment {
470 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471 write!(f, "{{{}}}", self.0)
472 }
473}
474
475impl fmt::Display for MoveComment {
476 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477 write!(f, "{{")?;
478 for (index, command) in self.commands.iter().enumerate() {
479 if index > 0 {
480 write!(f, " ")?;
481 }
482 write!(f, "{command}")?;
483 }
484 if let Some(text) = &self.text {
485 if !self.commands.is_empty() {
486 write!(f, " ")?;
487 }
488 write!(f, "{}", text.0)?;
489 }
490 write!(f, "}}")
491 }
492}
493
494impl fmt::Display for MoveNumber {
495 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496 let ply = self.0;
497 let number = ply / 2 + 1;
498 if ply.is_multiple_of(2) { write!(f, "{number}.") } else { write!(f, "{number}...") }
499 }
500}
501
502struct Wrap<'a, W: fmt::Write + ?Sized, const WIDTH: usize = 80> {
503 f: &'a mut W,
504 line: usize,
505 glued: bool,
506}
507
508impl<'a, W: fmt::Write + ?Sized, const WIDTH: usize> Wrap<'a, W, WIDTH> {
509 fn new(f: &'a mut W) -> Self {
510 Self { f, line: 0, glued: false }
511 }
512
513 fn token(&mut self, token: impl fmt::Display) -> fmt::Result {
514 let token = token.to_string();
515 let separator = usize::from(self.line > 0 && !self.glued);
516 if WIDTH > 0 && self.line > 0 && self.line + separator + token.len() > WIDTH {
517 writeln!(self.f)?;
518 self.line = 0;
519 self.glued = false;
520 } else if self.line > 0 && !self.glued {
521 write!(self.f, " ")?;
522 self.line += 1;
523 }
524 write!(self.f, "{token}")?;
525 self.line += token.len();
526 self.glued = false;
527 Ok(())
528 }
529
530 fn suffix(&mut self, token: impl fmt::Display) -> fmt::Result {
531 let token = token.to_string();
532 write!(self.f, "{token}")?;
533 self.line += token.len();
534 self.glued = false;
535 Ok(())
536 }
537
538 fn open(&mut self, token: &str) -> fmt::Result {
539 self.token(token)?;
540 self.glued = true;
541 Ok(())
542 }
543
544 fn close(&mut self, token: &str) -> fmt::Result {
545 write!(self.f, "{token}")?;
546 self.line += token.len();
547 self.glued = false;
548 Ok(())
549 }
550}
551
552fn should_write_move_number(ply: usize, index: usize, plays: &[Move]) -> bool {
553 ply.is_multiple_of(2)
554 || index == 0
555 || plays[index - 1].has_intervening_annotation_or_variation()
556}
557
558impl Move {
559 fn has_intervening_annotation_or_variation(&self) -> bool {
560 !self.annotations.is_empty() || self.comment.is_some() || !self.variations.is_empty()
561 }
562}
563
564#[cfg(test)]
565mod tests {
566 use crate::{
567 Position,
568 board::{Player, Role::*, Scharnagl},
569 formats::san,
570 position::{Check, Terminal},
571 square::{File::*, Square::*},
572 };
573
574 use super::*;
575
576 fn text(text: &str) -> Text {
577 Text::new(text).unwrap()
578 }
579
580 fn comment(comment: &str) -> Comment {
581 Comment(text(comment))
582 }
583
584 #[test]
585 fn parse_intro_placeholder() {
586 let pgn: Pgn = "1. -- {Introduction} *".parse().unwrap();
587 assert!(pgn.moves.is_empty());
588 assert_eq!(pgn.intro, Some(comment("Introduction")));
589 let game = crate::Game::from_pgn(pgn.clone()).unwrap();
590 assert_eq!(game.start().comment(), Some(&text("Introduction")));
591 }
592
593 #[test]
594 fn parse_pgn() {
595 let text = r#"
596 [Event "Casual Game"]
597 [Site "Berlin GER"]
598 [Date "1852.??.??"]
599 [Round "?"]
600 [White "Adolf Anderssen"]
601 [Black "Jean Dufresne"]
602 [Result "1-0"]
603
604 1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 {Italian Game} 1-0
605 "#;
606
607 let pgn = parse::pgn.parse(text).unwrap();
608 assert_eq!(pgn.tags.len(), 7);
609 assert_eq!(pgn.tags[0], Tag::Event("Casual Game".to_string()));
610 assert_eq!(pgn.moves.len(), 6);
611 assert_eq!(pgn.outcome, Outcome::White);
612 assert_eq!(pgn.moves[5].comment, Some(comment("Italian Game")));
613 }
614
615 #[test]
616 fn parse_pgn_without_termination_marker() {
617 let pgn = pgn.parse("1. e4 e5").unwrap();
618
619 assert_eq!(pgn.moves.len(), 2);
620 assert_eq!(pgn.outcome, Outcome::Unknown);
621 }
622
623 #[test]
624 fn parse_variation_and_nags() {
625 let text = r#"[Event "x"]
626
6271. e4! e5 $1 (1... c5 {Sicilian}) 2. Nf3 *
628"#;
629
630 let pgn = pgn.parse(text).unwrap();
631 let e4 = &pgn.moves[0];
632 assert_eq!(
633 e4.san.play,
634 san::Move::Normal {
635 role: Pawn,
636 file: None,
637 rank: None,
638 capture: false,
639 to: E4,
640 promotion: None,
641 }
642 );
643 assert_eq!(e4.annotations, vec![Annotation::Nag(Nag::Symbol("!".to_string()))]);
644
645 let e5 = &pgn.moves[1];
646 assert_eq!(e5.annotations, vec![Annotation::Nag(Nag::Numeric(1))]);
647 assert_eq!(e5.variations.len(), 1);
648 assert_eq!(e5.variations[0].moves[0].comment, Some(comment("Sicilian")));
649 assert_eq!(pgn.outcome, Outcome::Unknown);
650 }
651
652 #[test]
653 fn convert_to_game() {
654 let pgn = pgn
655 .parse(
656 r#"[Event "x"]
657
6581. e4! e5 $1 (1... c5 {Sicilian}) 2. Nf3 *
659"#,
660 )
661 .unwrap();
662 let game = crate::Game::from_pgn(pgn).unwrap();
663
664 assert_eq!(game.roster().event, Some(text("x")));
665 assert_eq!(game.tags().len(), 0);
666 assert_eq!(game.start().options().len(), 1);
667
668 let e4 = game.start().main().unwrap();
669 assert_eq!(e4.play().to, E4);
670 assert_eq!(e4.nags(), &[Nag::Symbol("!".to_string())]);
671 assert_eq!(e4.position().options().len(), 2);
672
673 let e5 = e4.position().main().unwrap();
674 assert_eq!(e5.play().to, E5);
675 assert_eq!(e5.nags(), &[Nag::Numeric(1)]);
676
677 let c5 = e4.position().alternatives().next().unwrap();
678 assert_eq!(c5.play().to, C5);
679 assert_eq!(c5.comment(), Some(&text("Sicilian")));
680 }
681
682 #[test]
683 fn title_roundtrip() {
684 let pgn: Pgn = "[Title \"Sveshnikov \\\"study\\\"\"]\n\n*".parse().unwrap();
685 let mut game = crate::Game::from_pgn(pgn).unwrap();
686 assert_eq!(game.title(), Some(&text("Sveshnikov \"study\"")));
687 assert!(game.tags().is_empty());
688
689 game.set_title(Some(text("New title")));
690 let exported = Pgn::from(game);
691 assert!(exported.tags.contains(&Tag::Title("New title".to_string())));
692 assert_eq!(crate::Game::from_pgn(exported).unwrap().title(), Some(&text("New title")));
693 }
694
695 #[test]
696 fn convert_orientation_and_commands() {
697 let pgn = pgn
698 .parse(
699 r#"[Orientation "BLACK"]
700
7011. e4 {[%foo bar][%cgexp true][%eval 0.205,18]} (1. d4) 1... e5 {[%eval #-3,22]} *
702"#,
703 )
704 .unwrap();
705 let game = crate::Game::from_pgn(pgn).unwrap();
706
707 assert_eq!(game.orientation(), Player::Black);
708 let e4 = game.start().main().unwrap();
709 assert!(e4.expanded());
710 assert_eq!(e4.evaluation(), None);
711 assert_eq!(e4.commands().len(), 2);
712 assert_eq!(e4.commands()[0].command.as_ref(), "foo");
713 let e5 = e4.position().main().unwrap();
714 assert_eq!(e5.evaluation(), None);
715
716 let pgn = Pgn::from(game).to_string();
717 assert!(pgn.contains("[Orientation \"black\"]"), "{pgn}");
718 let eval = pgn.find("[%eval 0.21,18]").unwrap();
719 let expanded = pgn.find("[%cgexp true]").unwrap();
720 let command = pgn.find("[%foo bar]").unwrap();
721 assert!(expanded < command && command < eval, "{pgn}");
722 assert!(pgn.contains("[%eval #-3,22]"), "{pgn}");
723 }
724
725 #[test]
726 fn eval_policies() {
727 let pgn: Pgn = r#"[Valuation "0.10,8"]
728
7291. e4 {[%eval 0.30,12][%foo bar]} (1. d4 {[%eval -0.20,10]}) *"#
730 .parse()
731 .unwrap();
732 let game = crate::Game::from_pgn(pgn.clone()).unwrap();
733 let included = Pgn::from(game.clone());
734 let dropped =
735 crate::Game::from_pgn_with(pgn.clone(), &Import { eval: import::Eval::Drop }).unwrap();
736 let omitted = Pgn::from(dropped);
737 let text = omitted.to_string();
738
739 assert!(included.to_string().contains("[Valuation \"0.10,8\"]"));
740 assert!(included.to_string().contains("[%eval 0.30,12]"));
741 assert!(!text.contains("[Valuation "), "{text}");
742 assert!(!text.contains("%eval"), "{text}");
743 assert!(text.contains("[%foo bar]"), "{text}");
744 assert!(text.contains("d4"), "{text}");
745 assert_eq!(Pgn::from(game), included);
746
747 let pgn: Pgn = "1. e4 {[%eval #0]} *".parse().unwrap();
748 let imported = crate::Game::from_pgn(pgn.clone()).unwrap();
749 assert!(imported.start().main().unwrap().evaluation().is_none());
750 assert_eq!(imported.start().main().unwrap().commands().len(), 1);
751 assert!(Pgn::from(imported).to_string().contains("[%eval #0]"));
752
753 let dropped =
754 crate::Game::from_pgn_with(pgn, &Import { eval: import::Eval::Drop }).unwrap();
755 assert!(dropped.start().main().unwrap().evaluation().is_none());
756 assert!(dropped.start().main().unwrap().commands().is_empty());
757
758 let malformed: Pgn = "1. e4 {[%eval unknown][%foo bar]} *".parse().unwrap();
759 let dropped =
760 crate::Game::from_pgn_with(malformed, &Import { eval: import::Eval::Drop }).unwrap();
761 let play = dropped.start().main().unwrap();
762 let commands = play.commands();
763 assert_eq!(commands.len(), 1);
764 assert_eq!(commands[0].command.as_ref(), "foo");
765
766 let evaluation: Option<Eval> =
767 (&position::Evaluation::Terminal(Terminal::Stalemate)).into();
768 assert_eq!(evaluation, None);
769 }
770
771 #[test]
772 fn convert_fen_game() {
773 let pgn = pgn
774 .parse(
775 r#"[FEN "4k3/8/8/8/8/8/4P3/4K3 w - - 0 1"]
776
7771. e4 *
778"#,
779 )
780 .unwrap();
781 let game = crate::Game::from_pgn(pgn).unwrap();
782
783 let e4 = game.start().main().unwrap();
784 assert_eq!(e4.play().from, E2);
785 assert_eq!(e4.play().to, E4);
786 }
787
788 #[test]
789 fn use_last_fen_tag_for_start() {
790 let pgn = pgn
791 .parse(
792 r#"[FEN "4k3/8/8/8/8/8/8/4K3 w - - 0 1"]
793[FEN "4k3/8/8/8/8/8/4P3/4K3 w - - 0 1"]
794
7951. e4 *
796"#,
797 )
798 .unwrap();
799
800 assert_eq!(pgn.start.fen(), "4k3/8/8/8/8/8/4P3/4K3 w - - 0 1");
801 }
802
803 #[test]
804 fn parse_variant_tag() {
805 let text = r#"
806 [Variant "Fischer Random"]
807 *
808 "#;
809 let pgn = pgn.parse(text).unwrap();
810
811 assert_eq!(pgn.tags, vec![Tag::Variant("Fischer Random".to_string())]);
812 assert_eq!(pgn.mode(), Freestyle);
813 assert!(pgn.to_string().contains("[Variant \"Fischer Random\"]"));
814 }
815
816 #[test]
817 fn prefer_freestyle_variant() {
818 let text = r#"
819 [Variant "Chess960"]
820 [Variant "Standard"]
821 *
822 "#;
823 let pgn = pgn.parse(text).unwrap();
824
825 assert_eq!(pgn.mode(), Freestyle);
826 }
827
828 #[test]
829 fn prefer_freestyle_over_unsupported_variant() {
830 let text = r#"
831 [Variant "Antichess"]
832 [Variant "Chess960"]
833 *
834 "#;
835 let pgn = pgn.parse(text).unwrap();
836
837 assert_eq!(pgn.mode(), Freestyle);
838
839 let text = r#"
840 [Variant "Chess960"]
841 [Variant "Antichess"]
842 *
843 "#;
844 let pgn = parse::pgn.parse(text).unwrap();
845
846 assert_eq!(pgn.mode(), Freestyle);
847 }
848
849 #[test]
850 fn ignore_unsupported_variant_tags() {
851 let text = r#"
852 [Variant "Antichess"]
853 *
854 "#;
855 let pgn = pgn.parse(text).unwrap();
856
857 assert_eq!(pgn.mode(), Chess);
858 }
859
860 #[test]
861 fn fall_back_to_position_castling() {
862 let fen = r#"
863 [FEN "8/8/8/8/8/8/8/8 w - - 0 1"]
864 *
865 "#;
866 let pgn = parse::pgn.parse(fen).unwrap();
867
868 assert_eq!(pgn.mode(), Chess);
869
870 let fen = r#"
871 [Variant "Standard"]
872 [FEN "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf - 2 9"]
873 *
874 "#;
875 let pgn = parse::pgn.parse(fen).unwrap();
876
877 assert_eq!(pgn.mode(), Freestyle);
878 assert!(pgn.to_string().contains(" w HFhf - 2 9\"]"));
879 }
880
881 #[test]
882 fn convert_to_game_mode() {
883 let pgn = pgn.parse(r#"[Event "x"] 1. e4 *"#).unwrap();
884 let chess = crate::Game::try_from(pgn).unwrap();
885 assert_eq!(chess.mode(), Chess);
886
887 let pgn = parse::pgn
888 .parse(
889 r#"[Variant "Chess960"]
890[FEN "bqnb1rkr/pp3ppp/3ppn2/2p5/5P2/P2P4/NPP1P1PP/BQ1BNRKR w HFhf - 2 9"]
8919. e4 *"#,
892 )
893 .unwrap();
894 let freestyle = crate::Game::try_from(pgn).unwrap();
895 assert_eq!(freestyle.mode(), Freestyle);
896 }
897
898 #[test]
899 fn convert_unsupported_variant_as_chess() {
900 let pgn = pgn.parse(r#"[Variant "Antichess"] *"#).unwrap();
901 let game = crate::Game::try_from(pgn).unwrap();
902
903 assert_eq!(game.mode(), Chess);
904 }
905
906 #[test]
907 fn reject_invalid_start() {
908 let pgn =
909 pgn.parse(r#"[Variant "Standard"] [FEN "4k3/8/8/8/8/8/8/4K2P w - - 0 1"] *"#).unwrap();
910 let error = crate::Game::try_from(pgn).err().unwrap();
911
912 assert!(matches!(error, convert::Error::Start { mode: Chess, .. }));
913 }
914
915 #[test]
916 fn display_fen_game_from_its_start_ply() {
917 let fen = "4k3/8/8/8/8/8/4P3/4K3 b - - 0 17";
918 let position = Position::from_fen(fen).unwrap();
919 let mut game = crate::Game::chess(position).unwrap();
920 game.start_mut().push(crate::Move::normal(King, E8, D8)).unwrap();
921
922 let pgn = Pgn::from(game);
923 assert!(pgn.to_string().contains("[SetUp \"1\"]"), "{}", pgn);
924 assert!(pgn.to_string().contains(&format!("[FEN \"{fen}\"]")), "{}", pgn);
925 assert!(pgn.to_string().contains("\n17... Kd8 *"), "{}", pgn);
926 assert_eq!(pgn.movetext(), "17... Kd8");
927 }
928
929 #[test]
930 fn display_figurine_movetext() {
931 let mut game = crate::Game::chess(Position::start()).unwrap();
932 let e4 = game.start_mut().into_push(crate::Move::normal(Pawn, E2, E4)).unwrap();
933 e4.into_position().push(crate::Move::normal(Knight, G8, F6)).unwrap();
934
935 let pgn = Pgn::from(game);
936 assert_eq!(pgn.movetext(), "1. e4 Nf6");
937 assert_eq!(pgn.figurine_movetext(), "1. e4 ♘f6");
938 }
939
940 #[test]
941 fn convert_from_game() {
942 let mut game = crate::Game::chess(Position::start()).unwrap();
943 game.update_roster(|roster| roster.event = Some(text("x")));
944
945 let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
946 game.start_mut().push(crate::Move::normal(Pawn, D2, D4)).unwrap();
947
948 let mut e4 = game.play_mut(e4).unwrap();
949 e4.update_nags(|nags| nags.push(Nag::Symbol("!".to_string())));
950 let mut position = e4.into_position();
951 position.push(crate::Move::normal(Pawn, E7, E5)).unwrap();
952 let mut c5 = position.push(crate::Move::normal(Pawn, C7, C5)).unwrap();
953 c5.set_comment(Some(text("Sicilian")));
954
955 let pgn = Pgn::from(game);
956 assert_eq!(
957 pgn.to_string(),
958 r#"[Event "x"]
959[Result "*"]
960[Orientation "white"]
961
9621. e4! (1. d4) 1... e5 (1... c5 {Sicilian}) *"#
963 );
964 }
965
966 #[test]
967 fn convert_from_freestyle_game() {
968 let game = crate::Game::freestyle(Position::freestyle(Scharnagl::CHESS));
969
970 let pgn = Pgn::from(game);
971
972 assert!(pgn.to_string().contains("[Variant \"Fischerandom\"]"), "{}", pgn);
973 assert!(pgn.to_string().contains("[SetUp \"1\"]"), "{}", pgn);
974 assert!(pgn.to_string().contains("[Chess960Id \"518\"]"), "{}", pgn);
975 assert!(
976 pgn.to_string()
977 .contains("[FEN \"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w HAha - 0 1\"]"),
978 "{}",
979 pgn
980 );
981 }
982
983 #[test]
984 fn roundtrip_kiwipete_game_tree() {
985 let fen = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1";
986 let position = Position::from_fen(fen).unwrap();
987 let mut game = crate::Game::chess(position).unwrap();
988
989 for play in position.legal_plays() {
990 let id = game.start_mut().push(play).unwrap().slot();
991 let replies = game.play(id).unwrap().position().legal().to_vec();
992 let mut position = game.play_mut(id).unwrap().into_position();
993 for reply in replies {
994 position.push(reply).unwrap();
995 }
996 }
997
998 let pgn = Pgn::from(game);
999 let game = crate::Game::from_pgn(pgn.clone()).unwrap();
1000 let roundtrip = Pgn::from(game);
1001
1002 assert_eq!(roundtrip, pgn);
1003 }
1004
1005 #[test]
1006 fn parse_annotations_between_variations() {
1007 let text = r#"[Event "x"]
1008
10091. e4 e5 {A} (1... c5) {B} (1... e6) {C} 2. Nf3 *
1010"#;
1011
1012 let pgn = pgn.parse(text).unwrap();
1013 let e5 = &pgn.moves[1];
1014 assert_eq!(e5.comment, Some(comment("A")));
1015 assert_eq!(e5.variations.len(), 2);
1016 assert_eq!(e5.variations[0].outro, Some(comment("B")));
1017 assert_eq!(e5.variations[1].outro, Some(comment("C")));
1018 assert_eq!(
1019 pgn.to_string(),
1020 r#"[Event "x"]
1021
10221. e4 e5 {A} (1... c5) {B} (1... e6) {C} 2. Nf3 *"#
1023 );
1024 }
1025
1026 #[test]
1027 fn extract_commands_from_move_comments() {
1028 let pgn =
1029 pgn.parse(r#"[Event "x"] 1. e4 {[%cal Ge2e4] [%clk 0:14:49] Good move.} *"#).unwrap();
1030 let e4 = &pgn.moves[0];
1031 assert_eq!(
1032 e4.annotations,
1033 vec![
1034 Annotation::Command(Command {
1035 command: text("cal"),
1036 parameters: vec!["Ge2e4".to_string()],
1037 }),
1038 Annotation::Command(Command {
1039 command: text("clk"),
1040 parameters: vec!["0:14:49".to_string()],
1041 }),
1042 ]
1043 );
1044 assert_eq!(e4.comment, Some(comment("Good move.")));
1045 assert_eq!(
1046 pgn.to_string(),
1047 r#"[Event "x"]
1048
10491. e4 {[%cal Ge2e4] [%clk 0:14:49] Good move.} *"#
1050 );
1051 }
1052
1053 #[test]
1054 fn extract_multi_parameter_commands() {
1055 let pgn = pgn
1056 .parse(r#"[Event "x"] 1. e4 {[%tqu "En","find the move","","","e2e4","",10]} *"#)
1057 .unwrap();
1058 let e4 = &pgn.moves[0];
1059 assert_eq!(
1060 e4.annotations,
1061 vec![Annotation::Command(Command {
1062 command: text("tqu"),
1063 parameters: vec![
1064 r#""En""#.to_string(),
1065 r#""find the move""#.to_string(),
1066 r#""""#.to_string(),
1067 r#""""#.to_string(),
1068 r#""e2e4""#.to_string(),
1069 r#""""#.to_string(),
1070 "10".to_string(),
1071 ],
1072 })]
1073 );
1074 assert_eq!(
1075 pgn.to_string(),
1076 r#"[Event "x"]
1077
10781. e4 {[%tqu "En","find the move","","","e2e4","",10]} *"#
1079 );
1080 }
1081
1082 #[test]
1083 fn extract_commands_with_trailing_whitespace() {
1084 let pgn = pgn.parse(r#"[Event "x"] 1. e4 {[%foo ]} *"#).unwrap();
1085 assert_eq!(
1086 pgn.moves[0].annotations,
1087 vec![Annotation::Command(Command { command: text("foo"), parameters: vec![] })]
1088 );
1089 assert_eq!(
1090 pgn.to_string(),
1091 r#"[Event "x"]
1092
10931. e4 {[%foo]} *"#
1094 );
1095 }
1096
1097 #[test]
1098 fn ignore_empty_comments() {
1099 let pgn = pgn.parse(r#"[Event "x"] 1. e4 {} { } e5 *"#).unwrap();
1100 assert_eq!(pgn.moves[0].comment, None);
1101 assert_eq!(pgn.moves[1].comment, None);
1102 }
1103
1104 #[test]
1105 fn parse_san_in_moves() {
1106 let pgn = pgn.parse(r#"[Event "x"] 1. exd8=Q# *"#).unwrap();
1107 assert_eq!(
1108 pgn.moves[0].san,
1109 san::San {
1110 play: san::Move::Normal {
1111 role: Pawn,
1112 file: Some(E),
1113 rank: None,
1114 capture: true,
1115 to: D8,
1116 promotion: Some(Queen),
1117 },
1118 check: Some(Check::Checkmate),
1119 }
1120 );
1121 }
1122
1123 #[test]
1124 fn display_game() {
1125 let pgn = pgn.parse(r#"[Event "x"] 1. e4! e5 $1 (1... c5 {Sicilian}) 2. Nf3 *"#).unwrap();
1126 assert_eq!(
1127 pgn.to_string(),
1128 "[Event \"x\"]\n\n1. e4! 1... e5 $1 (1... c5 {Sicilian}) 2. Nf3 *"
1129 );
1130 }
1131
1132 #[test]
1133 fn read_pgns_one_at_a_time() {
1134 let input = b"[Event \"a\"]\n1. e4 *\n\n[Event \"b\"]\n1. d4 *\n";
1135 let pgns = stream::pgns(&input[..]).map(|pgn| pgn.unwrap().unwrap()).collect::<Vec<_>>();
1136 assert_eq!(pgns.len(), 2);
1137 assert_eq!(pgns[0].tags[0], Tag::Event("a".to_string()));
1138 assert_eq!(pgns[1].tags[0], Tag::Event("b".to_string()));
1139 }
1140
1141 #[test]
1142 fn read_comment_without_splitting_pgn() {
1143 let input = b"[Event \"a\"]\n1. e4 {\n[not a tag]\n} *\n\n[Event \"b\"]\n1. d4 *\n";
1144 let games = stream::pgns(&input[..]).map(|pgn| pgn.unwrap().unwrap()).collect::<Vec<_>>();
1145 assert_eq!(games.len(), 2);
1146 assert_eq!(games[0].moves[0].comment, Some(comment("[not a tag]")));
1147 }
1148
1149 #[test]
1150 fn read_movetext_after_tag_on_same_line() {
1151 let input = b"[Event \"a\"] 1. e4 *\n\n[Event \"b\"] 1. d4 *\n";
1152 let games = stream::pgns(&input[..]).map(|pgn| pgn.unwrap().unwrap()).collect::<Vec<_>>();
1153 assert_eq!(games.len(), 2);
1154 assert_eq!(games[0].tags[0], Tag::Event("a".to_string()));
1155 assert_eq!(games[1].tags[0], Tag::Event("b".to_string()));
1156 }
1157
1158 #[test]
1159 fn parse_line_wrapped_black_move_without_number() {
1160 pgn.parse("[Event \"x\"]\n\n14. f3 b6\n15. Be2 *").unwrap();
1161 }
1162}