owlchess_base/
types.rs

1//! Core chess types
2
3use derive_more::Display;
4use std::{fmt, hint, str::FromStr};
5use thiserror::Error;
6
7/// Error when parsing [`Coord`] from string
8#[derive(Error, Debug, Clone, PartialEq, Eq)]
9pub enum CoordParseError {
10    /// Unexpected character for file coordinate
11    #[error("unexpected file char {0:?}")]
12    UnexpectedFileChar(char),
13    /// Unexpected character for rank coordinate
14    #[error("unexpected rank char {0:?}")]
15    UnexpectedRankChar(char),
16    /// Invalid string length
17    #[error("invalid string length")]
18    BadLength,
19}
20
21/// Error when parsing [`Cell`] from string
22#[derive(Error, Debug, Clone, PartialEq, Eq)]
23pub enum CellParseError {
24    /// Unexpected character
25    #[error("unexpected cell char {0:?}")]
26    UnexpectedChar(char),
27    /// Invalid string length
28    #[error("invalid string length")]
29    BadLength,
30}
31
32/// Error when parsing [`Color`] from string
33#[derive(Error, Debug, Clone, PartialEq, Eq)]
34pub enum ColorParseError {
35    /// Unexpected character
36    #[error("unexpected color char {0:?}")]
37    UnexpectedChar(char),
38    /// Invalid string length
39    #[error("invalid string length")]
40    BadLength,
41}
42
43/// Error when parsing [`CastlingRights`] from string
44#[derive(Error, Debug, Clone, PartialEq, Eq)]
45pub enum CastlingRightsParseError {
46    /// Unexpected character
47    #[error("unexpected char {0:?}")]
48    UnexpectedChar(char),
49    /// Duplicate character
50    #[error("duplicate char {0:?}")]
51    DuplicateChar(char),
52    /// The string is empty
53    #[error("the string is empty")]
54    EmptyString,
55}
56
57/// File (i. e. a vertical line) on a chess board
58#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
59#[repr(u8)]
60pub enum File {
61    A = 0,
62    B = 1,
63    C = 2,
64    D = 3,
65    E = 4,
66    F = 5,
67    G = 6,
68    H = 7,
69}
70
71impl File {
72    /// Returns a numeric index of the current file
73    ///
74    /// The files are numbered from left to right, i.e. file A has index 0, and file H has index 7.
75    #[inline]
76    pub const fn index(&self) -> usize {
77        *self as u8 as usize
78    }
79
80    /// Converts a file index to [`File`]
81    ///
82    /// # Safety
83    ///
84    /// The behavior is undefined when `val` is not in range `[0; 8)`.
85    #[inline]
86    pub const unsafe fn from_index_unchecked(val: usize) -> Self {
87        match val {
88            0 => File::A,
89            1 => File::B,
90            2 => File::C,
91            3 => File::D,
92            4 => File::E,
93            5 => File::F,
94            6 => File::G,
95            7 => File::H,
96            _ => hint::unreachable_unchecked(),
97        }
98    }
99
100    /// Converts a file index to [`File`]
101    ///
102    /// # Panics
103    ///
104    /// The function panics when `val` is not in range `[0; 8)`.
105    #[inline]
106    pub const fn from_index(val: usize) -> Self {
107        assert!(val < 8, "file index must be between 0 and 7");
108        unsafe { Self::from_index_unchecked(val) }
109    }
110
111    /// Returns an iterator over all the files, in ascending order of their indices
112    #[inline]
113    pub fn iter() -> impl Iterator<Item = Self> {
114        (0..8).map(|x| unsafe { Self::from_index_unchecked(x) })
115    }
116
117    #[inline]
118    unsafe fn from_char_unchecked(c: char) -> Self {
119        File::from_index_unchecked((u32::from(c) - u32::from('a')) as usize)
120    }
121
122    /// Creates a file from its character representation, if it's valid
123    ///
124    /// If `c` is a valid character representation of file, then the corresponding file is returned.
125    /// Otherwise, returns `None`.
126    ///
127    /// Note that the only valid character representations are lowercase Latin letters from `'a``
128    /// to `'h'` inclusively.
129    ///
130    /// # Example
131    ///
132    /// ```
133    /// # use owlchess_base::types::File;
134    /// #
135    /// assert_eq!(File::from_char('a'), Some(File::A));
136    /// assert_eq!(File::from_char('e'), Some(File::E));
137    /// assert_eq!(File::from_char('q'), None);
138    /// assert_eq!(File::from_char('A'), None);
139    /// ```
140    #[inline]
141    pub fn from_char(c: char) -> Option<Self> {
142        match c {
143            'a'..='h' => Some(unsafe { Self::from_char_unchecked(c) }),
144            _ => None,
145        }
146    }
147
148    /// Converts a file into its character representation
149    ///
150    /// # Example
151    ///
152    /// ```
153    /// # use owlchess_base::types::File;
154    /// #
155    /// assert_eq!(File::A.as_char(), 'a');
156    /// assert_eq!(File::E.as_char(), 'e');
157    /// ```
158    #[inline]
159    pub fn as_char(&self) -> char {
160        (b'a' + *self as u8) as char
161    }
162}
163
164impl fmt::Display for File {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
166        write!(f, "{}", self.as_char())
167    }
168}
169
170/// Rank (i. e. a horizontal line) on a chess board
171#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
172#[repr(u8)]
173pub enum Rank {
174    R8 = 0,
175    R7 = 1,
176    R6 = 2,
177    R5 = 3,
178    R4 = 4,
179    R3 = 5,
180    R2 = 6,
181    R1 = 7,
182}
183
184impl Rank {
185    /// Returns a numeric index of the current rank
186    ///
187    /// The ranks are numbered from top to bottom, i.e. rank 8 has index 0, and rank 1 has index 7.
188    #[inline]
189    pub const fn index(&self) -> usize {
190        *self as u8 as usize
191    }
192
193    /// Converts a rank index to [`Rank`]
194    ///
195    /// # Safety
196    ///
197    /// The behavior is undefined when `val` is not in range `[0; 8)`.
198    #[inline]
199    pub const unsafe fn from_index_unchecked(val: usize) -> Self {
200        match val {
201            0 => Rank::R8,
202            1 => Rank::R7,
203            2 => Rank::R6,
204            3 => Rank::R5,
205            4 => Rank::R4,
206            5 => Rank::R3,
207            6 => Rank::R2,
208            7 => Rank::R1,
209            _ => hint::unreachable_unchecked(),
210        }
211    }
212
213    /// Converts a rank index to [`Rank`]
214    ///
215    /// # Panics
216    ///
217    /// The function panics when `val` is not in range `[0; 8)`.
218    #[inline]
219    pub const fn from_index(val: usize) -> Self {
220        assert!(val < 8, "rank index must be between 0 and 7");
221        unsafe { Self::from_index_unchecked(val) }
222    }
223
224    /// Returns an iterator over all the ranks, in ascending order of their indices
225    #[inline]
226    pub fn iter() -> impl Iterator<Item = Self> {
227        (0..8).map(|x| unsafe { Self::from_index_unchecked(x) })
228    }
229
230    #[inline]
231    unsafe fn from_char_unchecked(c: char) -> Self {
232        Rank::from_index_unchecked((u32::from('8') - u32::from(c)) as usize)
233    }
234
235    /// Creates a rank from its character representation, if it's valid
236    ///
237    /// If `c` is a valid character representation of rank, then the corresponding rank is returned.
238    /// Otherwise, returns `None`.
239    ///
240    /// # Example
241    ///
242    /// ```
243    /// # use owlchess_base::types::Rank;
244    /// #
245    /// assert_eq!(Rank::from_char('1'), Some(Rank::R1));
246    /// assert_eq!(Rank::from_char('5'), Some(Rank::R5));
247    /// assert_eq!(Rank::from_char('9'), None);
248    /// assert_eq!(Rank::from_char('A'), None);
249    /// ```
250    #[inline]
251    pub fn from_char(c: char) -> Option<Self> {
252        match c {
253            '1'..='8' => Some(unsafe { Self::from_char_unchecked(c) }),
254            _ => None,
255        }
256    }
257    /// Converts a rank into its character representation
258    ///
259    /// # Example
260    ///
261    /// ```
262    /// # use owlchess_base::types::Rank;
263    /// #
264    /// assert_eq!(Rank::R1.as_char(), '1');
265    /// assert_eq!(Rank::R5.as_char(), '5');
266    /// ```
267    #[inline]
268    pub fn as_char(&self) -> char {
269        (b'8' - *self as u8) as char
270    }
271}
272
273impl fmt::Display for Rank {
274    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
275        write!(f, "{}", self.as_char())
276    }
277}
278
279/// Coordinate of a square
280#[derive(Copy, Clone, PartialEq, Eq, Hash)]
281pub struct Coord(u8);
282
283impl Coord {
284    /// Creates a coordinate from its index
285    ///
286    /// See [`Coord::index()`] for the details about index assignment.
287    ///
288    /// # Panics
289    ///
290    /// This function panics if `val` is not a valid index.
291    #[inline]
292    pub const fn from_index(val: usize) -> Coord {
293        assert!(val < 64, "coord must be between 0 and 63");
294        Coord(val as u8)
295    }
296
297    /// Creates a coordinate from its index
298    ///
299    /// See [`Coord::index()`] for the details about index assignment.
300    ///
301    /// # Safety
302    ///
303    /// The behavior is undefined if `val` is not a valid index.
304    #[inline]
305    pub const unsafe fn from_index_unchecked(val: usize) -> Coord {
306        Coord(val as u8)
307    }
308
309    /// Creates a square coordinate from the given file and rank
310    #[inline]
311    pub const fn from_parts(file: File, rank: Rank) -> Coord {
312        Coord(((rank as u8) << 3) | file as u8)
313    }
314
315    /// Returns the file on which the square is located
316    #[inline]
317    pub const fn file(&self) -> File {
318        unsafe { File::from_index_unchecked((self.0 & 7) as usize) }
319    }
320
321    /// Returns the rank on which the square is located
322    #[inline]
323    pub const fn rank(&self) -> Rank {
324        unsafe { Rank::from_index_unchecked((self.0 >> 3) as usize) }
325    }
326
327    /// Returns the index of the square
328    ///
329    /// The indices are assigned in a big-endian rank-file manner:
330    ///
331    /// ```notrust
332    /// 8 |  0  1  2  3  4  5  6  7
333    /// 7 |  8  9 10 11 12 13 14 15
334    /// 6 | 16 17 18 19 20 21 22 23
335    /// 5 | 24 25 26 27 28 29 30 31
336    /// 4 | 32 33 34 35 36 37 38 39
337    /// 3 | 40 41 42 43 44 45 46 47
338    /// 2 | 48 49 50 51 52 53 54 55
339    /// 1 | 56 57 58 59 60 61 62 63
340    /// --+------------------------
341    ///   |  a  b  c  d  e  f  g  h
342    /// ```
343    #[inline]
344    pub const fn index(&self) -> usize {
345        self.0 as usize
346    }
347
348    /// Flips the square vertically
349    ///
350    /// # Example
351    ///
352    /// ```
353    /// # use owlchess_base::types::{File, Rank, Coord};
354    /// #
355    /// let c3 = Coord::from_parts(File::C, Rank::R3);
356    /// let c6 = Coord::from_parts(File::C, Rank::R6);
357    /// assert_eq!(c3.flipped_rank(), c6);
358    /// assert_eq!(c6.flipped_rank(), c3);
359    /// ```
360    #[inline]
361    pub const fn flipped_rank(self) -> Coord {
362        Coord(self.0 ^ 56)
363    }
364
365    /// Flips the square horizontally
366    ///
367    /// # Example
368    ///
369    /// ```
370    /// # use owlchess_base::types::{File, Rank, Coord};
371    /// #
372    /// let c3 = Coord::from_parts(File::C, Rank::R3);
373    /// let f3 = Coord::from_parts(File::F, Rank::R3);
374    /// assert_eq!(c3.flipped_file(), f3);
375    /// assert_eq!(f3.flipped_file(), c3);
376    /// ```
377    #[inline]
378    pub const fn flipped_file(self) -> Coord {
379        Coord(self.0 ^ 7)
380    }
381
382    /// Returns the index of diagonal on which the square is located
383    ///
384    /// Diagonals have indices in range `[0; 15)`, where h1-h1 diagonal has index 0
385    /// and a8-a8 diagonal has index 14. The main diagonal a1-h8 has index 7.
386    #[inline]
387    pub const fn diag(&self) -> usize {
388        self.file().index() + self.rank().index()
389    }
390
391    /// Returns the index of antidiagonal on which the square is located
392    ///
393    /// Antidiagonals have indices in range `[0; 15)`, where a1-a1 antidiagonal has index 0
394    /// and h8-h8 antidiagonal has index 14. The main antidiagonal a8-h1 has index 7.
395    #[inline]
396    pub const fn antidiag(&self) -> usize {
397        7 - self.rank().index() + self.file().index()
398    }
399
400    /// Adds `delta` to the index of the coordinate
401    ///
402    /// # Panics
403    ///
404    /// The function panics if the index is invalid (i.e. not in range `[0; 64)`) after
405    /// such addition.
406    #[inline]
407    pub const fn add(self, delta: isize) -> Coord {
408        Coord::from_index(self.index().wrapping_add(delta as usize))
409    }
410
411    /// Adds `delta` to the index of the coordinate
412    ///
413    /// # Safety
414    ///
415    /// The behavior is undefined if the index is invalid (i.e. not in range `[0; 64)`)
416    /// after such addition.
417    #[inline]
418    pub const unsafe fn add_unchecked(self, delta: isize) -> Coord {
419        Coord::from_index_unchecked(self.index().wrapping_add(delta as usize))
420    }
421
422    /// Adds `delta_file` to the file index and `delta_rank` to the rank index.
423    /// If either file index or rank index becomes invalid, returns `None`, otherwise
424    /// the new file and rank are gathered into a new [`Coord`], which is returned
425    ///
426    /// # Example
427    ///
428    /// ```
429    /// # use owlchess_base::types::{File, Rank, Coord};
430    /// #
431    /// let b3 = Coord::from_parts(File::B, Rank::R3);
432    /// let e4 = Coord::from_parts(File::E, Rank::R4);
433    /// assert_eq!(b3.shift(-3, 1), None);
434    /// assert_eq!(b3.shift(3, -1), Some(e4));
435    /// ```
436    #[inline]
437    pub fn shift(self, delta_file: isize, delta_rank: isize) -> Option<Coord> {
438        let new_file = self.file().index().wrapping_add(delta_file as usize);
439        let new_rank = self.rank().index().wrapping_add(delta_rank as usize);
440        if new_file >= 8 || new_rank >= 8 {
441            return None;
442        }
443        unsafe {
444            Some(Coord::from_parts(
445                File::from_index_unchecked(new_file),
446                Rank::from_index_unchecked(new_rank),
447            ))
448        }
449    }
450
451    /// Iterates over all possible coordinates in ascending order of their indices
452    #[inline]
453    pub fn iter() -> impl Iterator<Item = Self> {
454        (0_u8..64_u8).map(Coord)
455    }
456}
457
458impl fmt::Debug for Coord {
459    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
460        if self.0 < 64 {
461            return write!(f, "Coord({})", self);
462        }
463        write!(f, "Coord(?{:?})", self.0)
464    }
465}
466
467impl fmt::Display for Coord {
468    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
469        write!(f, "{}{}", self.file().as_char(), self.rank().as_char())
470    }
471}
472
473impl FromStr for Coord {
474    type Err = CoordParseError;
475
476    fn from_str(s: &str) -> Result<Self, Self::Err> {
477        if s.len() != 2 {
478            return Err(CoordParseError::BadLength);
479        }
480        let bytes = s.as_bytes();
481        let (file_ch, rank_ch) = (bytes[0] as char, bytes[1] as char);
482        Ok(Coord::from_parts(
483            File::from_char(file_ch).ok_or(CoordParseError::UnexpectedFileChar(file_ch))?,
484            Rank::from_char(rank_ch).ok_or(CoordParseError::UnexpectedRankChar(rank_ch))?,
485        ))
486    }
487}
488
489/// Color of chess pieces (either white or black)
490#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
491#[repr(u8)]
492pub enum Color {
493    White = 0,
494    Black = 1,
495}
496
497impl Color {
498    /// Returns the opposite color
499    #[inline]
500    pub const fn inv(&self) -> Color {
501        match *self {
502            Color::White => Color::Black,
503            Color::Black => Color::White,
504        }
505    }
506
507    /// Returns a character representation of the color
508    ///
509    /// The character representation is `"w"` for white, and `"b"` for black.
510    #[inline]
511    pub fn as_char(&self) -> char {
512        match *self {
513            Color::White => 'w',
514            Color::Black => 'b',
515        }
516    }
517
518    /// Creates a color from its character representation
519    ///
520    /// If `c` is not a valid character representation of color, returns `None`.
521    #[inline]
522    pub fn from_char(c: char) -> Option<Color> {
523        match c {
524            'w' => Some(Color::White),
525            'b' => Some(Color::Black),
526            _ => None,
527        }
528    }
529
530    /// Returns a full string representation of the color (either `"white"` or `"black"`)
531    #[inline]
532    pub fn as_long_str(&self) -> &'static str {
533        match *self {
534            Color::White => "white",
535            Color::Black => "black",
536        }
537    }
538}
539
540impl fmt::Display for Color {
541    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
542        write!(f, "{}", self.as_char())
543    }
544}
545
546impl FromStr for Color {
547    type Err = ColorParseError;
548
549    fn from_str(s: &str) -> Result<Self, Self::Err> {
550        if s.len() != 1 {
551            return Err(ColorParseError::BadLength);
552        }
553        let ch = s.as_bytes()[0] as char;
554        Color::from_char(s.as_bytes()[0] as char).ok_or(ColorParseError::UnexpectedChar(ch))
555    }
556}
557
558/// Kind of chess pieces (without regard to piece color)
559#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
560#[repr(u8)]
561pub enum Piece {
562    Pawn = 0,
563    King = 1,
564    Knight = 2,
565    Bishop = 3,
566    Rook = 4,
567    Queen = 5,
568}
569
570impl Piece {
571    /// Number of different possible indices of [`Piece`]
572    ///
573    /// It exceeds maximum possible index by one.
574    pub const COUNT: usize = 6;
575
576    /// Returns a numeric index of the current piece
577    #[inline]
578    pub const fn index(&self) -> usize {
579        *self as u8 as usize
580    }
581
582    /// Converts a piece index to [`Piece`]
583    ///
584    /// # Safety
585    ///
586    /// The behavior is undefined if the index is invalid (i.e. it is greater or equal than [`Piece::COUNT`])
587    #[inline]
588    pub const unsafe fn from_index_unchecked(val: usize) -> Self {
589        match val {
590            0 => Self::Pawn,
591            1 => Self::King,
592            2 => Self::Knight,
593            3 => Self::Bishop,
594            4 => Self::Rook,
595            5 => Self::Queen,
596            _ => hint::unreachable_unchecked(),
597        }
598    }
599
600    /// Converts a piece index to [`Piece`]
601    ///
602    /// # Panics
603    ///
604    /// The function panics if the index is invalid (i.e. it is greater or equal than [`Piece::COUNT`])
605    #[inline]
606    pub const fn from_index(val: usize) -> Self {
607        assert!(val < Self::COUNT, "piece index must be between 0 and 5");
608        unsafe { Self::from_index_unchecked(val) }
609    }
610
611    /// Returns an iterator over all the pieces, in ascending order of their indices
612    #[inline]
613    pub fn iter() -> impl Iterator<Item = Self> {
614        (0..Self::COUNT).map(|x| unsafe { Self::from_index_unchecked(x) })
615    }
616}
617
618/// Contents of square on a chess board
619///
620/// A square can be either empty or contain a piece of some given color.
621///
622/// This type is one compact and is only one byte long to facilitate compact chess board
623/// representation.
624#[derive(Default, Copy, Clone, PartialEq, Eq, Hash)]
625pub struct Cell(u8);
626
627impl Cell {
628    /// [`Cell`] without any pieces
629    pub const EMPTY: Cell = Cell(0);
630
631    /// Number of different possible indices of [`Cell`]
632    ///
633    /// It exceeds maximum possible index by one.
634    pub const COUNT: usize = 13;
635
636    /// Returns `true` if the cell doesn't contain any pieces
637    #[inline]
638    pub const fn is_free(&self) -> bool {
639        self.0 == 0
640    }
641
642    /// Returns `true` if the cell contains a piece
643    #[inline]
644    pub const fn is_occupied(&self) -> bool {
645        self.0 != 0
646    }
647
648    /// Creates a cell from its index
649    ///
650    /// # Safety
651    ///
652    /// The behavior is undefined if the index is invalid (i.e. it is greater or equal than [`Cell::COUNT`])
653    #[inline]
654    pub const unsafe fn from_index_unchecked(val: usize) -> Cell {
655        Cell(val as u8)
656    }
657
658    /// Creates a cell from its index
659    ///
660    /// # Panics
661    ///
662    /// The function panics if the index is invalid (i.e. it is greater or equal than [`Cell::COUNT`])
663    #[inline]
664    pub const fn from_index(val: usize) -> Cell {
665        assert!(val < Self::COUNT, "index too large");
666        Cell(val as u8)
667    }
668
669    /// Returns the index of the cell
670    ///
671    /// Cell indices are stable between updates, and changing the index of some given cell
672    /// is considered API breakage.
673    #[inline]
674    pub const fn index(&self) -> usize {
675        self.0 as usize
676    }
677
678    /// Creates a cell with a piece `p` of color `c`
679    #[inline]
680    pub const fn from_parts(c: Color, p: Piece) -> Cell {
681        Cell(match c {
682            Color::White => 1 + p as u8,
683            Color::Black => 7 + p as u8,
684        })
685    }
686
687    /// Returns the color of the piece on the cell
688    ///
689    /// If the cell is empty, returns `None`.
690    #[inline]
691    pub const fn color(&self) -> Option<Color> {
692        match self.0 {
693            0 => None,
694            1..=6 => Some(Color::White),
695            _ => Some(Color::Black),
696        }
697    }
698
699    /// Returns the kind of the piece on the cell
700    ///
701    /// If the cell is empty, returns `None`.
702    #[inline]
703    pub const fn piece(&self) -> Option<Piece> {
704        match self.0 {
705            0 => None,
706            1 | 7 => Some(Piece::Pawn),
707            2 | 8 => Some(Piece::King),
708            3 | 9 => Some(Piece::Knight),
709            4 | 10 => Some(Piece::Bishop),
710            5 | 11 => Some(Piece::Rook),
711            6 | 12 => Some(Piece::Queen),
712            _ => unsafe { hint::unreachable_unchecked() },
713        }
714    }
715
716    /// Iterates over all possible cells in ascending order of their indices
717    #[inline]
718    pub fn iter() -> impl Iterator<Item = Self> {
719        (0..Self::COUNT).map(|x| unsafe { Self::from_index_unchecked(x) })
720    }
721
722    /// Returns a character representation of the cell
723    ///
724    /// Unlike [`Cell::as_utf8_char`], the representation is an ASCII character.
725    #[inline]
726    pub fn as_char(&self) -> char {
727        b".PKNBRQpknbrq"[self.0 as usize] as char
728    }
729
730    /// Converts a cell to a corresponding Unicode character
731    #[inline]
732    pub fn as_utf8_char(&self) -> char {
733        [
734            '.', '♙', '♔', '♘', '♗', '♖', '♕', '♟', '♚', '♞', '♝', '♜', '♛',
735        ][self.0 as usize]
736    }
737
738    /// Creates a cell from its character representation
739    ///
740    /// If `c` is not a valid character representation of a cell, returns `None`. Note that
741    /// only ASCII character repesentations as returned by [`Cell::as_char`] are accepted.
742    #[inline]
743    pub fn from_char(c: char) -> Option<Self> {
744        if c == '.' {
745            return Some(Cell::EMPTY);
746        }
747        let color = if c.is_ascii_uppercase() {
748            Color::White
749        } else {
750            Color::Black
751        };
752        let piece = match c.to_ascii_lowercase() {
753            'p' => Piece::Pawn,
754            'k' => Piece::King,
755            'n' => Piece::Knight,
756            'b' => Piece::Bishop,
757            'r' => Piece::Rook,
758            'q' => Piece::Queen,
759            _ => return None,
760        };
761        Some(Cell::from_parts(color, piece))
762    }
763}
764
765impl fmt::Debug for Cell {
766    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
767        if (self.0 as usize) < Self::COUNT {
768            return write!(f, "Cell({})", self.as_char());
769        }
770        write!(f, "Cell(?{:?})", self.0)
771    }
772}
773
774impl fmt::Display for Cell {
775    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
776        write!(f, "{}", self.as_char())
777    }
778}
779
780impl FromStr for Cell {
781    type Err = CellParseError;
782
783    fn from_str(s: &str) -> Result<Self, Self::Err> {
784        if s.len() != 1 {
785            return Err(CellParseError::BadLength);
786        }
787        let ch = s.as_bytes()[0] as char;
788        Cell::from_char(s.as_bytes()[0] as char).ok_or(CellParseError::UnexpectedChar(ch))
789    }
790}
791
792/// Castling side (either queenside or kingside)
793#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
794#[repr(u8)]
795pub enum CastlingSide {
796    /// Queenside castling (a.k.a. O-O-O)
797    Queen = 0,
798    /// Kingside castling (a.k.a. O-O)
799    King = 1,
800}
801
802/// Flags specifying allowed castling sides for both white and black
803#[derive(Default, Copy, Clone, PartialEq, Eq, Hash)]
804pub struct CastlingRights(u8);
805
806impl CastlingRights {
807    #[inline]
808    const fn to_index(c: Color, s: CastlingSide) -> u8 {
809        ((c as u8) << 1) | s as u8
810    }
811
812    #[inline]
813    const fn to_color_mask(c: Color) -> u8 {
814        3 << ((c as u8) << 1)
815    }
816
817    /// Empty castling rights (i.e. castling is not allowed at all)
818    pub const EMPTY: CastlingRights = CastlingRights(0);
819
820    /// Full castling rights (i.e. all possible castlings are allowed)
821    pub const FULL: CastlingRights = CastlingRights(15);
822
823    /// Returns `true` if color `c` is able to perform castling to side `s`
824    #[inline]
825    pub const fn has(&self, c: Color, s: CastlingSide) -> bool {
826        ((self.0 >> Self::to_index(c, s)) & 1) != 0
827    }
828
829    /// Returns `true` if color `c` is able to perform castling to at least one of
830    /// the sides.
831    #[inline]
832    pub const fn has_color(&self, c: Color) -> bool {
833        (self.0 & Self::to_color_mask(c)) != 0
834    }
835
836    /// Adds `s` to allowed castling sides for color `c`
837    #[inline]
838    pub const fn with(self, c: Color, s: CastlingSide) -> CastlingRights {
839        CastlingRights(self.0 | (1_u8 << Self::to_index(c, s)))
840    }
841
842    /// Removes `s` to allowed castling sides for color `c`
843    #[inline]
844    pub const fn without(self, c: Color, s: CastlingSide) -> CastlingRights {
845        CastlingRights(self.0 & !(1_u8 << Self::to_index(c, s)))
846    }
847
848    /// Adds `s` to allowed castling sides for color `c`
849    ///
850    /// Unlike [`CastlingRights::with`], mutates the current object instead of returning
851    /// a new one.
852    #[inline]
853    pub fn set(&mut self, c: Color, s: CastlingSide) {
854        *self = self.with(c, s)
855    }
856
857    /// Removes `s` to allowed castling sides for color `c`
858    ///
859    /// Unlike [`CastlingRights::without`], mutates the current object instead of returning
860    /// a new one.
861    #[inline]
862    pub fn unset(&mut self, c: Color, s: CastlingSide) {
863        *self = self.without(c, s)
864    }
865
866    /// Removes all the castling rights for color `c`
867    #[inline]
868    pub fn unset_color(&mut self, c: Color) {
869        self.unset(c, CastlingSide::King);
870        self.unset(c, CastlingSide::Queen);
871    }
872
873    /// Creates [`CastlingRights`] from index
874    ///
875    /// # Panics
876    ///
877    /// The function panics if `val` is an invalid index.
878    #[inline]
879    pub const fn from_index(val: usize) -> CastlingRights {
880        assert!(val < 16, "raw castling rights must be between 0 and 15");
881        CastlingRights(val as u8)
882    }
883
884    /// Converts [`CastlingRights`] into an index
885    ///
886    /// Indices are stable between updates, and changing the index of some given [`CastlingRights`]
887    /// instance is considered API breakage.
888    #[inline]
889    pub const fn index(&self) -> usize {
890        self.0 as usize
891    }
892}
893
894impl fmt::Debug for CastlingRights {
895    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
896        if self.0 < 16 {
897            return write!(f, "CastlingRights({})", self);
898        }
899        write!(f, "CastlingRights(?{:?})", self.0)
900    }
901}
902
903impl fmt::Display for CastlingRights {
904    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
905        if *self == Self::EMPTY {
906            return write!(f, "-");
907        }
908        if self.has(Color::White, CastlingSide::King) {
909            write!(f, "K")?;
910        }
911        if self.has(Color::White, CastlingSide::Queen) {
912            write!(f, "Q")?;
913        }
914        if self.has(Color::Black, CastlingSide::King) {
915            write!(f, "k")?;
916        }
917        if self.has(Color::Black, CastlingSide::Queen) {
918            write!(f, "q")?;
919        }
920        Ok(())
921    }
922}
923
924impl FromStr for CastlingRights {
925    type Err = CastlingRightsParseError;
926
927    fn from_str(s: &str) -> Result<CastlingRights, Self::Err> {
928        type Error = CastlingRightsParseError;
929        if s == "-" {
930            return Ok(CastlingRights::EMPTY);
931        }
932        if s.is_empty() {
933            return Err(Error::EmptyString);
934        }
935        let mut res = CastlingRights::EMPTY;
936        for b in s.bytes() {
937            let (color, side) = match b {
938                b'K' => (Color::White, CastlingSide::King),
939                b'Q' => (Color::White, CastlingSide::Queen),
940                b'k' => (Color::Black, CastlingSide::King),
941                b'q' => (Color::Black, CastlingSide::Queen),
942                _ => return Err(Error::UnexpectedChar(b as char)),
943            };
944            if res.has(color, side) {
945                return Err(Error::DuplicateChar(b as char));
946            }
947            res.set(color, side);
948        }
949        Ok(res)
950    }
951}
952
953/// Reason for game finish with draw
954#[non_exhaustive]
955#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, Hash)]
956pub enum DrawReason {
957    /// Draw by stalemate
958    #[display(fmt = "stalemate")]
959    Stalemate,
960    /// Draw by insufficient material
961    #[display(fmt = "insufficient material")]
962    InsufficientMaterial,
963    /// Draw by 75 moves
964    ///
965    /// This one is mandatory, in contrast with draw by 50 moves.
966    #[display(fmt = "75 move rule")]
967    Moves75,
968    /// Draw by five-fold repetition
969    ///
970    /// This one is mandatory, in contrast with draw by threefold repetition.
971    #[display(fmt = "fivefold repetition")]
972    Repeat5,
973    /// Draw by 50 moves
974    ///
975    /// According to FIDE rules, one can claim a draw if no player captures a piece or
976    /// makes a pawn move during the last 50 moves, but is not obligated to do so.
977    #[display(fmt = "50 move rule")]
978    Moves50,
979    /// Draw by threefold repetition
980    ///
981    /// In case of threefold repetition, one can claim a draw but is not obligated to do so.
982    #[display(fmt = "threefold repetition")]
983    Repeat3,
984    /// Draw by agreement
985    #[display(fmt = "draw by agreement")]
986    Agreement,
987    /// Reason is unknown
988    #[display(fmt = "draw by unknown reason")]
989    Unknown,
990}
991
992/// Reason for game finish with win
993#[non_exhaustive]
994#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, Hash)]
995pub enum WinReason {
996    /// Game ends with checkmate
997    #[display(fmt = "checkmate")]
998    Checkmate,
999    /// Opponent forfeits on time
1000    #[display(fmt = "opponent forfeits on time")]
1001    TimeForfeit,
1002    /// Opponent made an invalid move
1003    #[display(fmt = "opponent made an invalid move")]
1004    InvalidMove,
1005    /// Opponent is a chess engine and it either violated the protocol or crashed
1006    #[display(fmt = "opponent is a buggy chess engine")]
1007    EngineError,
1008    /// Opponent resigns
1009    #[display(fmt = "opponent resigns")]
1010    Resign,
1011    /// Opponent abandons the game
1012    #[display(fmt = "opponent abandons the game")]
1013    Abandon,
1014    /// Reason is unknown
1015    #[display(fmt = "unknown reason")]
1016    Unknown,
1017}
1018
1019/// Outcome of the finished game
1020#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1021pub enum Outcome {
1022    /// Win (either by White or by Black)
1023    Win {
1024        /// Winning side
1025        side: Color,
1026        /// Reason
1027        reason: WinReason,
1028    },
1029    /// Draw
1030    Draw(DrawReason),
1031}
1032
1033/// Filter to group various types of outcomes
1034#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
1035pub enum OutcomeFilter {
1036    /// Only outcomes with no legal moves are considered (i.e. checkmate and stalemate)
1037    Force,
1038    /// Only outcomes which are mandatorily applied by FIDE rules are considered
1039    ///
1040    /// This includes the following:
1041    /// - checkmate
1042    /// - stalemate
1043    /// - draw by insufficient material
1044    /// - draw by 75 moves
1045    /// - draw by five-fold repetition
1046    Strict,
1047    /// All the outcomes in [`Strict`](OutcomeFilter::Strict) plus the outcomes where
1048    /// player can claim a draw
1049    ///
1050    /// This additionally includes:
1051    /// - draw by 50 moves
1052    /// - draw by threefold repetitions
1053    Relaxed,
1054}
1055
1056impl Outcome {
1057    /// Extracts the winner from the outcome
1058    ///
1059    /// If this is a draw outcome, then `None` is returned
1060    #[inline]
1061    pub fn winner(&self) -> Option<Color> {
1062        match self {
1063            Self::Win { side, .. } => Some(*side),
1064            Self::Draw(_) => None,
1065        }
1066    }
1067
1068    /// Returns `true` if the outcome occured because one of the sides didn't have a legal move
1069    ///
1070    /// Similar to [`Outcome::passes(OutcomeFilter::Force)`](Outcome::passes)
1071    #[inline]
1072    pub fn is_force(&self) -> bool {
1073        matches!(
1074            *self,
1075            Self::Win {
1076                reason: WinReason::Checkmate,
1077                ..
1078            } | Self::Draw(DrawReason::Stalemate)
1079        )
1080    }
1081
1082    /// Returns `true` if the outcome passes filter `filter`
1083    ///
1084    /// See [`OutcomeFilter`] docs for the details about each filter
1085    #[inline]
1086    pub fn passes(&self, filter: OutcomeFilter) -> bool {
1087        if self.is_force() {
1088            return true;
1089        }
1090        if matches!(filter, OutcomeFilter::Strict | OutcomeFilter::Relaxed)
1091            && matches!(
1092                *self,
1093                Self::Draw(
1094                    DrawReason::InsufficientMaterial | DrawReason::Moves75 | DrawReason::Repeat5
1095                )
1096            )
1097        {
1098            return true;
1099        }
1100        matches!(filter, OutcomeFilter::Relaxed)
1101            && matches!(*self, Self::Draw(DrawReason::Moves50 | DrawReason::Repeat3))
1102    }
1103}
1104
1105impl fmt::Display for Outcome {
1106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1107        match self {
1108            Self::Draw(reason) => reason.fmt(f),
1109            Self::Win { side, reason } => match reason {
1110                WinReason::Checkmate => write!(f, "{} checkmates", side.as_long_str()),
1111                WinReason::TimeForfeit => {
1112                    write!(f, "{} forfeits on time", side.inv().as_long_str())
1113                }
1114                WinReason::InvalidMove => {
1115                    write!(f, "{} made an invalid move", side.inv().as_long_str())
1116                }
1117                WinReason::EngineError => {
1118                    write!(f, "{} is a buggy chess engine", side.inv().as_long_str())
1119                }
1120                WinReason::Resign => write!(f, "{} resigns", side.inv().as_long_str()),
1121                WinReason::Abandon => write!(f, "{} abandons the game", side.inv().as_long_str()),
1122                WinReason::Unknown => write!(f, "{} wins by unknown reason", side.as_long_str()),
1123            },
1124        }
1125    }
1126}
1127
1128/// Short status of the game (either running of finished)
1129#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, Hash)]
1130pub enum GameStatus {
1131    /// White wins
1132    #[display(fmt = "1-0")]
1133    White,
1134    /// Black wins
1135    #[display(fmt = "0-1")]
1136    Black,
1137    /// Draw
1138    #[display(fmt = "1/2-1/2")]
1139    Draw,
1140    /// Game is still running
1141    #[display(fmt = "*")]
1142    Running,
1143}
1144
1145impl From<Option<Outcome>> for GameStatus {
1146    #[inline]
1147    fn from(src: Option<Outcome>) -> Self {
1148        match src {
1149            Some(Outcome::Win {
1150                side: Color::White, ..
1151            }) => Self::White,
1152            Some(Outcome::Win {
1153                side: Color::Black, ..
1154            }) => Self::Black,
1155            Some(Outcome::Draw(_)) => Self::Draw,
1156            None => Self::Running,
1157        }
1158    }
1159}
1160
1161impl From<Outcome> for GameStatus {
1162    #[inline]
1163    fn from(src: Outcome) -> Self {
1164        Self::from(Some(src))
1165    }
1166}
1167
1168impl From<&Outcome> for GameStatus {
1169    #[inline]
1170    fn from(src: &Outcome) -> Self {
1171        Self::from(Some(*src))
1172    }
1173}
1174
1175#[cfg(test)]
1176mod tests {
1177    use super::*;
1178
1179    #[test]
1180    fn test_file() {
1181        for (idx, file) in File::iter().enumerate() {
1182            assert_eq!(file.index(), idx);
1183            assert_eq!(File::from_index(idx), file);
1184        }
1185    }
1186
1187    #[test]
1188    fn test_rank() {
1189        for (idx, rank) in Rank::iter().enumerate() {
1190            assert_eq!(rank.index(), idx);
1191            assert_eq!(Rank::from_index(idx), rank);
1192        }
1193    }
1194
1195    #[test]
1196    fn test_piece() {
1197        for (idx, piece) in Piece::iter().enumerate() {
1198            assert_eq!(piece.index(), idx);
1199            assert_eq!(Piece::from_index(idx), piece);
1200        }
1201    }
1202
1203    #[test]
1204    fn test_coord() {
1205        let mut coords = Vec::new();
1206        for rank in Rank::iter() {
1207            for file in File::iter() {
1208                let coord = Coord::from_parts(file, rank);
1209                assert_eq!(coord.file(), file);
1210                assert_eq!(coord.rank(), rank);
1211                coords.push(coord);
1212            }
1213        }
1214        assert_eq!(coords, Coord::iter().collect::<Vec<_>>());
1215    }
1216
1217    #[test]
1218    fn test_cell() {
1219        assert_eq!(Cell::EMPTY.color(), None);
1220        assert_eq!(Cell::EMPTY.piece(), None);
1221        let mut cells = vec![Cell::EMPTY];
1222        for color in [Color::White, Color::Black] {
1223            for piece in [
1224                Piece::Pawn,
1225                Piece::King,
1226                Piece::Knight,
1227                Piece::Bishop,
1228                Piece::Rook,
1229                Piece::Queen,
1230            ] {
1231                let cell = Cell::from_parts(color, piece);
1232                assert_eq!(cell.color(), Some(color));
1233                assert_eq!(cell.piece(), Some(piece));
1234                cells.push(cell);
1235            }
1236        }
1237        assert_eq!(cells, Cell::iter().collect::<Vec<_>>());
1238    }
1239
1240    #[test]
1241    fn test_castling() {
1242        let empty = CastlingRights::EMPTY;
1243        assert!(!empty.has(Color::White, CastlingSide::Queen));
1244        assert!(!empty.has(Color::White, CastlingSide::King));
1245        assert!(!empty.has_color(Color::White));
1246        assert!(!empty.has(Color::Black, CastlingSide::Queen));
1247        assert!(!empty.has(Color::Black, CastlingSide::King));
1248        assert!(!empty.has_color(Color::Black));
1249        assert_eq!(empty.to_string(), "-");
1250        assert_eq!(CastlingRights::from_str("-"), Ok(empty));
1251
1252        let full = CastlingRights::FULL;
1253        assert!(full.has(Color::White, CastlingSide::Queen));
1254        assert!(full.has(Color::White, CastlingSide::King));
1255        assert!(full.has_color(Color::White));
1256        assert!(full.has(Color::Black, CastlingSide::Queen));
1257        assert!(full.has(Color::Black, CastlingSide::King));
1258        assert!(full.has_color(Color::Black));
1259        assert_eq!(full.to_string(), "KQkq");
1260        assert_eq!(CastlingRights::from_str("KQkq"), Ok(full));
1261
1262        let mut rights = CastlingRights::EMPTY;
1263        rights.set(Color::White, CastlingSide::King);
1264        assert!(!rights.has(Color::White, CastlingSide::Queen));
1265        assert!(rights.has(Color::White, CastlingSide::King));
1266        assert!(rights.has_color(Color::White));
1267        assert!(!rights.has(Color::Black, CastlingSide::Queen));
1268        assert!(!rights.has(Color::Black, CastlingSide::King));
1269        assert!(!rights.has_color(Color::Black));
1270        assert_eq!(rights.to_string(), "K");
1271        assert_eq!(CastlingRights::from_str("K"), Ok(rights));
1272
1273        rights.unset(Color::White, CastlingSide::King);
1274        rights.set(Color::Black, CastlingSide::Queen);
1275        assert!(!rights.has(Color::White, CastlingSide::Queen));
1276        assert!(!rights.has(Color::White, CastlingSide::King));
1277        assert!(!rights.has_color(Color::White));
1278        assert!(rights.has(Color::Black, CastlingSide::Queen));
1279        assert!(!rights.has(Color::Black, CastlingSide::King));
1280        assert!(rights.has_color(Color::Black));
1281        assert_eq!(rights.to_string(), "q");
1282        assert_eq!(CastlingRights::from_str("q"), Ok(rights));
1283    }
1284
1285    #[test]
1286    fn test_coord_str() {
1287        assert_eq!(
1288            Coord::from_parts(File::B, Rank::R4).to_string(),
1289            "b4".to_string()
1290        );
1291        assert_eq!(
1292            Coord::from_parts(File::A, Rank::R1).to_string(),
1293            "a1".to_string()
1294        );
1295        assert_eq!(
1296            Coord::from_str("a1"),
1297            Ok(Coord::from_parts(File::A, Rank::R1))
1298        );
1299        assert_eq!(
1300            Coord::from_str("b4"),
1301            Ok(Coord::from_parts(File::B, Rank::R4))
1302        );
1303        assert!(Coord::from_str("h9").is_err());
1304        assert!(Coord::from_str("i4").is_err());
1305    }
1306
1307    #[test]
1308    fn test_cell_str() {
1309        for cell in Cell::iter() {
1310            let s = cell.to_string();
1311            assert_eq!(Cell::from_str(&s), Ok(cell));
1312        }
1313    }
1314}