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