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)]
18pub 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}