Skip to main content

chess/formats/pgn/
convert.rs

1use crate::{
2    board::scharnagl_by_id,
3    game::{self, Mode, Node, 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 error: {0}")]
27    Game(#[from] game::Error),
28}
29
30pub type Result<T, E = Error> = core::result::Result<T, E>;
31
32impl From<crate::Game> for Game {
33    fn from(game: crate::Game) -> Self {
34        let mode = game.mode();
35        let freestyle = mode.is_freestyle();
36        let start = game.start();
37        let moves = pgn_moves(&game, game.start_options());
38        let mut tags = pgn_roster(&game.roster, game.outcome);
39        if freestyle {
40            tags.push(Tag::freestyle());
41        }
42        tags.extend(game.tags.into_iter().map(Tag::Other));
43
44        if freestyle || (start.parts() != Position::start().parts()) {
45            pgn_set_start(&mut tags, start.parts());
46        }
47        if freestyle && let Some(id) = scharnagl_by_id(start.board().standard_id()) {
48            tags.push(Tag::Chess960Id(id));
49        }
50
51        Self {
52            tags,
53            start: start.parts(),
54            intro: game.intro.map(Comment),
55            moves,
56            outcome: game.outcome,
57        }
58    }
59}
60
61impl TryFrom<Game> for crate::Game {
62    type Error = Error;
63
64    fn try_from(pgn: Game) -> Result<Self> {
65        let mode = pgn.mode();
66        let position = pgn.start.validate().map_err(|error| Error::Start {
67            mode,
68            fen: pgn.start.fen(),
69            error,
70        })?;
71        Ok(game_from_position(pgn, position, mode)?)
72    }
73}
74
75impl crate::Game {
76    pub fn from_pgn(pgn: Game) -> Result<Self> {
77        pgn.try_into()
78    }
79}
80
81fn game_from_position(
82    pgn: Game,
83    position: Position,
84    mode: Mode,
85) -> core::result::Result<crate::Game, Resolve> {
86    let mut game = crate::Game::new(position, mode);
87    game.roster = game_roster(&pgn.tags);
88    game.tags = game_tags(&pgn.tags);
89    game.intro = pgn.intro.map(Into::into);
90    game.outcome = pgn.outcome;
91
92    game_moves(&mut game, Node::Start, 0, pgn.moves)?;
93
94    Ok(game)
95}
96
97fn game_moves(
98    game: &mut crate::Game,
99    mut previous: Node,
100    mut ply: usize,
101    moves: Vec<Move>,
102) -> core::result::Result<(), Resolve> {
103    for pgn_move in moves {
104        let mut options = game.options_mut(previous).expect("previous play exists");
105        let play = pgn_move.san.resolve(options.as_ref().legal())?;
106        let mut play = options.push(play)?;
107        let slot = play.slot();
108        play.meta.comment = pgn_move.comment.map(Into::into);
109        for annotation in pgn_move.annotations {
110            match annotation {
111                Annotation::Nag(nag) => play.meta.nags.push(nag),
112                Annotation::Command(command) => play.meta.commands.push(command),
113            }
114        }
115
116        for variation in pgn_move.variations {
117            game_variation(game, previous, ply, variation)?;
118        }
119
120        previous = Node::Play(slot);
121        ply += 1;
122    }
123
124    Ok(())
125}
126
127fn game_variation(
128    game: &mut crate::Game,
129    previous: Node,
130    ply: usize,
131    variation: Variation,
132) -> core::result::Result<(), Resolve> {
133    let Some((first, rest)) = variation.moves.split_first() else {
134        return Ok(());
135    };
136
137    let mut options = game.options_mut(previous).expect("previous play exists");
138    let play = first.san.resolve(options.as_ref().legal())?;
139    let mut play = options.push(play)?;
140    let slot = play.slot();
141    play.meta.intro = variation.intro.map(Into::into);
142    play.meta.outro = variation.outro.map(Into::into);
143    play.meta.comment = first.comment.clone().map(Into::into);
144    for annotation in &first.annotations {
145        match annotation {
146            Annotation::Nag(nag) => play.meta.nags.push(nag.clone()),
147            Annotation::Command(command) => play.meta.commands.push(command.clone()),
148        }
149    }
150
151    for variation in &first.variations {
152        game_variation(game, previous, ply, variation.clone())?;
153    }
154    game_moves(game, Node::Play(slot), ply + 1, rest.to_vec())
155}
156
157//
158// Helper functions for From<crate::Game> for pgn::Game
159//
160// The prefix `pgn_` means pgn::Game
161//
162
163fn game_roster(tags: &[Tag]) -> Roster {
164    let mut roster = Roster::default();
165
166    for tag in tags {
167        match tag {
168            Tag::Event(value) => roster.event = roster_text(value, "?"),
169            Tag::Site(value) => roster.site = roster_text(value, "?"),
170            Tag::Date(value) => roster.date = roster_text(value, "????.??.??"),
171            Tag::Round(value) => roster.round = roster_text(value, "?"),
172            Tag::White(value) => roster.white = roster_text(value, "?"),
173            Tag::Black(value) => roster.black = roster_text(value, "?"),
174            Tag::Outcome(_)
175            | Tag::Fen(_)
176            | Tag::SetUp(_)
177            | Tag::Variant(_)
178            | Tag::Chess960Id(_)
179            | Tag::Other(_) => {}
180        }
181    }
182
183    roster
184}
185
186fn roster_text(value: &str, default: &str) -> Option<game::Text> {
187    let value = game::Text::new(value)?;
188    (value.as_ref() != default).then_some(value)
189}
190
191fn game_tags(tags: &[Tag]) -> Vec<game::Tag> {
192    tags.iter()
193        .filter_map(|tag| match tag {
194            Tag::Other(tag) => Some(tag.clone()),
195            _ => None,
196        })
197        .collect()
198}
199
200fn pgn_roster(roster: &Roster, outcome: game::Outcome) -> Vec<Tag> {
201    let mut tags = Vec::new();
202    if let Some(value) = &roster.event {
203        tags.push(Tag::Event(value.to_string()));
204    }
205    if let Some(value) = &roster.site {
206        tags.push(Tag::Site(value.to_string()));
207    }
208    if let Some(value) = &roster.date {
209        tags.push(Tag::Date(value.to_string()));
210    }
211    if let Some(value) = &roster.round {
212        tags.push(Tag::Round(value.to_string()));
213    }
214    if let Some(value) = &roster.white {
215        tags.push(Tag::White(value.to_string()));
216    }
217    if let Some(value) = &roster.black {
218        tags.push(Tag::Black(value.to_string()));
219    }
220    tags.push(Tag::Outcome(outcome));
221    tags
222}
223
224fn pgn_set_start(tags: &mut Vec<Tag>, position: position::Parts) {
225    tags.push(Tag::SetUp(true));
226    tags.push(Tag::Fen(position));
227}
228
229fn pgn_moves<'g>(game: &'g crate::Game, options: game::OptionsRef<'g>) -> Vec<Move> {
230    let mut moves = Vec::new();
231    let mut options = options;
232
233    while let Some((play, variations)) = options.split_first() {
234        moves.push(pgn_move(game, &play, variations));
235        options = play.options();
236    }
237
238    moves
239}
240
241fn pgn_move(
242    game: &crate::Game,
243    play: &game::Play,
244    variations: game::OptionsRef<'_>,
245) -> Move {
246    Move {
247        san: san::San::from((play.play(), play.short(), play.check())),
248        comment: play.meta.comment.clone().map(Comment),
249        annotations: pgn_annotations(&play.meta),
250        variations: variations.iter().map(|play| pgn_variation(game, play.slot())).collect(),
251    }
252}
253
254fn pgn_variation(game: &crate::Game, slot: Slot) -> Variation {
255    let play = game.play(slot).expect("option must reference an existing play");
256    Variation {
257        intro: play.meta.intro.clone().map(Comment),
258        moves: pgn_moves_from(game, &play),
259        outro: play.meta.outro.clone().map(Comment),
260    }
261}
262
263fn pgn_moves_from<'g>(game: &'g crate::Game, play: &game::PlayRef<'g>) -> Vec<Move> {
264    let mut moves = vec![pgn_move_without_variations(play)];
265    moves.extend(pgn_moves(game, play.options()));
266    moves
267}
268
269fn pgn_move_without_variations(play: &game::Play) -> Move {
270    Move {
271        san: san::San::from((play.play(), play.short(), play.check())),
272        comment: play.meta.comment.clone().map(Comment),
273        annotations: pgn_annotations(&play.meta),
274        variations: Vec::new(),
275    }
276}
277
278fn pgn_annotations(meta: &game::Meta) -> Vec<Annotation> {
279    meta.nags
280        .iter()
281        .cloned()
282        .map(Annotation::Nag)
283        .chain(meta.commands.iter().cloned().map(Annotation::Command))
284        .collect()
285}