Skip to main content

chess/
game.rs

1use core::{fmt, num::NonZeroU32, ops::Deref, str::FromStr};
2
3use std::collections::{BTreeMap as Map, BTreeSet as Set};
4
5use crate as chess;
6use crate::{
7    Player,
8    formats::Text,
9    position::{self, Check},
10    square::{File, Rank},
11};
12
13use self::layout::Layout as _;
14
15// #[cfg(feature = "serde")]
16// pub mod storage;
17
18pub mod change;
19pub mod options;
20pub mod traversal;
21
22mod cursor;
23mod layout;
24mod merge;
25pub(in crate::game) mod tree;
26
27/// An error from accessing or editing a game tree.
28#[derive(Debug, thiserror::Error)]
29pub enum Error {
30    #[error("games have different starting positions or modes")]
31    DifferentStart,
32    #[error(transparent)]
33    Start(#[from] start::Error),
34    #[error(transparent)]
35    Traversal(#[from] traversal::Error),
36    #[error(transparent)]
37    Options(#[from] options::Error),
38    #[error(transparent)]
39    Change(#[from] change::Error),
40}
41
42/// A result of editing a game tree.
43pub type Result<T, E = Error> = core::result::Result<T, E>;
44
45/// Errors from the rooted boundary of a game tree.
46pub mod start {
47    /// An error from requesting an incoming move at the start position.
48    #[derive(Debug, thiserror::Error)]
49    #[error("start position has no incoming move")]
50    pub struct Error;
51
52    /// A result of accessing an incoming move.
53    pub type Result<T, E = Error> = core::result::Result<T, E>;
54}
55
56/// A game tree and its game-level data.
57#[derive(Clone, PartialEq)]
58pub struct Game {
59    mode: Mode,
60    changes: Option<Vec<change::Change>>,
61    tree: tree::Tree,
62    public: GamePublic,
63}
64
65/// A position followed by a linear sequence of moves.
66#[derive(Clone, Debug, PartialEq)]
67pub struct Line<P, M> {
68    pub position: P,
69    pub moves: Vec<M>,
70}
71
72/// Public data associated with a game.
73#[derive(Clone, Deserialize, PartialEq, Serialize)]
74pub struct GamePublic {
75    #[serde(default, skip_serializing_if = "macros::is_default")]
76    pub title: Option<Text>,
77    pub orientation: Player,
78    #[serde(default, skip_serializing_if = "macros::is_default")]
79    pub outcome: Outcome,
80    #[serde(default, skip_serializing_if = "macros::is_default")]
81    pub roster: Roster,
82    #[serde(default, skip_serializing_if = "macros::is_default")]
83    pub tags: Vec<Tag>,
84}
85
86impl Default for GamePublic {
87    fn default() -> Self {
88        Self {
89            title: None,
90            orientation: Player::White,
91            outcome: Outcome::default(),
92            roster: Roster::default(),
93            tags: Vec::new(),
94        }
95    }
96}
97
98/// The allocated slot of a position within a game.
99#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, PartialOrd, Eq, Ord, Serialize)]
100#[serde(from = "u32", into = "u32")]
101pub enum PositionSlot {
102    #[default]
103    Start,
104    Move(MoveSlot),
105}
106
107/// The allocated slot of a move within a game.
108#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Eq, Ord, Serialize)]
109#[serde(transparent)]
110pub struct MoveSlot(NonZeroU32);
111
112/// The starting position slot.
113pub const START: PositionSlot = PositionSlot::Start;
114
115/// A position in a game, with its derived, evaluation, and public data.
116#[derive(Clone, PartialEq)]
117pub struct Position {
118    // pointers right
119    options: Vec<MoveSlot>,
120
121    // core datum
122    position: chess::Position,
123
124    // derived data
125    check: Option<Check>,
126    legal: Vec<chess::Move>,
127
128    // evaluation data
129    evaluation: Option<position::Evaluation>,
130
131    // public data
132    pub public: PositionPublic,
133}
134
135/// Public data associated with a position.
136#[derive(Clone, Default, Deserialize, PartialEq, Serialize)]
137pub struct PositionPublic {
138    #[serde(default, skip_serializing_if = "macros::is_default")]
139    pub comment: Option<Text>,
140    #[serde(default, skip_serializing_if = "macros::is_default")]
141    pub expanded: bool,
142}
143
144/// A move in a game, with its derived and public data.
145#[derive(Clone, PartialEq)]
146pub struct Move {
147    // pointer left
148    previous: PositionSlot,
149
150    // core datum
151    play: chess::Move,
152
153    // derived data
154    short: Short,
155
156    pub public: MovePublic,
157}
158
159/// Public data associated with a move.
160#[derive(Clone, Default, Deserialize, PartialEq, Serialize)]
161pub struct MovePublic {
162    #[serde(default, skip_serializing_if = "macros::is_default")]
163    pub affixes: Affixes,
164    #[serde(default, skip_serializing_if = "macros::is_default")]
165    pub commands: Vec<Command>,
166    #[serde(default, skip_serializing_if = "macros::is_default")]
167    pub nags: Vec<Nag>,
168}
169
170/// An immutable reference to a position in a game.
171#[derive(Clone, Copy)]
172pub struct PositionRef<'g> {
173    game: &'g Game,
174    slot: PositionSlot,
175}
176
177/// A mutable reference to a position in a game.
178pub struct PositionMut<'g> {
179    game: &'g mut Game,
180    slot: PositionSlot,
181    protected: Protected,
182}
183
184/// An immutable reference to a move in a game.
185#[derive(Clone, Copy)]
186pub struct MoveRef<'g> {
187    game: &'g Game,
188    slot: MoveSlot,
189}
190
191/// A mutable reference to a move in a game.
192pub struct MoveMut<'g> {
193    game: &'g mut Game,
194    slot: MoveSlot,
195    protected: Protected,
196}
197
198/// A movable position within a game tree.
199#[derive(Clone, PartialEq)]
200pub struct Cursor<G = Game> {
201    game: G,
202    slot: PositionSlot,
203    preserve: Set<PositionSlot>,
204}
205
206/// A cursor over an immutably borrowed game.
207pub type CursorRef<'g> = Cursor<&'g Game>;
208
209/// A cursor over a mutably borrowed game.
210pub type CursorMut<'g> = Cursor<&'g mut Game>;
211
212/// Comments surrounding an option in PGN movetext.
213#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
214pub struct Affixes {
215    #[serde(default, skip_serializing_if = "macros::is_default")]
216    pub intro: Option<Text>,
217    #[serde(default, skip_serializing_if = "macros::is_default")]
218    pub outro: Option<Text>,
219}
220
221#[derive(Default)]
222struct Ambiguity {
223    exists: bool,
224    file: bool,
225    rank: bool,
226}
227
228#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
229pub struct Command {
230    pub command: Text,
231    #[serde(default, skip_serializing_if = "Vec::is_empty")]
232    pub parameters: Vec<String>,
233}
234
235/// A detached, injective mapping from one slot allocation to another.
236///
237/// Moreover, each new move slot is greater than the slot of the position containing it as an option.
238pub struct Reslot(Map<MoveSlot, MoveSlot>);
239
240/// Canonical tree-slot assignment order.
241#[derive(Clone, Copy, Debug, Eq, PartialEq)]
242#[non_exhaustive]
243pub enum Layout {
244    /// The move encounter order used while constructing a game from PGN.
245    ///
246    /// For `1. e4 (1. d4 d5) e5`, slots are assigned to `e4`, `d4`, `d5`, then `e5`
247    /// consecutively as 1, 2, 3, 4.
248    Pgn,
249}
250
251#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
252#[serde(rename_all = "lowercase")]
253pub enum Mode {
254    #[default]
255    Chess,
256    Freestyle,
257}
258
259#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
260#[serde(untagged)]
261pub enum Nag {
262    Numeric(u32),
263    Symbol(String),
264}
265
266#[derive(Clone, Copy, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
267pub enum Outcome {
268    White,
269    Black,
270    Draw,
271    #[default]
272    Unknown,
273}
274
275/// Positions retained by attached mutable values.
276#[derive(Clone)]
277pub struct Protected {
278    pub unremovable: Set<PositionSlot>,
279}
280
281#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)]
282pub struct Roster {
283    #[serde(skip_serializing_if = "Option::is_none")]
284    pub event: Option<Text>,
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub site: Option<Text>,
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub date: Option<Text>,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    pub round: Option<Text>,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub white: Option<Text>,
293    #[serde(skip_serializing_if = "Option::is_none")]
294    pub black: Option<Text>,
295}
296
297#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
298pub struct Short {
299    pub file: Option<File>,
300    pub rank: Option<Rank>,
301}
302
303#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
304pub struct Tag {
305    pub key: Text,
306    pub value: String,
307}
308
309impl Game {
310    pub fn new(position: chess::Position) -> Self {
311        let mode = position.requires_freestyle().into();
312        Self::unchecked_with_mode(position, mode)
313    }
314
315    pub fn with_mode(position: chess::Position, mode: Mode) -> Option<Self> {
316        (mode.is_freestyle() || !position.requires_freestyle())
317            .then(|| Self::unchecked_with_mode(position, mode))
318    }
319
320    pub fn chess(position: chess::Position) -> Option<Self> {
321        Self::with_mode(position, Mode::Chess)
322    }
323
324    pub fn freestyle(position: chess::Position) -> Self {
325        Self::unchecked_with_mode(position, Mode::Freestyle)
326    }
327
328    /// Reassigns move slots in a prescribed layout and returns the old-to-new mapping.
329    ///
330    /// Tracked changes are discarded, as they use the the old slots.
331    /// Change tracking remains enabled or disabled as configured.
332    pub fn reslot(&mut self, layout: Layout) -> Reslot {
333        let reslot = layout.reslot(self);
334        self.tree.reslot(&reslot);
335        if let Some(changes) = self.changes.as_mut() {
336            changes.clear();
337        }
338        reslot
339    }
340
341    pub const fn mode(&self) -> Mode {
342        self.mode
343    }
344
345    pub const fn tracks_changes(&self) -> bool {
346        self.changes.is_some()
347    }
348
349    pub fn track_changes(&mut self, on: bool) {
350        if on {
351            self.changes.get_or_insert_default();
352        } else {
353            self.changes = None;
354        }
355    }
356
357    pub fn changes(&self) -> Option<&[change::Change]> {
358        self.changes.as_deref()
359    }
360
361    pub fn revisions(&self) -> Option<usize> {
362        self.changes.as_ref().map(Vec::len)
363    }
364
365    pub fn clear_evaluations(&mut self) {
366        self.tree.clear_evaluations();
367    }
368
369    pub const fn public(&self) -> &GamePublic {
370        &self.public
371    }
372
373    pub const fn title(&self) -> Option<&Text> {
374        self.public.title.as_ref()
375    }
376
377    pub const fn orientation(&self) -> Player {
378        self.public.orientation
379    }
380
381    pub const fn outcome(&self) -> Outcome {
382        self.public.outcome
383    }
384
385    pub const fn roster(&self) -> &Roster {
386        &self.public.roster
387    }
388
389    pub const fn tags(&self) -> &[Tag] {
390        self.public.tags.as_slice()
391    }
392
393    pub fn set_orientation(&mut self, orientation: Player) {
394        self.update_public(|public| public.orientation = orientation);
395    }
396
397    pub fn set_title(&mut self, title: Option<Text>) {
398        self.update_public(|public| public.title = title);
399    }
400
401    pub fn set_outcome(&mut self, outcome: Outcome) {
402        self.update_public(|public| public.outcome = outcome);
403    }
404
405    pub fn update_roster(&mut self, update: impl FnOnce(&mut Roster)) {
406        self.update_public(|public| update(&mut public.roster));
407    }
408
409    pub fn update_tags(&mut self, update: impl FnOnce(&mut Vec<Tag>)) {
410        self.update_public(|public| update(&mut public.tags));
411    }
412
413    pub fn update_public(&mut self, update: impl FnOnce(&mut GamePublic)) {
414        let before = self.tracks_changes().then(|| self.public.clone());
415        update(&mut self.public);
416        if let Some(before) = before
417            && before != self.public
418        {
419            self.track(change::Patch::Game(Box::new(before)));
420        }
421    }
422
423    fn unchecked_with_mode(position: chess::Position, mode: Mode) -> Self {
424        Self::unchecked_with_mode_next(position, mode, MoveSlot::MIN)
425    }
426
427    fn unchecked_with_mode_next(position: chess::Position, mode: Mode, next: MoveSlot) -> Self {
428        Self {
429            mode,
430            changes: None,
431            tree: tree::Tree::new(position, next),
432            public: Default::default(),
433        }
434    }
435}
436
437impl Position {
438    fn new(position: chess::Position) -> Self {
439        let legal = position.legal_plays();
440        let check = Check::new(position.is_check(), legal.is_empty());
441
442        Self {
443            options: Vec::new(),
444            position,
445            check,
446            legal,
447            evaluation: None,
448            public: PositionPublic::default(),
449        }
450    }
451
452    pub const fn options(&self) -> &[MoveSlot] {
453        self.options.as_slice()
454    }
455
456    pub const fn position(&self) -> chess::Position {
457        self.position
458    }
459
460    pub fn comment(&self) -> Option<&Text> {
461        self.public.comment.as_ref()
462    }
463
464    pub const fn evaluation(&self) -> Option<&position::Evaluation> {
465        self.evaluation.as_ref()
466    }
467
468    pub const fn expanded(&self) -> bool {
469        self.public.expanded
470    }
471
472    pub const fn legal(&self) -> &[chess::Move] {
473        self.legal.as_slice()
474    }
475
476    pub const fn check(&self) -> Option<Check> {
477        self.check
478    }
479
480    pub const fn is_terminal(&self) -> bool {
481        self.legal.is_empty()
482    }
483
484    pub const fn terminal(&self) -> Option<position::Terminal> {
485        position::Terminal::new(self.check.is_some(), self.is_terminal())
486    }
487
488    pub const fn is_checkmate(&self) -> bool {
489        matches!(self.terminal(), Some(position::Terminal::Checkmate))
490    }
491
492    pub const fn is_stalemate(&self) -> bool {
493        matches!(self.terminal(), Some(position::Terminal::Stalemate))
494    }
495}
496
497impl Deref for Position {
498    type Target = chess::Position;
499
500    fn deref(&self) -> &chess::Position {
501        &self.position
502    }
503}
504
505impl Move {
506    pub const fn affixes(&self) -> &Affixes {
507        &self.public.affixes
508    }
509
510    pub const fn commands(&self) -> &[Command] {
511        self.public.commands.as_slice()
512    }
513
514    pub const fn nags(&self) -> &[Nag] {
515        self.public.nags.as_slice()
516    }
517
518    pub const fn previous(&self) -> PositionSlot {
519        self.previous
520    }
521
522    pub const fn play(&self) -> chess::Move {
523        self.play
524    }
525
526    pub const fn short(&self) -> Short {
527        self.short
528    }
529}
530
531impl Deref for Move {
532    type Target = chess::Move;
533
534    fn deref(&self) -> &chess::Move {
535        &self.play
536    }
537}
538
539impl Ambiguity {
540    fn consider(mut self, play: chess::Move, other: chess::Move) -> Self {
541        self.exists = true;
542        self.file |= other.from.file() == play.from.file();
543        self.rank |= other.from.rank() == play.from.rank();
544        self
545    }
546
547    fn file_resolves(&self) -> bool {
548        self.exists && !self.file
549    }
550
551    fn rank_resolves(&self) -> bool {
552        self.exists && !self.rank
553    }
554}
555
556impl Command {
557    pub fn expanded() -> Self {
558        Self {
559            command: Text::new("cgexp").expect("literal is non-empty"),
560            parameters: vec!["true".to_string()],
561        }
562    }
563
564    pub fn is_expanded(&self) -> bool {
565        self.command.as_ref() == "cgexp"
566            && self.parameters.len() == 1
567            && self.parameters[0] == "true"
568    }
569}
570
571impl Mode {
572    pub const fn is_freestyle(self) -> bool {
573        matches!(self, Self::Freestyle)
574    }
575}
576
577impl From<bool> for Mode {
578    fn from(freestyle: bool) -> Self {
579        if freestyle { Self::Freestyle } else { Self::Chess }
580    }
581}
582
583impl PositionSlot {
584    pub const fn is_start(&self) -> bool {
585        matches!(self, Self::Start)
586    }
587}
588
589impl MoveSlot {
590    pub const MIN: Self = Self(NonZeroU32::MIN);
591    pub const MAX: Self = Self(NonZeroU32::MAX);
592
593    pub const fn next(self) -> Option<Self> {
594        match self.0.checked_add(1) {
595            Some(slot) => Some(Self(slot)),
596            None => None,
597        }
598    }
599
600    pub const fn prev(self) -> Option<Self> {
601        match NonZeroU32::new(self.0.get() - 1) {
602            Some(slot) => Some(Self(slot)),
603            None => None,
604        }
605    }
606
607    /// Maps iterator.enumerate() indices 0, 1, 2...
608    /// to Self::MIN = 1, Self::MIN + 1 = 2, etc.
609    pub fn from_index(index: usize) -> Option<Self> {
610        let slot = u32::try_from(index).ok()?.checked_add(1)?;
611        Some(Self(NonZeroU32::new(slot)?))
612    }
613}
614
615impl fmt::Display for MoveSlot {
616    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617        write!(f, "{}", self.0)
618    }
619}
620
621impl FromStr for MoveSlot {
622    type Err = <NonZeroU32 as FromStr>::Err;
623
624    fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
625        s.parse().map(Self)
626    }
627}
628
629impl fmt::Display for PositionSlot {
630    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
631        write!(f, "{}", u32::from(*self))
632    }
633}
634
635impl FromStr for PositionSlot {
636    type Err = <u32 as FromStr>::Err;
637
638    fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
639        s.parse::<u32>().map(Self::from)
640    }
641}
642
643impl From<MoveSlot> for PositionSlot {
644    fn from(slot: MoveSlot) -> Self {
645        Self::Move(slot)
646    }
647}
648
649impl From<NonZeroU32> for MoveSlot {
650    fn from(slot: NonZeroU32) -> Self {
651        Self(slot)
652    }
653}
654
655impl From<MoveSlot> for NonZeroU32 {
656    fn from(slot: MoveSlot) -> Self {
657        slot.0
658    }
659}
660
661impl From<u32> for PositionSlot {
662    fn from(slot: u32) -> Self {
663        match NonZeroU32::new(slot) {
664            Some(slot) => Self::Move(MoveSlot(slot)),
665            None => Self::Start,
666        }
667    }
668}
669
670impl From<PositionSlot> for u32 {
671    fn from(slot: PositionSlot) -> Self {
672        match slot {
673            PositionSlot::Move(slot) => slot.0.get(),
674            PositionSlot::Start => 0,
675        }
676    }
677}
678
679impl Reslot {
680    pub fn len(&self) -> usize {
681        self.0.len()
682    }
683
684    pub fn is_empty(&self) -> bool {
685        self.0.is_empty()
686    }
687
688    pub fn iter(&self) -> impl Iterator<Item = (MoveSlot, MoveSlot)> + '_ {
689        self.0.iter().map(|(&before, &after)| (before, after))
690    }
691
692    pub fn play(&self, slot: MoveSlot) -> Option<MoveSlot> {
693        self.0.get(&slot).copied()
694    }
695
696    pub fn position(&self, slot: PositionSlot) -> Option<PositionSlot> {
697        match slot {
698            PositionSlot::Move(slot) => self.play(slot).map(Into::into),
699            PositionSlot::Start => Some(PositionSlot::Start),
700        }
701    }
702}
703
704impl Default for Protected {
705    fn default() -> Self {
706        Self::new()
707    }
708}
709
710impl Protected {
711    pub const fn new() -> Self {
712        Self { unremovable: Set::new() }
713    }
714
715    pub fn with_unremovable(&self, slot: PositionSlot) -> Self {
716        let mut protected = self.clone();
717        protected.unremovable.insert(slot);
718        protected
719    }
720}
721
722impl Short {
723    pub fn new(legal: &[chess::Move], play: chess::Move) -> Self {
724        let mut short = Short::default();
725
726        if play.role == crate::Role::Pawn || play.is_castle() {
727            return short;
728        }
729
730        let different_play = |other: &chess::Move| *other != play;
731        let same_role_and_to = |other: &chess::Move| (other.role, other.to) == (play.role, play.to);
732        let ambiguity = legal
733            .iter()
734            .copied()
735            .filter(different_play)
736            .filter(same_role_and_to)
737            .fold(Ambiguity::default(), |ambiguity, other| ambiguity.consider(play, other));
738
739        if ambiguity.file_resolves() {
740            short.file = Some(play.from.file());
741        } else if ambiguity.rank_resolves() {
742            short.rank = Some(play.from.rank());
743        } else if ambiguity.exists {
744            short.file = Some(play.from.file());
745            short.rank = Some(play.from.rank());
746        }
747
748        short
749    }
750}
751
752#[cfg(test)]
753mod tests {
754    use super::*;
755
756    #[test]
757    fn start_sorts_before_moves() {
758        // Tree reslot derives the next move slot from the greatest position slot.
759        assert!(PositionSlot::Start < PositionSlot::Move(MoveSlot::MIN));
760    }
761
762    #[test]
763    fn position_slots_are_u32s() {
764        for (position, integer) in [
765            (PositionSlot::Start, 0),
766            (PositionSlot::Move(MoveSlot::MIN), 1),
767            (PositionSlot::Move(MoveSlot::MAX), u32::MAX),
768        ] {
769            assert_eq!(PositionSlot::from(integer), position);
770            assert_eq!(u32::from(position), integer);
771        }
772    }
773
774    #[test]
775    fn move_slots_are_nonzero_u32s() {
776        for integer in [NonZeroU32::MIN, NonZeroU32::MAX] {
777            let slot = MoveSlot::from(integer);
778            assert_eq!(NonZeroU32::from(slot), integer);
779        }
780    }
781
782    #[test]
783    fn slots_are_numeric_text() {
784        let play = MoveSlot::from_index(21).unwrap();
785
786        assert_eq!(play.to_string(), "22");
787        assert_eq!("22".parse::<MoveSlot>().unwrap(), play);
788        assert!("0".parse::<MoveSlot>().is_err());
789
790        assert_eq!(PositionSlot::Start.to_string(), "0");
791        assert_eq!("0".parse::<PositionSlot>().unwrap(), PositionSlot::Start);
792        assert_eq!(PositionSlot::Move(play).to_string(), "22");
793        assert_eq!("22".parse::<PositionSlot>().unwrap(), PositionSlot::Move(play));
794    }
795
796    #[test]
797    fn slots_are_json_numbers() {
798        let play = MoveSlot::from_index(21).unwrap();
799
800        assert_eq!(serde_json::to_string(&play).unwrap(), "22");
801        assert_eq!(serde_json::from_str::<MoveSlot>("22").unwrap(), play);
802
803        assert_eq!(serde_json::to_string(&PositionSlot::Start).unwrap(), "0");
804        assert_eq!(serde_json::from_str::<PositionSlot>("0").unwrap(), PositionSlot::Start);
805        assert_eq!(serde_json::to_string(&PositionSlot::Move(play)).unwrap(), "22");
806        assert_eq!(serde_json::from_str::<PositionSlot>("22").unwrap(), PositionSlot::Move(play));
807    }
808}