Skip to main content

chess/game/
change.rs

1use core::ops::Neg;
2
3use super::{self as game, *};
4
5/// An error from applying a game patch.
6#[derive(Debug, thiserror::Error)]
7pub enum Error {
8    #[error("change tracking is disabled")]
9    Disabled,
10    #[error(transparent)]
11    Insert(#[from] Insert),
12}
13
14/// An error from inserting a patched branch.
15#[derive(Debug, thiserror::Error)]
16pub enum Insert {
17    #[error("move slot {0} is unavailable")]
18    Unavailable(MoveSlot),
19}
20
21/// A game mutation.
22// TODO: Review the JSON shape before treating persisted revisions as a stable format.
23#[derive(Clone, Deserialize, PartialEq, Serialize)]
24pub enum Patch {
25    /// Replaces public game data.
26    Game(Box<GamePublic>),
27    /// Replaces public position data.
28    Position { slot: PositionSlot, public: Box<PositionPublic> },
29    /// Replaces public move data.
30    Move { slot: MoveSlot, public: Box<MovePublic> },
31    /// Inserts a branch at an option index.
32    Insert { position: PositionSlot, index: usize, branch: Branch },
33    /// Removes the branch rooted at a move.
34    Remove(MoveSlot),
35    /// Moves an option between indices.
36    Drag { position: PositionSlot, from: usize, to: usize },
37    /// Exchanges two options.
38    Swap { position: PositionSlot, a: usize, b: usize },
39}
40
41/// A reversible game change.
42#[derive(Clone, Deserialize, PartialEq, Serialize)]
43pub enum Change {
44    Game { before: Box<GamePublic>, after: Box<GamePublic> },
45    Position { slot: PositionSlot, before: Box<PositionPublic>, after: Box<PositionPublic> },
46    Move { slot: MoveSlot, before: Box<MovePublic>, after: Box<MovePublic> },
47    Insert { position: PositionSlot, index: usize, branch: Branch },
48    Remove { position: PositionSlot, index: usize, branch: Branch },
49    Drag { position: PositionSlot, from: usize, to: usize },
50    Swap { position: PositionSlot, a: usize, b: usize },
51}
52
53/// A move and the complete game subtree following it.
54#[derive(Clone, Deserialize, PartialEq, Serialize)]
55pub struct Branch {
56    /// The move's assigned tree slot.
57    pub slot: MoveSlot,
58    /// The chess move.
59    pub play: chess::Move,
60    /// Public data associated with the move and resulting position.
61    #[serde(default, skip_serializing_if = "macros::is_default")]
62    pub public: BranchPublic,
63    /// Ordered continuations from the resulting position.
64    #[serde(default, skip_serializing_if = "macros::is_default")]
65    pub options: Vec<Branch>,
66}
67
68/// Public data associated with a branch root.
69#[derive(Clone, Default, Deserialize, PartialEq, Serialize)]
70pub struct BranchPublic {
71    /// Public data associated with the move.
72    #[serde(default, skip_serializing_if = "macros::is_default")]
73    pub play: MovePublic,
74    /// Public data associated with the resulting position.
75    #[serde(default, skip_serializing_if = "macros::is_default")]
76    pub position: PositionPublic,
77}
78
79impl Game {
80    pub fn undo(&mut self) -> game::Result<()> {
81        let Some(mut changes) = self.changes.take() else {
82            return Err(Error::Disabled.into());
83        };
84        let Some(change) = changes.pop() else {
85            self.changes = Some(changes);
86            return Ok(());
87        };
88        let backward = -change;
89        if let Err(error) = self.apply(&backward) {
90            changes.push(-backward);
91            self.changes = Some(changes);
92            return Err(error);
93        }
94        self.changes = Some(changes);
95        Ok(())
96    }
97
98    pub fn restore(&mut self, revision: usize) -> game::Result<()> {
99        if !self.tracks_changes() {
100            return Err(Error::Disabled.into());
101        }
102        while self.revisions().is_some_and(|revisions| revisions > revision) {
103            self.undo()?;
104        }
105        Ok(())
106    }
107
108    pub fn patch(&mut self, patch: Patch) -> game::Result<()> {
109        match patch {
110            Patch::Game(public) => self.update_public(|current| *current = *public),
111            Patch::Position { slot, public } => {
112                self.position_mut(slot)?.update_public(|current| *current = *public);
113            }
114            Patch::Move { slot, public } => {
115                self.play_mut(slot)?.update_public(|current| *current = *public);
116            }
117            Patch::Insert { position, index, branch } => {
118                self.insert_tracked(position, index, branch)?;
119            }
120            Patch::Remove(slot) => {
121                let position = self.play(slot)?.previous().slot();
122                self.position_mut(position)?.remove(slot)?;
123            }
124            Patch::Drag { position, from, to } => {
125                self.position_mut(position)?.drag(from, to)?;
126            }
127            Patch::Swap { position, a, b } => {
128                self.position_mut(position)?.swap(a, b)?;
129            }
130        }
131        Ok(())
132    }
133
134    pub(in crate::game) fn track(&mut self, inverse: Patch) {
135        let Some(mut changes) = self.changes.take() else { return };
136        let change = -inverse.reversible(self).expect("recorded patch applies to the current game");
137        changes.push(change);
138        self.changes = Some(changes);
139    }
140
141    fn apply(&mut self, change: &Change) -> game::Result<()> {
142        match change {
143            Change::Game { after, .. } => self.public = (**after).clone(),
144            Change::Position { slot, after, .. } => {
145                self.position_mut(*slot)?.position_mut().public = (**after).clone();
146            }
147            Change::Move { slot, after, .. } => {
148                self.play_mut(*slot)?.play_mut().public = (**after).clone();
149            }
150            Change::Drag { position, from, to } => {
151                self.position_mut(*position)?.drag_untracked(*from, *to)?;
152            }
153            Change::Swap { position, a, b } => {
154                self.position_mut(*position)?.swap_untracked(*a, *b)?;
155            }
156            Change::Remove { branch, .. } => {
157                let position = self.play(branch.slot)?.previous().slot();
158                self.position_mut(position)?.remove_untracked(branch.slot)?;
159            }
160            Change::Insert { position, index, branch } => {
161                self.insert_untracked(*position, *index, branch)?;
162            }
163        }
164        Ok(())
165    }
166
167    fn insert_tracked(
168        &mut self,
169        position: PositionSlot,
170        index: usize,
171        branch: Branch,
172    ) -> game::Result<()> {
173        self.insert_untracked(position, index, &branch)?;
174        if let Some(changes) = self.changes.as_mut() {
175            changes.push(Change::Insert { position, index, branch });
176        }
177        Ok(())
178    }
179
180    fn insert_untracked(
181        &mut self,
182        position: PositionSlot,
183        index: usize,
184        branch: &Branch,
185    ) -> game::Result<()> {
186        let parent = self.position(position)?;
187        if index > parent.options().len() {
188            let missing = traversal::Missing { position, choice: index.into() };
189            return Err(traversal::Error::from(missing).into());
190        }
191        if let Ok(index) = parent.index(branch.play) {
192            let duplicate = options::Insert::Duplicate(index);
193            return Err(options::Error::from(duplicate).into());
194        }
195
196        let mut trial = parent.into_game();
197        insert_branch(&mut trial, Some(self), START, 0, branch)?;
198        insert_branch(self, None, position, index, branch)
199    }
200}
201
202impl Patch {
203    fn reversible(self, game: &Game) -> traversal::Result<Change> {
204        Ok(match self {
205            Self::Game(after) => Change::Game { before: Box::new(game.public.clone()), after },
206            Self::Position { slot, public: after } => Change::Position {
207                slot,
208                before: Box::new(game.position(slot)?.public.clone()),
209                after,
210            },
211            Self::Move { slot, public: after } => {
212                Change::Move { slot, before: Box::new(game.play(slot)?.public.clone()), after }
213            }
214            Self::Insert { position, index, branch } => Change::Insert { position, index, branch },
215            Self::Remove(slot) => {
216                let position = game.play(slot)?.previous();
217                let index = position.index(slot)?;
218                Change::Remove { position: position.slot(), index, branch: game.tree.branch(slot) }
219            }
220            Self::Drag { position, from, to } => Change::Drag { position, from, to },
221            Self::Swap { position, a, b } => Change::Swap { position, a, b },
222        })
223    }
224}
225
226impl Change {
227    pub fn forward(&self) -> Patch {
228        match self {
229            Self::Game { after, .. } => Patch::Game(after.clone()),
230            Self::Position { slot, after, .. } => {
231                Patch::Position { slot: *slot, public: after.clone() }
232            }
233            Self::Move { slot, after, .. } => Patch::Move { slot: *slot, public: after.clone() },
234            Self::Insert { position, index, branch } => {
235                Patch::Insert { position: *position, index: *index, branch: branch.clone() }
236            }
237            Self::Remove { branch, .. } => Patch::Remove(branch.slot),
238            Self::Drag { position, from, to } => {
239                Patch::Drag { position: *position, from: *from, to: *to }
240            }
241            Self::Swap { position, a, b } => Patch::Swap { position: *position, a: *a, b: *b },
242        }
243    }
244
245    pub fn backward(&self) -> Patch {
246        match self {
247            Self::Game { before, .. } => Patch::Game(before.clone()),
248            Self::Position { slot, before, .. } => {
249                Patch::Position { slot: *slot, public: before.clone() }
250            }
251            Self::Move { slot, before, .. } => Patch::Move { slot: *slot, public: before.clone() },
252            Self::Insert { branch, .. } => Patch::Remove(branch.slot),
253            Self::Remove { position, index, branch } => {
254                Patch::Insert { position: *position, index: *index, branch: branch.clone() }
255            }
256            Self::Drag { position, from, to } => {
257                Patch::Drag { position: *position, from: *to, to: *from }
258            }
259            Self::Swap { position, a, b } => Patch::Swap { position: *position, a: *a, b: *b },
260        }
261    }
262}
263
264impl Neg for Change {
265    type Output = Self;
266
267    fn neg(self) -> Self::Output {
268        match self {
269            Self::Game { before, after } => Self::Game { before: after, after: before },
270            Self::Position { slot, before, after } => {
271                Self::Position { slot, before: after, after: before }
272            }
273            Self::Move { slot, before, after } => Self::Move { slot, before: after, after: before },
274            Self::Insert { position, index, branch } => Self::Remove { position, index, branch },
275            Self::Remove { position, index, branch } => Self::Insert { position, index, branch },
276            Self::Drag { position, from, to } => Self::Drag { position, from: to, to: from },
277            Self::Swap { position, a, b } => Self::Swap { position, a, b },
278        }
279    }
280}
281
282fn insert_branch(
283    game: &mut Game,
284    source: Option<&Game>,
285    position: PositionSlot,
286    index: usize,
287    branch: &Branch,
288) -> game::Result<()> {
289    let unavailable_in_source = source.is_some_and(|source| source.play(branch.slot).is_ok());
290    let unavailable_in_game = game.play(branch.slot).is_ok();
291    if unavailable_in_source || unavailable_in_game {
292        return Err(Error::Insert(Insert::Unavailable(branch.slot)).into());
293    }
294
295    game.position_mut(position)?.insert_at_untracked(Some(branch.slot), index, branch.play)?;
296    let mut play = game.play_mut(branch.slot)?;
297    play.play_mut().public = branch.public.play.clone();
298    play.position_mut().position_mut().public = branch.public.position.clone();
299
300    for (index, option) in branch.options.iter().enumerate() {
301        insert_branch(game, source, branch.slot.into(), index, option)?;
302    }
303    Ok(())
304}
305
306#[cfg(test)]
307mod tests {
308    use arbitrary::{Arbitrary as _, Unstructured};
309
310    use crate::{
311        Game, Player, Position,
312        board::Role::{Knight, Pawn},
313        formats::Pgn,
314        position::{Evaluation, Terminal},
315        square::Square::*,
316    };
317
318    use super::*;
319
320    #[test]
321    fn game_change_tracking() {
322        let mut game = Game::new(Position::start());
323        assert!(!game.tracks_changes());
324        assert_eq!(game.revisions(), None);
325        assert!(matches!(game.undo(), Err(game::Error::Change(Error::Disabled))));
326        assert!(matches!(game.restore(0), Err(game::Error::Change(Error::Disabled))));
327
328        game.track_changes(true);
329        assert!(game.tracks_changes());
330        assert_eq!(game.revisions(), Some(0));
331        game.track_changes(false);
332        assert!(!game.tracks_changes());
333        assert!(game.changes().is_none());
334    }
335
336    #[test]
337    fn game_public_changes() {
338        let mut game = Game::new(Position::start());
339        game.set_orientation(Player::Black);
340        assert!(game.changes().is_none());
341
342        game.track_changes(true);
343        game.set_orientation(Player::Black);
344        game.update_roster(|_| ());
345        game.update_tags(|_| ());
346        assert!(game.changes().unwrap().is_empty());
347
348        game.set_orientation(Player::White);
349        game.set_outcome(Outcome::Draw);
350        game.update_roster(|roster| roster.event = Some("Event".parse().unwrap()));
351        game.update_tags(|tags| {
352            tags.push(Tag { key: "Annotator".parse().unwrap(), value: "Player".to_string() });
353        });
354        assert_eq!(game.changes().unwrap().len(), 4);
355
356        let Change::Game { before, after } = &game.changes().unwrap()[0] else { panic!() };
357        assert_eq!(before.orientation, Player::Black);
358        assert_eq!(after.orientation, Player::White);
359
360        let Change::Game { before, after } = &game.changes().unwrap()[3] else { panic!() };
361        assert!(before.tags.is_empty());
362        assert_eq!(after.tags.len(), 1);
363    }
364
365    #[test]
366    fn position_public_changes() {
367        let mut game = Game::new(Position::start());
368        let comment: Text = "Comment".parse().unwrap();
369        game.start_mut().set_comment(Some(comment.clone()));
370        assert!(game.changes().is_none());
371
372        game.track_changes(true);
373        game.start_mut().set_comment(Some(comment));
374        assert!(game.changes().unwrap().is_empty());
375
376        game.start_mut().set_comment(None);
377        game.start_mut().set_evaluation(Some(Evaluation::Terminal(Terminal::Stalemate)));
378        game.start_mut().set_expanded(true);
379        assert_eq!(game.changes().unwrap().len(), 2);
380        assert_eq!(game.start().evaluation(), Some(&Evaluation::Terminal(Terminal::Stalemate)));
381
382        let Change::Position { slot, before, after } = &game.changes().unwrap()[0] else {
383            panic!()
384        };
385        assert_eq!(*slot, START);
386        assert!(before.comment.is_some());
387        assert!(after.comment.is_none());
388    }
389
390    #[test]
391    fn clear_evaluations_does_not_change_history() {
392        let mut game = Game::new(Position::start());
393        game.track_changes(true);
394        let slot = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
395        let evaluation = Evaluation::Terminal(Terminal::Stalemate);
396        game.start_mut().set_evaluation(Some(evaluation.clone()));
397        game.play_mut(slot).unwrap().position_mut().set_evaluation(Some(evaluation));
398        let revision = game.revisions();
399
400        game.clear_evaluations();
401
402        assert_eq!(game.start().evaluation(), None);
403        assert_eq!(game.play(slot).unwrap().evaluation(), None);
404        assert_eq!(game.revisions(), revision);
405    }
406
407    #[test]
408    fn move_public_changes() {
409        let mut game = Game::new(Position::start());
410        let slot = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
411        let intro: Text = "Intro".parse().unwrap();
412        game.play_mut(slot).unwrap().set_intro(Some(intro.clone()));
413        assert!(game.changes().is_none());
414
415        game.track_changes(true);
416        game.play_mut(slot).unwrap().set_intro(Some(intro));
417        assert!(game.changes().unwrap().is_empty());
418
419        game.play_mut(slot).unwrap().set_intro(None);
420        game.play_mut(slot).unwrap().set_outro(Some("Outro".parse().unwrap()));
421        game.play_mut(slot).unwrap().update_commands(|commands| commands.push(Command::expanded()));
422        game.play_mut(slot).unwrap().update_nags(|nags| nags.push(Nag::Numeric(1)));
423        assert_eq!(game.changes().unwrap().len(), 4);
424
425        let Change::Move { slot: patched, before, after } = &game.changes().unwrap()[0] else {
426            panic!()
427        };
428        assert_eq!(*patched, slot);
429        assert!(before.affixes.intro.is_some());
430        assert!(after.affixes.intro.is_none());
431    }
432
433    #[test]
434    fn option_order_changes() {
435        let mut game = Game::new(Position::start());
436        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
437        let d4 = game.start_mut().push(crate::Move::normal(Pawn, D2, D4)).unwrap().slot();
438        let c4 = game.start_mut().push(crate::Move::normal(Pawn, C2, C4)).unwrap().slot();
439
440        game.track_changes(true);
441        assert!(!game.start_mut().promote(e4).unwrap());
442        assert!(game.changes().unwrap().is_empty());
443
444        assert!(game.start_mut().promote(c4).unwrap());
445        assert!(game.start_mut().swap(c4, d4).unwrap());
446        assert_eq!(game.changes().unwrap().len(), 2);
447
448        let Change::Drag { position, from, to } = game.changes().unwrap()[0] else { panic!() };
449        assert_eq!(position, START);
450        assert_eq!((from, to), (2, 0));
451
452        let Change::Swap { position, a, b } = game.changes().unwrap()[1] else { panic!() };
453        assert_eq!(position, START);
454        assert_eq!((a, b), (0, 2));
455    }
456
457    #[test]
458    fn apply_public_patch() {
459        let mut game = Game::new(Position::start());
460        game.track_changes(true);
461        game.set_outcome(Outcome::Draw);
462
463        let draw = game.public.clone();
464        let change = game.changes().unwrap()[0].clone();
465        game.apply(&-change).unwrap();
466        assert_eq!(game.outcome(), Outcome::Unknown);
467        assert_eq!(game.changes().unwrap().len(), 1);
468
469        let change = Patch::Game(Box::new(draw)).reversible(&game).unwrap();
470        game.apply(&change).unwrap();
471        assert_eq!(game.outcome(), Outcome::Draw);
472        assert_eq!(game.changes().unwrap().len(), 1);
473    }
474
475    #[test]
476    fn patch_records_actual_state() {
477        let mut game = Game::new(Position::start());
478        let mut public = game.public.clone();
479        public.outcome = Outcome::Draw;
480        game.patch(Patch::Game(Box::new(public.clone()))).unwrap();
481        assert!(game.changes().is_none());
482
483        game.track_changes(true);
484        public.outcome = Outcome::White;
485        game.patch(Patch::Game(Box::new(public))).unwrap();
486
487        let Change::Game { before, after } = &game.changes().unwrap()[0] else { panic!() };
488        assert_eq!(before.outcome, Outcome::Draw);
489        assert_eq!(after.outcome, Outcome::White);
490    }
491
492    #[test]
493    fn replay_forward_history() {
494        let mut game = Game::new(Position::start());
495        let base = game.clone();
496        game.track_changes(true);
497        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
498        game.play_mut(e4).unwrap().set_comment(Some("King's pawn".parse().unwrap()));
499        game.play_mut(e4).unwrap().position_mut().push(crate::Move::normal(Pawn, E7, E5)).unwrap();
500        game.set_outcome(Outcome::Draw);
501
502        let patches = game
503            .changes()
504            .unwrap()
505            .iter()
506            .map(|change| {
507                let encoded_change = serde_json::to_string(change).unwrap();
508                let decoded_change: Change = serde_json::from_str(&encoded_change).unwrap();
509                assert!(decoded_change == *change);
510
511                let patch = change.forward();
512                let encoded_patch = serde_json::to_string(&patch).unwrap();
513                let decoded_patch: Patch = serde_json::from_str(&encoded_patch).unwrap();
514                assert!(decoded_patch == patch);
515                decoded_patch
516            })
517            .collect::<Vec<_>>();
518
519        assert_eq!(
520            serde_json::to_value(&patches[0]).unwrap(),
521            serde_json::json!({
522                "Insert": {"position": 0, "index": 0, "branch": {"slot": 1, "play": "e2-e4"}}
523            })
524        );
525        assert_eq!(
526            serde_json::to_value(&game.changes().unwrap()[1]).unwrap(),
527            serde_json::json!({
528                "Position": {"slot": 1, "before": {}, "after": {"comment": "King's pawn"}}
529            })
530        );
531
532        let mut replay = base;
533        replay.track_changes(true);
534        for patch in patches {
535            replay.patch(patch).unwrap();
536        }
537
538        assert!(replay.changes() == game.changes());
539        assert_eq!(Pgn::from(replay), Pgn::from(game));
540    }
541
542    #[test]
543    fn apply_order_patch() {
544        let mut game = Game::new(Position::start());
545        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
546        let d4 = game.start_mut().push(crate::Move::normal(Pawn, D2, D4)).unwrap().slot();
547        let c4 = game.start_mut().push(crate::Move::normal(Pawn, C2, C4)).unwrap().slot();
548        game.track_changes(true);
549        game.start_mut().promote(c4).unwrap();
550
551        let change = game.changes().unwrap()[0].clone();
552        game.apply(&-change).unwrap();
553        assert_eq!(game.start().options(), [e4, d4, c4]);
554        assert_eq!(game.changes().unwrap().len(), 1);
555    }
556
557    #[test]
558    fn apply_remove_patch() {
559        let mut game = Game::new(Position::start());
560        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
561        let e5 = game
562            .play_mut(e4)
563            .unwrap()
564            .position_mut()
565            .push(crate::Move::normal(Pawn, E7, E5))
566            .unwrap()
567            .slot();
568        game.track_changes(true);
569
570        game.patch(Patch::Remove(e4)).unwrap();
571        assert!(game.play(e4).is_err());
572        assert!(game.play(e5).is_err());
573
574        let Change::Remove { position, index, branch } = &game.changes().unwrap()[0] else {
575            panic!()
576        };
577        assert_eq!(*position, START);
578        assert_eq!(*index, 0);
579        assert_eq!(branch.slot, e4);
580        assert_eq!(branch.options[0].slot, e5);
581    }
582
583    #[test]
584    fn branch_capture() {
585        let mut game = Game::new(Position::start());
586        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
587        let e5 = game
588            .play_mut(e4)
589            .unwrap()
590            .position_mut()
591            .push(crate::Move::normal(Pawn, E7, E5))
592            .unwrap()
593            .slot();
594        let c5 = game
595            .play_mut(e4)
596            .unwrap()
597            .position_mut()
598            .push(crate::Move::normal(Pawn, C7, C5))
599            .unwrap()
600            .slot();
601        let nf3 = game
602            .play_mut(e5)
603            .unwrap()
604            .position_mut()
605            .push(crate::Move::normal(Knight, G1, F3))
606            .unwrap()
607            .slot();
608        game.play_mut(e4).unwrap().set_intro(Some("Intro".parse().unwrap()));
609        game.play_mut(e4).unwrap().set_comment(Some("Comment".parse().unwrap()));
610
611        let branch = game.tree.branch(e4);
612        assert_eq!(branch.slot, e4);
613        assert_eq!(branch.play, crate::Move::normal(Pawn, E2, E4));
614        assert!(branch.public.play.affixes.intro.is_some());
615        assert!(branch.public.position.comment.is_some());
616        assert_eq!(branch.options.iter().map(|branch| branch.slot).collect::<Vec<_>>(), [e5, c5]);
617        assert_eq!(branch.options[0].options[0].slot, nf3);
618    }
619
620    #[test]
621    fn remove_patch() {
622        let mut game = Game::new(Position::start());
623        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
624        let e5 = game
625            .play_mut(e4)
626            .unwrap()
627            .position_mut()
628            .push(crate::Move::normal(Pawn, E7, E5))
629            .unwrap()
630            .slot();
631
632        game.track_changes(true);
633        game.start_mut().remove(e4).unwrap();
634        assert!(game.play(e4).is_err());
635        assert!(game.play(e5).is_err());
636        assert_eq!(game.changes().unwrap().len(), 1);
637
638        let Change::Remove { position, index, branch } = &game.changes().unwrap()[0] else {
639            panic!()
640        };
641        assert_eq!(*position, START);
642        assert_eq!(*index, 0);
643        assert_eq!(branch.slot, e4);
644        assert_eq!(branch.options[0].slot, e5);
645
646        assert!(game.start_mut().remove(e4).is_err());
647        assert_eq!(game.changes().unwrap().len(), 1);
648    }
649
650    #[test]
651    fn insert_patch() {
652        let mut game = Game::new(Position::start());
653        let play = crate::Move::normal(Pawn, E2, E4);
654        game.track_changes(true);
655
656        let e4 = game.start_mut().push(play).unwrap().slot();
657        assert_eq!(game.changes().unwrap().len(), 1);
658        let Change::Insert { branch, .. } = &game.changes().unwrap()[0] else { panic!() };
659        assert_eq!(branch.slot, e4);
660        assert_eq!(game.start().options(), [e4]);
661
662        assert!(game.start_mut().push(play).is_err());
663        assert_eq!(game.changes().unwrap().len(), 1);
664    }
665
666    #[test]
667    fn insert_branch_round_trip() {
668        let mut game = Game::new(Position::start());
669        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
670        let e5 = game
671            .play_mut(e4)
672            .unwrap()
673            .position_mut()
674            .push(crate::Move::normal(Pawn, E7, E5))
675            .unwrap()
676            .slot();
677        game.play_mut(e4).unwrap().position_mut().push(crate::Move::normal(Pawn, C7, C5)).unwrap();
678        game.play_mut(e5)
679            .unwrap()
680            .position_mut()
681            .push(crate::Move::normal(Knight, G1, F3))
682            .unwrap();
683        game.play_mut(e4).unwrap().set_intro(Some("Intro".parse().unwrap()));
684        game.play_mut(e5).unwrap().set_comment(Some("Comment".parse().unwrap()));
685
686        let before = game.clone();
687        let branch = game.tree.branch(e4);
688        game.start_mut().remove(e4).unwrap();
689        game.patch(Patch::Insert { position: START, index: 0, branch }).unwrap();
690
691        assert!(game == before);
692    }
693
694    #[test]
695    fn failed_insert_is_atomic() {
696        let mut game = Game::new(Position::start());
697        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
698        game.play_mut(e4).unwrap().position_mut().push(crate::Move::normal(Pawn, E7, E5)).unwrap();
699        let mut branch = game.tree.branch(e4);
700        game.start_mut().remove(e4).unwrap();
701        let before = game.clone();
702        branch.options[0].play = crate::Move::normal(Pawn, E2, E4);
703
704        let error = game.patch(Patch::Insert { position: START, index: 0, branch }).unwrap_err();
705
706        assert!(matches!(
707            error,
708            game::Error::Options(options::Error::Insert(options::Insert::Illegal))
709        ));
710        assert!(game == before);
711    }
712
713    #[test]
714    fn unavailable_insert_is_atomic() {
715        let mut source = Game::new(Position::start());
716        let e4 = source.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
717        let branch = source.tree.branch(e4);
718
719        let mut game = Game::new(Position::start());
720        game.start_mut().push(crate::Move::normal(Pawn, D2, D4)).unwrap();
721        let before = game.clone();
722
723        let error = game.patch(Patch::Insert { position: START, index: 1, branch }).unwrap_err();
724
725        assert!(matches!(
726            error,
727            game::Error::Change(Error::Insert(Insert::Unavailable(slot))) if slot == e4
728        ));
729        assert!(game == before);
730    }
731
732    #[test]
733    fn into_game() {
734        let mut game = Game::new(Position::start());
735        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
736        let d4 = game.start_mut().push(crate::Move::normal(Pawn, D2, D4)).unwrap().slot();
737        let e5 = game
738            .play_mut(e4)
739            .unwrap()
740            .position_mut()
741            .push(crate::Move::normal(Pawn, E7, E5))
742            .unwrap()
743            .slot();
744
745        let mut position = game.play(e4).unwrap().position().into_game();
746        assert!(position.start().options().is_empty());
747        let position_e5 =
748            position.start_mut().push(crate::Move::normal(Pawn, E7, E5)).unwrap().slot();
749        assert_eq!(position_e5, e5.next().unwrap());
750
751        let mut play = game.play(e4).unwrap().into_game();
752        assert_eq!(play.start().options(), [e4]);
753        assert!(play.play(e4).unwrap().position().options().is_empty());
754        assert!(play.play(d4).is_err());
755        assert!(play.play(e5).is_err());
756        let play_e5 = play
757            .play_mut(e4)
758            .unwrap()
759            .position_mut()
760            .push(crate::Move::normal(Pawn, E7, E5))
761            .unwrap()
762            .slot();
763        assert_eq!(play_e5, e5.next().unwrap());
764    }
765
766    #[test]
767    fn arbitrary_insert_round_trip() {
768        let mut tested = 0;
769        for seed in 0_u8..100 {
770            let bytes = (0..1024)
771                .map(|index| (index as u8).wrapping_mul(31).wrapping_add(seed))
772                .collect::<Vec<_>>();
773            let mut input = Unstructured::new(&bytes);
774            let Ok(mut game) = Game::arbitrary(&mut input) else { continue };
775            let Some(slot) = game.start().iter().next().map(|play| play.slot()) else { continue };
776
777            let before = game.clone();
778            let branch = game.tree.branch(slot);
779            game.start_mut().remove(slot).unwrap();
780            game.patch(Patch::Insert { position: START, index: 0, branch }).unwrap();
781            assert!(game == before);
782
783            tested += 1;
784            if tested == 10 {
785                break;
786            }
787        }
788        assert_eq!(tested, 10);
789    }
790
791    #[test]
792    fn insert_branch_at_deep_nonzero_index() {
793        let mut game = Game::new(Position::start());
794        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
795        game.play_mut(e4).unwrap().position_mut().push(crate::Move::normal(Pawn, E7, E5)).unwrap();
796        let c5 = game
797            .play_mut(e4)
798            .unwrap()
799            .position_mut()
800            .push(crate::Move::normal(Pawn, C7, C5))
801            .unwrap()
802            .slot();
803        let before = game.clone();
804        let branch = game.tree.branch(c5);
805
806        game.play_mut(e4).unwrap().position_mut().remove(c5).unwrap();
807        game.patch(Patch::Insert { position: e4.into(), index: 1, branch }).unwrap();
808
809        assert!(game == before);
810    }
811
812    #[test]
813    fn insert_branch_records_one_change() {
814        let mut source = Game::new(Position::start());
815        let e4 = source.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
816        source
817            .play_mut(e4)
818            .unwrap()
819            .position_mut()
820            .push(crate::Move::normal(Pawn, E7, E5))
821            .unwrap();
822        let branch = source.tree.branch(e4);
823
824        let mut game = Game::new(Position::start());
825        game.track_changes(true);
826        game.patch(Patch::Insert { position: START, index: 0, branch }).unwrap();
827
828        assert_eq!(game.changes().unwrap().len(), 1);
829        assert!(matches!(
830            game.changes().unwrap()[0],
831            Change::Insert { ref branch, .. } if branch.slot == e4
832        ));
833    }
834
835    #[test]
836    fn out_of_range_insert_is_atomic() {
837        let mut source = Game::new(Position::start());
838        let e4 = source.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
839        let branch = source.tree.branch(e4);
840
841        let mut game = Game::new(Position::start());
842        let before = game.clone();
843        let error = game.patch(Patch::Insert { position: START, index: 1, branch }).unwrap_err();
844
845        assert!(matches!(
846            error,
847            game::Error::Traversal(traversal::Error::Option(ref missing))
848                if missing.position == START
849                    && missing.choice == traversal::Choice::Index(1)
850        ));
851        assert!(game == before);
852    }
853
854    #[test]
855    fn duplicate_branch_slot_is_atomic() {
856        let mut source = Game::new(Position::start());
857        let e4 = source.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
858        source
859            .play_mut(e4)
860            .unwrap()
861            .position_mut()
862            .push(crate::Move::normal(Pawn, E7, E5))
863            .unwrap();
864        let mut branch = source.tree.branch(e4);
865        branch.options[0].slot = e4;
866
867        let mut game = Game::new(Position::start());
868        let before = game.clone();
869        let error = game.patch(Patch::Insert { position: START, index: 0, branch }).unwrap_err();
870
871        assert!(matches!(
872            error,
873            game::Error::Change(Error::Insert(Insert::Unavailable(slot))) if slot == e4
874        ));
875        assert!(game == before);
876    }
877
878    #[test]
879    fn undo_public_mutation() {
880        let mut game = Game::new(Position::start());
881        game.track_changes(true);
882        game.set_outcome(Outcome::Draw);
883
884        game.undo().unwrap();
885        assert_eq!(game.outcome(), Outcome::Unknown);
886        assert!(game.changes().unwrap().is_empty());
887        game.undo().unwrap();
888    }
889
890    #[test]
891    fn reslot_clears_history() {
892        let mut game = Game::new(Position::start());
893        game.track_changes(true);
894        game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap();
895
896        game.reslot(Layout::Pgn);
897
898        assert!(game.changes().unwrap().is_empty());
899        game.set_outcome(Outcome::Draw);
900        assert_eq!(game.changes().unwrap().len(), 1);
901    }
902
903    #[test]
904    fn undo_branch_removal() {
905        let mut game = Game::new(Position::start());
906        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
907        game.play_mut(e4).unwrap().position_mut().push(crate::Move::normal(Pawn, E7, E5)).unwrap();
908        game.track_changes(true);
909        let before = game.clone();
910
911        game.start_mut().remove(e4).unwrap();
912        game.undo().unwrap();
913
914        assert!(game == before);
915    }
916
917    #[test]
918    fn restore_revision() {
919        let mut game = Game::new(Position::start());
920        game.track_changes(true);
921        game.set_orientation(Player::Black);
922        game.set_outcome(Outcome::Draw);
923
924        game.restore(1).unwrap();
925        assert_eq!(game.orientation(), Player::Black);
926        assert_eq!(game.outcome(), Outcome::Unknown);
927        assert_eq!(game.changes().unwrap().len(), 1);
928
929        game.restore(2).unwrap();
930        assert_eq!(game.changes().unwrap().len(), 1);
931
932        game.restore(0).unwrap();
933        assert_eq!(game.orientation(), Player::White);
934        assert!(game.changes().unwrap().is_empty());
935    }
936
937    #[test]
938    fn undo_position_and_move_public() {
939        let mut game = Game::new(Position::start());
940        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
941        game.track_changes(true);
942
943        game.start_mut().set_comment(Some("Position".parse().unwrap()));
944        game.play_mut(e4).unwrap().set_intro(Some("Move".parse().unwrap()));
945        game.restore(0).unwrap();
946
947        assert!(game.start().comment().is_none());
948        assert!(game.play(e4).unwrap().public.affixes.intro.is_none());
949    }
950
951    #[test]
952    fn undo_pushed_move() {
953        let mut game = Game::new(Position::start());
954        game.track_changes(true);
955        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
956
957        game.undo().unwrap();
958        assert!(game.start().options().is_empty());
959        assert!(game.play(e4).is_err());
960    }
961
962    #[test]
963    fn undo_option_order() {
964        let mut game = Game::new(Position::start());
965        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
966        let d4 = game.start_mut().push(crate::Move::normal(Pawn, D2, D4)).unwrap().slot();
967        let c4 = game.start_mut().push(crate::Move::normal(Pawn, C2, C4)).unwrap().slot();
968        game.track_changes(true);
969        let before = game.clone();
970
971        game.start_mut().promote(c4).unwrap();
972        game.start_mut().swap(e4, d4).unwrap();
973        game.restore(0).unwrap();
974
975        assert!(game == before);
976    }
977
978    #[test]
979    fn failed_undo_restores_change() {
980        let mut game = Game::new(Position::start());
981        let e4 = game.start_mut().push(crate::Move::normal(Pawn, E2, E4)).unwrap().slot();
982        let branch = game.tree.branch(e4);
983        game.start_mut().remove(e4).unwrap();
984        game.track_changes(true);
985        game.changes.as_mut().unwrap().push(Change::Insert { position: START, index: 0, branch });
986
987        let error = game.undo().unwrap_err();
988
989        assert!(matches!(
990            error,
991            game::Error::Traversal(traversal::Error::Move(slot)) if slot == e4
992        ));
993        assert!(matches!(
994            game.changes().unwrap(),
995            [Change::Insert { branch, .. }] if branch.slot == e4
996        ));
997    }
998}