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#[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 #[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 #[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 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#[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 #[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 #[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 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#[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#[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 #[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 #[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 #[inline]
407 pub const fn from_coords(file: File, rank: Rank) -> Square {
408 unsafe { Square::new_unchecked(file.to_u32() | (rank.to_u32() << 3)) }
411 }
412
413 #[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 #[inline]
453 pub const fn file(self) -> File {
454 File::new(self.to_u32() & 7)
455 }
456
457 #[inline]
468 pub const fn rank(self) -> Rank {
469 Rank::new(self.to_u32() >> 3)
470 }
471
472 #[inline]
483 pub const fn coords(self) -> (File, Rank) {
484 (self.file(), self.rank())
485 }
486
487 #[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 #[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 #[must_use]
524 #[inline]
525 pub const fn xor(self, other: Square) -> Square {
526 unsafe { Square::new_unchecked(self.to_u32() ^ other.to_u32()) }
528 }
529
530 #[must_use]
539 #[inline]
540 pub const fn flip_horizontal(self) -> Square {
541 self.xor(Square::H1)
542 }
543
544 #[must_use]
553 #[inline]
554 pub const fn flip_vertical(self) -> Square {
555 self.xor(Square::A8)
556 }
557
558 #[must_use]
567 #[inline]
568 pub const fn flip_diagonal(self) -> Square {
569 unsafe { Square::new_unchecked(self.to_u32().wrapping_mul(0x2080_0000) >> 26) }
573 }
574
575 #[must_use]
584 #[inline]
585 pub const fn flip_anti_diagonal(self) -> Square {
586 self.flip_diagonal().rotate_180()
587 }
588
589 #[must_use]
598 #[inline]
599 pub const fn rotate_90(self) -> Square {
600 self.flip_diagonal().flip_vertical()
601 }
602
603 #[must_use]
612 #[inline]
613 pub const fn rotate_180(self) -> Square {
614 self.xor(Square::H8)
615 }
616
617 #[must_use]
626 #[inline]
627 pub const fn rotate_270(self) -> Square {
628 self.flip_diagonal().flip_horizontal()
629 }
630
631 #[inline]
640 pub const fn is_light(self) -> bool {
641 (self.rank().to_u32() + self.file().to_u32()) % 2 == 1
642 }
643
644 #[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 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 #[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}