Skip to main content

chess/formats/pgn/
convert.rs

1use crate::{
2    Player, Scharnagl,
3    game::{self, Mode, Roster},
4    position::{self, Position},
5};
6
7use super::*;
8
9#[derive(Debug, thiserror::Error)]
10pub enum Error {
11    #[error(transparent)]
12    Resolve(#[from] Resolve),
13    #[error("invalid PGN start position {fen}: {error}")]
14    Start { mode: Mode, fen: String, error: position::Error },
15}
16
17#[derive(Debug, thiserror::Error)]
18// Conceptually, resolving PGN movetext into a game tree has four expected
19// failure modes: invalid SAN, illegal SAN, ambiguous SAN, or a duplicate move.
20// The source-shaped variants below should be refined with the Game API.
21//
22// Additionally, our currently san.resolve calculates legal moves twice.
23pub enum Resolve {
24    #[error("SAN error: {0}")]
25    San(#[from] san::Error),
26    #[error("game options error: {0}")]
27    Options(#[from] game::options::Error),
28}
29
30pub type Result<T, E = Error> = core::result::Result<T, E>;
31
32impl From<crate::Game> for Pgn {
33    fn from(game: crate::Game) -> Self {
34        Self::from_game(&game, &Export::default())
35    }
36}
37
38impl Pgn {
39    pub fn from_game(game: &crate::Game, export: &Export) -> Self {
40        export.game(game)
41    }
42}
43
44impl TryFrom<Pgn> for crate::Game {
45    type Error = Error;
46
47    fn try_from(pgn: Pgn) -> Result<Self> {
48        Self::from_pgn_with(pgn, &Default::default())
49    }
50}
51
52impl crate::Game {
53    pub fn from_pgn(pgn: Pgn) -> Result<Self> {
54        pgn.try_into()
55    }
56
57    pub fn from_pgn_with(pgn: Pgn, import: &Import) -> Result<Self> {
58        import.game(pgn)
59    }
60}
61
62impl Import {
63    fn game(&self, pgn: Pgn) -> Result<crate::Game> {
64        let mode = pgn.mode();
65        let position = pgn.start.validate().map_err(|error| Error::Start {
66            mode,
67            fen: pgn.start.fen(),
68            error,
69        })?;
70        Ok(self.game_with_position(pgn, position, mode)?)
71    }
72
73    fn game_with_position(
74        &self,
75        pgn: Pgn,
76        position: Position,
77        mode: Mode,
78    ) -> Result<crate::Game, Resolve> {
79        let mut game = if mode.is_freestyle() {
80            crate::Game::freestyle(position)
81        } else {
82            crate::Game::new(position)
83        };
84        game.set_title(Self::title(&pgn.tags));
85        game.update_roster(|roster| *roster = Self::roster(&pgn.tags));
86        game.update_tags(|tags| *tags = self.tags(&pgn.tags));
87        game.set_outcome(pgn.outcome);
88        game.set_orientation(Self::orientation(&pgn.tags));
89
90        let mut start = game.start_mut();
91        start.set_comment(pgn.intro.map(Into::into));
92        self.plays(start, pgn.moves)?;
93
94        Ok(game)
95    }
96
97    fn plays<'g>(
98        &self,
99        mut position: game::PositionMut<'g>,
100        plays: impl IntoIterator<Item = Move>,
101    ) -> Result<(), Resolve> {
102        for pgn_play in plays {
103            position = self.play(position, pgn_play)?.into_position();
104        }
105
106        Ok(())
107    }
108
109    fn play<'g>(
110        &self,
111        position: game::PositionMut<'g>,
112        pgn_play: Move,
113    ) -> Result<game::MoveMut<'g>, Resolve> {
114        let play = pgn_play.san.resolve(position.legal())?;
115        let mut play = position.into_push(play)?;
116        play.set_comment(pgn_play.comment.map(Into::into));
117        self.annotations(&mut play, pgn_play.annotations);
118
119        for variation in pgn_play.variations {
120            self.variation(play.previous_mut(), variation)?;
121        }
122
123        Ok(play)
124    }
125
126    fn variation(
127        &self,
128        position: game::PositionMut<'_>,
129        variation: Variation,
130    ) -> Result<(), Resolve> {
131        let Variation { intro, moves: plays, outro } = variation;
132        let mut plays = plays.into_iter();
133        let Some(first) = plays.next() else {
134            return Ok(());
135        };
136
137        let mut play = self.play(position, first)?;
138        play.set_intro(intro.map(Into::into));
139        play.set_outro(outro.map(Into::into));
140        self.plays(play.into_position(), plays)
141    }
142
143    fn roster(tags: &[Tag]) -> Roster {
144        let mut roster = Roster::default();
145
146        for tag in tags {
147            match tag {
148                Tag::Title(_) => {}
149                Tag::Event(value) => roster.event = Self::roster_text(value, "?"),
150                Tag::Site(value) => roster.site = Self::roster_text(value, "?"),
151                Tag::Date(value) => roster.date = Self::roster_text(value, "????.??.??"),
152                Tag::Round(value) => roster.round = Self::roster_text(value, "?"),
153                Tag::White(value) => roster.white = Self::roster_text(value, "?"),
154                Tag::Black(value) => roster.black = Self::roster_text(value, "?"),
155                Tag::Outcome(_)
156                | Tag::Fen(_)
157                | Tag::SetUp(_)
158                | Tag::Variant(_)
159                | Tag::Chess960Id(_)
160                | Tag::Orientation(_)
161                | Tag::Valuation(_)
162                | Tag::Other(_) => {}
163            }
164        }
165
166        roster
167    }
168
169    fn title(tags: &[Tag]) -> Option<Text> {
170        tags.iter().rev().find_map(|tag| match tag {
171            Tag::Title(value) => Text::new(value).ok(),
172            _ => None,
173        })
174    }
175
176    fn orientation(tags: &[Tag]) -> Player {
177        tags.iter()
178            .rev()
179            .find_map(|tag| match tag {
180                Tag::Orientation(player) => Some(*player),
181                _ => None,
182            })
183            .unwrap_or(Player::White)
184    }
185
186    fn annotations(
187        &self,
188        play: &mut game::MoveMut<'_>,
189        annotations: impl IntoIterator<Item = Annotation>,
190    ) {
191        for annotation in annotations {
192            match annotation {
193                Annotation::Nag(nag) => play.update_nags(|nags| nags.push(nag)),
194                Annotation::Valuation(evaluation) => match self.eval {
195                    import::Eval::Drop => {}
196                    import::Eval::Passthrough => {
197                        play.update_commands(|commands| commands.push(evaluation.command()))
198                    }
199                },
200                Annotation::Command(command) => {
201                    if self.eval == import::Eval::Drop && command.command.as_ref() == "eval" {
202                        continue;
203                    }
204                    if command.is_expanded() {
205                        play.position_mut().set_expanded(true);
206                    } else {
207                        play.update_commands(|commands| commands.push(command));
208                    }
209                }
210            }
211        }
212    }
213
214    fn roster_text(value: &str, default: &str) -> Option<Text> {
215        let value = Text::new(value).ok()?;
216        (value.as_ref() != default).then_some(value)
217    }
218
219    fn tags(&self, tags: &[Tag]) -> Vec<game::Tag> {
220        tags.iter()
221            .filter_map(|tag| match tag {
222                Tag::Other(tag)
223                    if self.eval != import::Eval::Drop || tag.key.as_ref() != "Valuation" =>
224                {
225                    Some(tag.clone())
226                }
227                Tag::Valuation(evaluation) if self.eval == import::Eval::Passthrough => {
228                    Some(game::Tag {
229                        key: Text::new("Valuation").expect("tag name is non-empty"),
230                        value: evaluation.to_string(),
231                    })
232                }
233                _ => None,
234            })
235            .collect()
236    }
237}
238
239impl Export {
240    fn game(&self, game: &crate::Game) -> Pgn {
241        let start = game.start();
242        let position = start.position();
243        let intro = start.comment().cloned().map(Comment);
244        let moves = self.plays(start);
245        let outcome = game.outcome();
246        let tags = self.tags(game, position);
247
248        Pgn { start: position.parts(), tags, intro, moves, outcome }
249    }
250
251    fn tags(&self, game: &crate::Game, position: Position) -> Vec<Tag> {
252        let freestyle = game.mode().is_freestyle();
253        let evaluation = (self.eval == export::Eval::Overwrite)
254            .then(|| game.start().evaluation().and_then(Into::into))
255            .flatten();
256        let mut tags = Self::roster(game.roster(), game.outcome(), game.orientation());
257        if let Some(title) = game.title() {
258            tags.insert(0, Tag::Title(title.to_string()));
259        }
260        if freestyle {
261            tags.push(Tag::freestyle());
262        }
263        tags.extend(
264            game.tags()
265                .iter()
266                .filter(|tag| {
267                    tag.key.as_ref() != "Valuation" || self.eval == export::Eval::Passthrough
268                })
269                .cloned()
270                .map(Tag::Other),
271        );
272
273        if freestyle || (position.parts() != Position::start().parts()) {
274            Self::set_start(&mut tags, position.parts());
275        }
276        if freestyle && let Some(id) = Scharnagl::from_board(position.board()) {
277            tags.push(Tag::Chess960Id(id));
278        }
279        if let Some(evaluation) = evaluation {
280            tags.push(Tag::Valuation(evaluation));
281        }
282        tags
283    }
284
285    fn roster(roster: &Roster, outcome: game::Outcome, orientation: Player) -> Vec<Tag> {
286        let mut tags = Vec::new();
287        if let Some(value) = &roster.event {
288            tags.push(Tag::Event(value.to_string()));
289        }
290        if let Some(value) = &roster.site {
291            tags.push(Tag::Site(value.to_string()));
292        }
293        if let Some(value) = &roster.date {
294            tags.push(Tag::Date(value.to_string()));
295        }
296        if let Some(value) = &roster.round {
297            tags.push(Tag::Round(value.to_string()));
298        }
299        if let Some(value) = &roster.white {
300            tags.push(Tag::White(value.to_string()));
301        }
302        if let Some(value) = &roster.black {
303            tags.push(Tag::Black(value.to_string()));
304        }
305        tags.push(Tag::Outcome(outcome));
306        tags.push(Tag::Orientation(orientation));
307        tags
308    }
309
310    fn set_start(tags: &mut Vec<Tag>, position: position::Parts) {
311        tags.push(Tag::SetUp(true));
312        tags.push(Tag::Fen(position));
313    }
314
315    fn plays(&self, mut position: game::PositionRef<'_>) -> Vec<Move> {
316        let mut plays = Vec::new();
317
318        while let Ok(play) = position.main() {
319            let variations = position.alternatives().map(|play| self.variation(play)).collect();
320            plays.push(self.play(&play, variations));
321            position = play.position();
322        }
323
324        plays
325    }
326
327    fn play(&self, play: &game::MoveRef<'_>, variations: Vec<Variation>) -> Move {
328        Move {
329            san: play.san(),
330            comment: play.comment().cloned().map(Comment),
331            annotations: self.annotations(play),
332            variations,
333        }
334    }
335
336    fn variation(&self, play: game::MoveRef<'_>) -> Variation {
337        let affixes = play.affixes();
338
339        Variation {
340            intro: affixes.intro.clone().map(Comment),
341            moves: self.plays_from(&play),
342            outro: affixes.outro.clone().map(Comment),
343        }
344    }
345
346    fn plays_from(&self, play: &game::MoveRef<'_>) -> Vec<Move> {
347        let mut plays = vec![self.play(play, Vec::new())];
348        plays.extend(self.plays(play.position()));
349        plays
350    }
351
352    fn annotations(&self, play: &game::MoveRef<'_>) -> Vec<Annotation> {
353        let position = play.position();
354        let evaluation = (self.eval == export::Eval::Overwrite)
355            .then(|| position.evaluation().and_then(Into::into))
356            .flatten();
357        let mut annotations: Vec<_> = play.nags().iter().cloned().map(Annotation::Nag).collect();
358        if let Some(evaluation) = evaluation {
359            annotations.push(Annotation::Valuation(evaluation));
360        }
361        if position.expanded() {
362            annotations.push(Annotation::Command(game::Command::expanded()));
363        }
364        annotations.extend(
365            play.commands()
366                .iter()
367                .filter(|command| {
368                    command.command.as_ref() != "eval" || self.eval == export::Eval::Passthrough
369                })
370                .cloned()
371                .map(Annotation::Command),
372        );
373        annotations
374    }
375}