shakmaty/
square.rs

1use core::{
2    error,
3    fmt::{self, Write as _},
4    mem,
5    num::TryFromIntError,
6    ops::Sub,
7    str,
8};
9
10use crate::util::{out_of_range_error, AppendAscii};
11
12macro_rules! try_from_int_impl {
13    ($type:ty, $lower:expr, $upper:expr, $($t:ty)+) => {
14        $(impl core::convert::TryFrom<$t> for $type {
15            type Error = TryFromIntError;
16
17            #[inline]
18            #[allow(unused_comparisons)]
19            fn try_from(value: $t) -> Result<$type, Self::Error> {
20                if ($lower..$upper).contains(&value) {
21                    Ok(<$type>::new(value as u32))
22                } else {
23                    Err(out_of_range_error())
24                }
25            }
26        })+
27    }
28}
29
30/// A file of the chessboard.
31#[allow(missing_docs)]
32#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
33#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
34#[repr(u8)]
35pub enum File {
36    A = 0,
37    B,
38    C,
39    D,
40    E,
41    F,
42    G,
43    H,
44}
45
46impl File {
47    /// Gets a `File` from an integer index.
48    ///
49    /// # Panics
50    ///
51    /// Panics if the index is not in the range `0..=7`.
52    #[track_caller]
53    #[inline]
54    pub const fn new(index: u32) -> File {
55        assert!(index < 8);
56        unsafe { File::new_unchecked(index) }
57    }
58
59    /// Gets a `File` from an integer index.
60    ///
61    /// # Safety
62    ///
63    /// It is the callers responsibility to ensure the index is in the range
64    /// `0..=7`.
65    #[inline]
66    pub const unsafe fn new_unchecked(index: u32) -> File {
67        debug_assert!(index < 8);
68        unsafe { mem::transmute(index as u8) }
69    }
70
71    #[inline]
72    pub const fn from_char(ch: char) -> Option<File> {
73        match ch {
74            'a' => Some(File::A),
75            'b' => Some(File::B),
76            'c' => Some(File::C),
77            'd' => Some(File::D),
78            'e' => Some(File::E),
79            'f' => Some(File::F),
80            'g' => Some(File::G),
81            'h' => Some(File::H),
82            _ => None,
83        }
84    }
85
86    #[inline]
87    pub const fn char(self) -> char {
88        match self {
89            File::A => 'a',
90            File::B => 'b',
91            File::C => 'c',
92            File::D => 'd',
93            File::E => 'e',
94            File::F => 'f',
95            File::G => 'g',
96            File::H => 'h',
97        }
98    }
99
100    #[inline]
101    pub const fn upper_char(self) -> char {
102        match self {
103            File::A => 'A',
104            File::B => 'B',
105            File::C => 'C',
106            File::D => 'D',
107            File::E => 'E',
108            File::F => 'F',
109            File::G => 'G',
110            File::H => 'H',
111        }
112    }
113
114    #[must_use]
115    #[inline]
116    pub fn offset(self, delta: i32) -> Option<File> {
117        self.to_u32()
118            .checked_add_signed(delta)
119            .and_then(|index| index.try_into().ok())
120    }
121
122    #[inline]
123    pub const fn distance(self, other: File) -> u32 {
124        self.to_u32().abs_diff(other.to_u32())
125    }
126
127    #[must_use]
128    #[inline]
129    pub const fn flip_horizontal(self) -> File {
130        File::new(7 - self.to_u32())
131    }
132
133    #[must_use]
134    #[inline]
135    pub const fn flip_diagonal(self) -> Rank {
136        Rank::new(self.to_u32())
137    }
138
139    #[must_use]
140    #[inline]
141    pub const fn flip_anti_diagonal(self) -> Rank {
142        Rank::new(7 - self.to_u32())
143    }
144
145    #[must_use]
146    #[inline]
147    pub const fn to_u32(self) -> u32 {
148        self as u32
149    }
150
151    #[must_use]
152    #[inline(always)]
153    pub const fn to_usize(self) -> usize {
154        self as usize
155    }
156
157    /// `A`, ..., `H`.
158    pub const ALL: [File; 8] = [
159        File::A,
160        File::B,
161        File::C,
162        File::D,
163        File::E,
164        File::F,
165        File::G,
166        File::H,
167    ];
168}
169
170impl Sub for File {
171    type Output = i32;
172
173    #[inline]
174    fn sub(self, other: File) -> i32 {
175        i32::from(self) - i32::from(other)
176    }
177}
178
179impl fmt::Display for File {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        f.write_char(self.char())
182    }
183}
184
185from_enum_as_int_impl! { File, u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 usize isize }
186try_from_int_impl! { File, 0, 8, u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 usize isize }
187
188/// A rank of the chessboard.
189#[allow(missing_docs)]
190#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
191#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
192#[repr(u8)]
193pub enum Rank {
194    First = 0,
195    Second,
196    Third,
197    Fourth,
198    Fifth,
199    Sixth,
200    Seventh,
201    Eighth,
202}
203
204impl Rank {
205    /// Gets a `Rank` from an integer index.
206    ///
207    /// # Panics
208    ///
209    /// Panics if the index is not in the range `0..=7`.
210    #[track_caller]
211    #[inline]
212    pub const fn new(index: u32) -> Rank {
213        assert!(index < 8);
214        unsafe { Rank::new_unchecked(index) }
215    }
216
217    /// Gets a `Rank` from an integer index.
218    ///
219    /// # Safety
220    ///
221    /// It is the callers responsibility to ensure the index is in the range
222    /// `0..=7`.
223    #[inline]
224    pub const unsafe fn new_unchecked(index: u32) -> Rank {
225        debug_assert!(index < 8);
226        unsafe { mem::transmute(index as u8) }
227    }
228
229    #[inline]
230    pub const fn from_char(ch: char) -> Option<Rank> {
231        match ch {
232            '1' => Some(Rank::First),
233            '2' => Some(Rank::Second),
234            '3' => Some(Rank::Third),
235            '4' => Some(Rank::Fourth),
236            '5' => Some(Rank::Fifth),
237            '6' => Some(Rank::Sixth),
238            '7' => Some(Rank::Seventh),
239            '8' => Some(Rank::Eighth),
240            _ => None,
241        }
242    }
243
244    #[inline]
245    pub const fn char(self) -> char {
246        match self {
247            Rank::First => '1',
248            Rank::Second => '2',
249            Rank::Third => '3',
250            Rank::Fourth => '4',
251            Rank::Fifth => '5',
252            Rank::Sixth => '6',
253            Rank::Seventh => '7',
254            Rank::Eighth => '8',
255        }
256    }
257
258    #[must_use]
259    #[inline]
260    pub fn offset(self, delta: i32) -> Option<Rank> {
261        self.to_u32()
262            .checked_add_signed(delta)
263            .and_then(|index| index.try_into().ok())
264    }
265
266    #[inline]
267    pub const fn distance(self, other: Rank) -> u32 {
268        self.to_u32().abs_diff(other.to_u32())
269    }
270
271    #[must_use]
272    #[inline]
273    pub const fn flip_vertical(self) -> Rank {
274        Rank::new(7 - self.to_u32())
275    }
276
277    #[must_use]
278    #[inline]
279    pub const fn flip_diagonal(self) -> File {
280        File::new(self.to_u32())
281    }
282
283    #[must_use]
284    #[inline]
285    pub const fn flip_anti_diagonal(self) -> File {
286        File::new(7 - self.to_u32())
287    }
288
289    #[must_use]
290    #[inline]
291    pub const fn to_u32(self) -> u32 {
292        self as u32
293    }
294
295    #[must_use]
296    #[inline]
297    pub const fn to_usize(self) -> usize {
298        self as usize
299    }
300
301    /// `First`, ..., `Eighth`.
302    pub const ALL: [Rank; 8] = [
303        Rank::First,
304        Rank::Second,
305        Rank::Third,
306        Rank::Fourth,
307        Rank::Fifth,
308        Rank::Sixth,
309        Rank::Seventh,
310        Rank::Eighth,
311    ];
312}
313
314impl Sub for Rank {
315    type Output = i32;
316
317    #[inline]
318    fn sub(self, other: Rank) -> i32 {
319        i32::from(self) - i32::from(other)
320    }
321}
322
323impl fmt::Display for Rank {
324    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325        f.write_char(self.char())
326    }
327}
328
329from_enum_as_int_impl! { Rank, u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 usize isize }
330try_from_int_impl! { Rank, 0, 8, u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 usize isize }
331
332/// Error when parsing an invalid square name.
333#[derive(Clone, Debug)]
334pub struct ParseSquareError;
335
336impl fmt::Display for ParseSquareError {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        f.write_str("invalid square name")
339    }
340}
341
342impl error::Error for ParseSquareError {}
343
344/// A square of the chessboard.
345#[rustfmt::skip]
346#[allow(missing_docs)]
347#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
348#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
349#[repr(u8)]
350pub enum Square {
351    A1 = 0, B1, C1, D1, E1, F1, G1, H1,
352    A2, B2, C2, D2, E2, F2, G2, H2,
353    A3, B3, C3, D3, E3, F3, G3, H3,
354    A4, B4, C4, D4, E4, F4, G4, H4,
355    A5, B5, C5, D5, E5, F5, G5, H5,
356    A6, B6, C6, D6, E6, F6, G6, H6,
357    A7, B7, C7, D7, E7, F7, G7, H7,
358    A8, B8, C8, D8, E8, F8, G8, H8,
359}
360
361impl Square {
362    /// Gets a `Square` from an integer index.
363    ///
364    /// # Panics
365    ///
366    /// Panics if the index is not in the range `0..=63`.
367    ///
368    /// # Examples
369    ///
370    /// ```
371    /// use shakmaty::Square;
372    ///
373    /// assert_eq!(Square::new(0), Square::A1);
374    /// assert_eq!(Square::new(63), Square::H8);
375    /// ```
376    #[track_caller]
377    #[inline]
378    pub const fn new(index: u32) -> Square {
379        assert!(index < 64);
380        unsafe { Square::new_unchecked(index) }
381    }
382
383    /// Gets a `Square` from an integer index.
384    ///
385    /// # Safety
386    ///
387    /// It is the callers responsibility to ensure it is in the range `0..=63`.
388    #[inline]
389    pub const unsafe fn new_unchecked(index: u32) -> Square {
390        debug_assert!(index < 64);
391        unsafe { mem::transmute(index as u8) }
392    }
393
394    /// Tries to get a square from file and rank.
395    ///
396    /// # Examples
397    ///
398    /// ```
399    /// use shakmaty::{Square, File, Rank};
400    ///
401    /// assert_eq!(Square::from_coords(File::A, Rank::First), Square::A1);
402    /// ```
403    #[inline]
404    pub const fn from_coords(file: File, rank: Rank) -> Square {
405        // Safety: Files and ranks are represented with 3 bits each, and all
406        // 6 bit values are in the range 0..=63.
407        unsafe { Square::new_unchecked(file.to_u32() | (rank.to_u32() << 3)) }
408    }
409
410    /// Parses a square name.
411    ///
412    /// # Errors
413    ///
414    /// Returns [`ParseSquareError`] if the input is not a valid square name
415    /// in lowercase ASCII characters.
416    ///
417    /// # Example
418    ///
419    /// ```
420    /// use shakmaty::Square;
421    /// let sq = Square::from_ascii(b"a5")?;
422    /// assert_eq!(sq, Square::A5);
423    /// # Ok::<_, shakmaty::ParseSquareError>(())
424    /// ```
425    #[inline]
426    pub const fn from_ascii(s: &[u8]) -> Result<Square, ParseSquareError> {
427        if s.len() == 2 {
428            match (File::from_char(s[0] as char), Rank::from_char(s[1] as char)) {
429                (Some(file), Some(rank)) => Ok(Square::from_coords(file, rank)),
430                _ => Err(ParseSquareError),
431            }
432        } else {
433            Err(ParseSquareError)
434        }
435    }
436
437    /// Gets the file.
438    ///
439    /// # Examples
440    ///
441    /// ```
442    /// use shakmaty::{Square, File};
443    ///
444    /// assert_eq!(Square::A1.file(), File::A);
445    /// assert_eq!(Square::B2.file(), File::B);
446    /// ```
447    #[inline]
448    pub const fn file(self) -> File {
449        File::new(self.to_u32() & 7)
450    }
451
452    /// Gets the rank.
453    ///
454    /// # Examples
455    ///
456    /// ```
457    /// use shakmaty::{Square, Rank};
458    ///
459    /// assert_eq!(Square::A1.rank(), Rank::First);
460    /// assert_eq!(Square::B2.rank(), Rank::Second);
461    /// ```
462    #[inline]
463    pub const fn rank(self) -> Rank {
464        Rank::new(self.to_u32() >> 3)
465    }
466
467    /// Gets file and rank.
468    ///
469    /// # Examples
470    ///
471    /// ```
472    /// use shakmaty::{Square, File, Rank};
473    ///
474    /// assert_eq!(Square::A1.coords(), (File::A, Rank::First));
475    /// assert_eq!(Square::H8.coords(), (File::H, Rank::Eighth));
476    /// ```
477    #[inline]
478    pub const fn coords(self) -> (File, Rank) {
479        (self.file(), self.rank())
480    }
481
482    /// Calculates the offset from a square index.
483    ///
484    /// # Examples
485    ///
486    /// ```
487    /// use shakmaty::Square;
488    ///
489    /// assert_eq!(Square::F3.offset(8), Some(Square::F4));
490    /// assert_eq!(Square::F3.offset(-1), Some(Square::E3));
491    ///
492    /// assert_eq!(Square::F3.offset(48), None);
493    /// ```
494    #[must_use]
495    #[inline]
496    pub fn offset(self, delta: i32) -> Option<Square> {
497        self.to_u32()
498            .checked_add_signed(delta)
499            .and_then(|index| index.try_into().ok())
500    }
501
502    /// Calculates the offset from a square index without checking for
503    /// overflow.
504    ///
505    /// # Safety
506    ///
507    /// It is the callers responsibility to ensure that `delta` is a valid
508    /// offset for `self`.
509    #[must_use]
510    #[inline]
511    pub const unsafe fn offset_unchecked(self, delta: i32) -> Square {
512        debug_assert!(-64 < delta && delta < 64);
513        unsafe { Square::new_unchecked(self.to_u32().wrapping_add_signed(delta)) }
514    }
515
516    /// Return the bitwise XOR of the numeric square representations. For some
517    /// operands this is a useful geometric transformation.
518    #[must_use]
519    #[inline]
520    pub const fn xor(self, other: Square) -> Square {
521        // Safety: 6 bit value XOR 6 bit value -> 6 bit value.
522        unsafe { Square::new_unchecked(self.to_u32() ^ other.to_u32()) }
523    }
524
525    /// Flip the square horizontally.
526    ///
527    /// ```
528    /// use shakmaty::Square;
529    ///
530    /// assert_eq!(Square::H1.flip_horizontal(), Square::A1);
531    /// assert_eq!(Square::D3.flip_horizontal(), Square::E3);
532    /// ```
533    #[must_use]
534    #[inline]
535    pub const fn flip_horizontal(self) -> Square {
536        self.xor(Square::H1)
537    }
538
539    /// Flip the square vertically.
540    ///
541    /// ```
542    /// use shakmaty::Square;
543    ///
544    /// assert_eq!(Square::A8.flip_vertical(), Square::A1);
545    /// assert_eq!(Square::D3.flip_vertical(), Square::D6);
546    /// ```
547    #[must_use]
548    #[inline]
549    pub const fn flip_vertical(self) -> Square {
550        self.xor(Square::A8)
551    }
552
553    /// Flip at the a1-h8 diagonal by swapping file and rank.
554    ///
555    /// ```
556    /// use shakmaty::Square;
557    ///
558    /// assert_eq!(Square::A1.flip_diagonal(), Square::A1);
559    /// assert_eq!(Square::A3.flip_diagonal(), Square::C1);
560    /// ```
561    #[must_use]
562    #[inline]
563    pub const fn flip_diagonal(self) -> Square {
564        // See https://www.chessprogramming.org/Flipping_Mirroring_and_Rotating#Diagonal.
565        // Safety: We are selecting 32 - 26 = 6 bits with the shift, and all
566        // 6 bits values are in the range 0..=63.
567        unsafe { Square::new_unchecked(self.to_u32().wrapping_mul(0x2080_0000) >> 26) }
568    }
569
570    /// Flip at the h1-a8 diagonal.
571    ///
572    /// ```
573    /// use shakmaty::Square;
574    ///
575    /// assert_eq!(Square::A1.flip_anti_diagonal(), Square::H8);
576    /// assert_eq!(Square::A3.flip_anti_diagonal(), Square::F8);
577    /// ```
578    #[must_use]
579    #[inline]
580    pub const fn flip_anti_diagonal(self) -> Square {
581        self.flip_diagonal().rotate_180()
582    }
583
584    /// Rotate 90 degrees clockwise.
585    ///
586    /// ```
587    /// use shakmaty::Square;
588    ///
589    /// assert_eq!(Square::A1.rotate_90(), Square::A8);
590    /// assert_eq!(Square::A3.rotate_90(), Square::C8);
591    /// ```
592    #[must_use]
593    #[inline]
594    pub const fn rotate_90(self) -> Square {
595        self.flip_diagonal().flip_vertical()
596    }
597
598    /// Rotate 180 degrees.
599    ///
600    /// ```
601    /// use shakmaty::Square;
602    ///
603    /// assert_eq!(Square::A1.rotate_180(), Square::H8);
604    /// assert_eq!(Square::A3.rotate_180(), Square::H6);
605    /// ```
606    #[must_use]
607    #[inline]
608    pub const fn rotate_180(self) -> Square {
609        self.xor(Square::H8)
610    }
611
612    /// Rotate 270 degrees clockwise.
613    ///
614    /// ```
615    /// use shakmaty::Square;
616    ///
617    /// assert_eq!(Square::A1.rotate_270(), Square::H1);
618    /// assert_eq!(Square::A3.rotate_270(), Square::F1);
619    /// ```
620    #[must_use]
621    #[inline]
622    pub const fn rotate_270(self) -> Square {
623        self.flip_diagonal().flip_horizontal()
624    }
625
626    /// Tests is the square is a light square.
627    ///
628    /// ```
629    /// use shakmaty::Square;
630    ///
631    /// assert!(Square::D1.is_light());
632    /// assert!(!Square::D8.is_light());
633    /// ```
634    #[inline]
635    pub const fn is_light(self) -> bool {
636        (self.rank().to_u32() + self.file().to_u32()) % 2 == 1
637    }
638
639    /// Tests is the square is a dark square.
640    ///
641    /// ```
642    /// use shakmaty::Square;
643    ///
644    /// assert!(Square::E1.is_dark());
645    /// assert!(!Square::E8.is_dark());
646    /// ```
647    #[inline]
648    pub const fn is_dark(self) -> bool {
649        (self.rank().to_u32() + self.file().to_u32()) % 2 == 0
650    }
651
652    /// The distance between the two squares, i.e. the number of king steps
653    /// to get from one square to the other.
654    ///
655    /// ```
656    /// use shakmaty::Square;
657    ///
658    /// assert_eq!(Square::A2.distance(Square::B5), 3);
659    /// ```
660    pub const fn distance(self, other: Square) -> u32 {
661        let file_distance = self.file().distance(other.file());
662        let rank_distance = self.rank().distance(other.rank());
663
664        if file_distance > rank_distance {
665            file_distance
666        } else {
667            rank_distance
668        }
669    }
670
671    #[must_use]
672    #[inline]
673    pub const fn to_u32(self) -> u32 {
674        self as u32
675    }
676
677    #[must_use]
678    #[inline]
679    pub const fn to_usize(self) -> usize {
680        self as usize
681    }
682
683    pub(crate) fn append_to<W: AppendAscii>(self, f: &mut W) -> Result<(), W::Error> {
684        f.append_ascii(self.file().char())?;
685        f.append_ascii(self.rank().char())
686    }
687
688    #[cfg(feature = "alloc")]
689    pub fn append_to_string(&self, s: &mut alloc::string::String) {
690        let _ = self.append_to(s);
691    }
692
693    #[cfg(feature = "alloc")]
694    pub fn append_ascii_to(&self, buf: &mut alloc::vec::Vec<u8>) {
695        let _ = self.append_to(buf);
696    }
697}
698
699mod all_squares {
700    use super::Square::{self, *};
701    impl Square {
702        /// `A1`, `B1`, ..., `G8`, `H8`.
703        #[rustfmt::skip]
704        pub const ALL: [Square; 64] = [
705            A1, B1, C1, D1, E1, F1, G1, H1,
706            A2, B2, C2, D2, E2, F2, G2, H2,
707            A3, B3, C3, D3, E3, F3, G3, H3,
708            A4, B4, C4, D4, E4, F4, G4, H4,
709            A5, B5, C5, D5, E5, F5, G5, H5,
710            A6, B6, C6, D6, E6, F6, G6, H6,
711            A7, B7, C7, D7, E7, F7, G7, H7,
712            A8, B8, C8, D8, E8, F8, G8, H8,
713        ];
714    }
715}
716
717from_enum_as_int_impl! { Square, u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 usize isize }
718try_from_int_impl! { Square, 0, 64, u8 i8 u16 i16 u32 i32 u64 i64 u128 i128 usize isize }
719
720impl Sub for Square {
721    type Output = i32;
722
723    #[inline]
724    fn sub(self, other: Square) -> i32 {
725        i32::from(self) - i32::from(other)
726    }
727}
728
729impl From<(File, Rank)> for Square {
730    #[inline]
731    fn from((file, rank): (File, Rank)) -> Square {
732        Square::from_coords(file, rank)
733    }
734}
735
736impl str::FromStr for Square {
737    type Err = ParseSquareError;
738
739    fn from_str(s: &str) -> Result<Square, ParseSquareError> {
740        Square::from_ascii(s.as_bytes())
741    }
742}
743
744impl fmt::Display for Square {
745    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
746        self.append_to(f)
747    }
748}
749
750impl fmt::Debug for Square {
751    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
752        f.write_char(self.file().upper_char())?;
753        f.write_char(self.rank().char())
754    }
755}
756
757#[cfg(test)]
758mod tests {
759    use super::*;
760
761    #[test]
762    fn test_square() {
763        for file in (0..8).map(File::new) {
764            for rank in (0..8).map(Rank::new) {
765                let square = Square::from_coords(file, rank);
766                assert_eq!(square.file(), file);
767                assert_eq!(square.rank(), rank);
768            }
769        }
770    }
771
772    #[cfg(feature = "nohash-hasher")]
773    #[test]
774    fn test_nohash_hasher() {
775        use core::hash::{Hash, Hasher};
776
777        let mut hasher = nohash_hasher::NoHashHasher::<Square>::default();
778        Square::H1.hash(&mut hasher);
779        assert_eq!(hasher.finish(), 7);
780    }
781}