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
15pub mod change;
19pub mod options;
20pub mod traversal;
21
22mod cursor;
23mod layout;
24mod merge;
25pub(in crate::game) mod tree;
26
27#[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
42pub type Result<T, E = Error> = core::result::Result<T, E>;
44
45pub mod start {
47 #[derive(Debug, thiserror::Error)]
49 #[error("start position has no incoming move")]
50 pub struct Error;
51
52 pub type Result<T, E = Error> = core::result::Result<T, E>;
54}
55
56#[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#[derive(Clone, Debug, PartialEq)]
67pub struct Line<P, M> {
68 pub position: P,
69 pub moves: Vec<M>,
70}
71
72#[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#[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#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Eq, Ord, Serialize)]
109#[serde(transparent)]
110pub struct MoveSlot(NonZeroU32);
111
112pub const START: PositionSlot = PositionSlot::Start;
114
115#[derive(Clone, PartialEq)]
117pub struct Position {
118 options: Vec<MoveSlot>,
120
121 position: chess::Position,
123
124 check: Option<Check>,
126 legal: Vec<chess::Move>,
127
128 evaluation: Option<position::Evaluation>,
130
131 pub public: PositionPublic,
133}
134
135#[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#[derive(Clone, PartialEq)]
146pub struct Move {
147 previous: PositionSlot,
149
150 play: chess::Move,
152
153 short: Short,
155
156 pub public: MovePublic,
157}
158
159#[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#[derive(Clone, Copy)]
172pub struct PositionRef<'g> {
173 game: &'g Game,
174 slot: PositionSlot,
175}
176
177pub struct PositionMut<'g> {
179 game: &'g mut Game,
180 slot: PositionSlot,
181 protected: Protected,
182}
183
184#[derive(Clone, Copy)]
186pub struct MoveRef<'g> {
187 game: &'g Game,
188 slot: MoveSlot,
189}
190
191pub struct MoveMut<'g> {
193 game: &'g mut Game,
194 slot: MoveSlot,
195 protected: Protected,
196}
197
198#[derive(Clone, PartialEq)]
200pub struct Cursor<G = Game> {
201 game: G,
202 slot: PositionSlot,
203 preserve: Set<PositionSlot>,
204}
205
206pub type CursorRef<'g> = Cursor<&'g Game>;
208
209pub type CursorMut<'g> = Cursor<&'g mut Game>;
211
212#[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
235pub struct Reslot(Map<MoveSlot, MoveSlot>);
239
240#[derive(Clone, Copy, Debug, Eq, PartialEq)]
242#[non_exhaustive]
243pub enum Layout {
244 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#[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 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 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 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}