haitaka_types/
square.rs

1//! The [`Square`] enum represent squares on a Shogi board
2//!
3//! By Japanese convention squares are written as {file}{rank}. For instance, the topmost
4//! rightmost square in board diagrams is written either as "1a", "11", or "1一";
5//! the center square is written as "5e", "55", or "5五". Internally we represent square
6//! "1a" as Square::A1, and square "5e" as Square::E5. So, other than in Internation Chess,
7//! ranks (rows) are indicated by letters and files (columns) by numerals.
8//!
9//! Squares are ordered internally in file-major order: A1, B1, C1, ... I8, I9. This
10//! means that the squares on the rightmost file (File::One) correspond to the LSB bits
11//! of the bitboards. The main reason for choosing this internal layout is that it
12//! makes move generation of Lance moves easier to implement and faster (since Lances
13//! slide along files).
14//!    
15use core::convert::TryInto;
16use core::str::FromStr;
17
18use crate::*;
19
20macro_rules! define_square_with_docs {
21    ($($square:ident),*) => {
22        crate::helpers::simple_enum! {
23            /// A square on a Shogi board.
24            #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
25            pub enum Square {
26                $(
27                    #[doc = concat!("The ", stringify!($square), " square.")]
28                    $square
29                ),*
30            }
31        }
32    }
33}
34
35// Defining the squares in file-major order.
36// Note: Changing this order will break BitBoard::has (and other functions).
37
38define_square_with_docs! {
39    A1, B1, C1, D1, E1, F1, G1, H1, I1,
40    A2, B2, C2, D2, E2, F2, G2, H2, I2,
41    A3, B3, C3, D3, E3, F3, G3, H3, I3,
42    A4, B4, C4, D4, E4, F4, G4, H4, I4,
43    A5, B5, C5, D5, E5, F5, G5, H5, I5,
44    A6, B6, C6, D6, E6, F6, G6, H6, I6,
45    A7, B7, C7, D7, E7, F7, G7, H7, I7,
46    A8, B8, C8, D8, E8, F8, G8, H8, I8,
47    A9, B9, C9, D9, E9, F9, G9, H9, I9
48}
49
50crate::helpers::simple_error! {
51    /// The value was not a valid [`Square`].
52    pub struct SquareParseError = "The value was not a valid Square.";
53}
54
55impl FromStr for Square {
56    type Err = SquareParseError;
57
58    // "1a" => File::One, Rank::A => Square::A1
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        let mut chars = s.chars();
61        let file = chars
62            .next()
63            .and_then(|c| c.try_into().ok())
64            .ok_or(SquareParseError)?;
65        let rank = chars
66            .next()
67            .and_then(|c| c.try_into().ok())
68            .ok_or(SquareParseError)?;
69        if chars.next().is_some() {
70            return Err(SquareParseError);
71        }
72        Ok(Square::new(file, rank))
73    }
74}
75
76impl core::fmt::Display for Square {
77    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> {
78        write!(f, "{}{}", self.file(), self.rank())
79    }
80}
81
82// Directions  Diagrams     Square indices
83// NW N NE     A9 ... A1    72 ...  0
84//  W . E         ...          ...
85// SW S SE     I9 ... I1    80 ...  9
86
87// Diagonal mask for forward ("up") slashing diagonals (/).
88const NEG_MASK: u128 = 0x100401004010040100401;
89
90// Diagonal mask for backward ("down") slashing diagonals (\).
91const POS_MASK: u128 = 0x1010101010101010100;
92
93// BitBoards for the two main diagonals.
94const NEGD: BitBoard = BitBoard::new(NEG_MASK);
95const POSD: BitBoard = BitBoard::new(POS_MASK);
96
97/// Down-slanting diagonals.
98///
99/// A square (file, rank) is on down-slanting diagonal `POS_DIA[file + rank]`.
100/// The down-slanting diagonals are indexed as
101/// ```text
102///    8  7  6  5  4  3  2  1  0
103///    9  8  7  6  5  4  3  2  1
104///   10  9  8  7  6  5  4  3  2
105///   11 10  9  8  7  6  5  4  3
106///   12 11 10  9  8  7  6  5  4
107///   13 12 11 10  9  8  7  6  5
108///   13 12 11 10  9  8  7  6  5
109///   14 13 12 11 10  9  8  7  6
110///   15 14 13 12 11 10  9  8  7
111///   16 15 14 13 12 11 10  9  8
112/// ```
113///
114/// # Examples
115/// ```
116/// use haitaka_types::*;
117/// assert_eq!(POS_DIA[8], bitboard! {
118///     X . . . . . . . .
119///     . X . . . . . . .
120///     . . X . . . . . .
121///     . . . X . . . . .
122///     . . . . X . . . .
123///     . . . . . X . . .
124///     . . . . . . X . .
125///     . . . . . . . X .
126///     . . . . . . . . X
127/// });
128/// assert_eq!(POS_DIA[2], bitboard! {
129///     . . . . . . X . .
130///     . . . . . . . X .
131///     . . . . . . . . X
132///     . . . . . . . . .
133///     . . . . . . . . .
134///     . . . . . . . . .
135///     . . . . . . . . .
136///     . . . . . . . . .
137///     . . . . . . . . .
138/// });
139/// assert_eq!(POS_DIA[16], bitboard! {
140///     . . . . . . . . .
141///     . . . . . . . . .
142///     . . . . . . . . .
143///     . . . . . . . . .
144///     . . . . . . . . .
145///     . . . . . . . . .
146///     . . . . . . . . .
147///     . . . . . . . . .
148///     X . . . . . . . .
149/// });
150/// ```
151pub const POS_DIA: [BitBoard; 17] = [
152    POSD.shr(8),
153    POSD.shr(7),
154    POSD.shr(6),
155    POSD.shr(5),
156    POSD.shr(4),
157    POSD.shr(3),
158    POSD.shr(2),
159    POSD.shr(1),
160    POSD,
161    POSD.shl(1),
162    POSD.shl(2),
163    POSD.shl(3),
164    POSD.shl(4),
165    POSD.shl(5),
166    POSD.shl(6),
167    POSD.shl(7),
168    POSD.shl(8),
169];
170
171/// Up-slanting diagonals.
172///
173/// A square (file, rank) is on up-slanting diagonal NEG_DIA[8 + rank - file].
174/// The upslanting diagonals are indexed as
175/// ```text
176///     8  7  6  5  4  3  2  1  0 |  
177///     --------------------------+---
178///     0  1  2  3  4  5  6  7  8 | 0  
179///     1  2  3  4  5  6  7  8  9 | 1
180///     2  3  4  5  6  7  8  9 10 | 2
181///     3  4  5  6  7  8  9 10 11 | 3
182///     4  5  6  7  8  9 10 11 12 | 4
183///     5  6  7  8  9 10 11 12 13 | 5
184///     6  7  8  9 10 11 12 13 14 | 6
185///     7  8  9 10 11 12 13 14 15 | 7
186///     8  9 10 11 12 13 14 15 16 | 8
187/// ```
188///
189/// # Examples
190/// ```
191/// use haitaka_types::*;
192/// assert_eq!(NEG_DIA[8], bitboard! {
193///     . . . . . . . . X
194///     . . . . . . . X .
195///     . . . . . . X . .
196///     . . . . . X . . .
197///     . . . . X . . . .
198///     . . . X . . . . .
199///     . . X . . . . . .
200///     . X . . . . . . .
201///     X . . . . . . . .
202/// });
203/// assert_eq!(NEG_DIA[14], bitboard! {
204///     . . . . . . . . .
205///     . . . . . . . . .
206///     . . . . . . . . .
207///     . . . . . . . . .
208///     . . . . . . . . .
209///     . . . . . . . . .
210///     . . . . . . . . X
211///     . . . . . . . X .
212///     . . . . . . X . .
213/// });
214/// assert_eq!(NEG_DIA[0], bitboard! {
215///     X . . . . . . . .
216///     . . . . . . . . .
217///     . . . . . . . . .
218///     . . . . . . . . .     
219///     . . . . . . . . .
220///     . . . . . . . . .
221///     . . . . . . . . .
222///     . . . . . . . . .
223///     . . . . . . . . .
224/// });
225/// ```
226pub const NEG_DIA: [BitBoard; 17] = [
227    NEGD.shr(8),
228    NEGD.shr(7),
229    NEGD.shr(6),
230    NEGD.shr(5),
231    NEGD.shr(4),
232    NEGD.shr(3),
233    NEGD.shr(2),
234    NEGD.shr(1),
235    NEGD,
236    NEGD.shl(1),
237    NEGD.shl(2),
238    NEGD.shl(3),
239    NEGD.shl(4),
240    NEGD.shl(5),
241    NEGD.shl(6),
242    NEGD.shl(7),
243    NEGD.shl(8),
244];
245
246impl Square {
247    /// Make a square from a file and a rank.
248    /// # Examples
249    /// ```
250    /// # use haitaka_types::*;
251    /// assert_eq!(Square::new(File::One, Rank::A), Square::A1);
252    /// assert_eq!(Square::new(File::Two, Rank::B), Square::B2);
253    /// ```
254    #[inline(always)]
255    pub const fn new(file: File, rank: Rank) -> Self {
256        Self::index_const((file as usize) * 9 + (rank as usize))
257    }
258
259    /// Get the file of this square.
260    /// # Examples
261    /// ```
262    /// # use haitaka_types::*;
263    /// assert_eq!(Square::A1.file(), File::One);
264    /// assert_eq!(Square::B2.file(), File::Two);
265    /// ```
266    #[inline(always)]
267    pub const fn file(self) -> File {
268        File::index_const(self as usize / 9)
269    }
270
271    /// Get the rank of this square.
272    /// # Examples
273    /// ```
274    /// # use haitaka_types::*;
275    /// assert_eq!(Square::A1.rank(), Rank::A);
276    /// assert_eq!(Square::B2.rank(), Rank::B);
277    /// ```
278    #[inline(always)]
279    pub const fn rank(self) -> Rank {
280        Rank::index_const(self as usize % 9)
281    }
282
283    /// Get a bitboard with this square set.
284    /// ```
285    /// # use haitaka_types::*;
286    /// assert_eq!(Square::G8.bitboard(), bitboard! {
287    ///     . . . . . . . . .
288    ///     . . . . . . . . .
289    ///     . . . . . . . . .
290    ///     . . . . . . . . .
291    ///     . . . . . . . . .
292    ///     . . . . . . . . .
293    ///     . X . . . . . . .
294    ///     . . . . . . . . .
295    ///     . . . . . . . . .
296    /// });
297    /// ```
298    #[inline(always)]
299    pub const fn bitboard(self) -> BitBoard {
300        BitBoard(1 << self as usize)
301    }
302
303    /// Get the bitboard with the "up" (forward-slanting "/") diagonal for this square.
304    ///
305    /// # Examples
306    /// ```
307    /// use haitaka_types::*;
308    /// assert_eq!(Square::E5.up_diagonal(), bitboard! {
309    ///     . . . . . . . . X
310    ///     . . . . . . . X .
311    ///     . . . . . . X . .
312    ///     . . . . . X . . .
313    ///     . . . . X . . . .
314    ///     . . . X . . . . .
315    ///     . . X . . . . . .
316    ///     . X . . . . . . .
317    ///     X . . . . . . . .
318    /// });
319    /// assert_eq!(Square::G3.up_diagonal(), bitboard! {
320    ///     . . . . . . . . .
321    ///     . . . . . . . . .
322    ///     . . . . . . . . .
323    ///     . . . . . . . . .
324    ///     . . . . . . . . X
325    ///     . . . . . . . X .
326    ///     . . . . . . X . .
327    ///     . . . . . X . . .
328    ///     . . . . X . . . .
329    /// });
330    /// assert_eq!(Square::A1.up_diagonal(), Square::I9.up_diagonal());
331    /// assert_eq!(Square::A9.up_diagonal(), Square::A9.bitboard());
332    /// assert_eq!(Square::I1.up_diagonal(), Square::I1.bitboard());
333    /// ```
334    #[inline(always)]
335    pub const fn up_diagonal(self) -> BitBoard {
336        let rank = self as usize % 9;
337        let file = self as usize / 9;
338        NEG_DIA[8 + rank - file]
339    }
340
341    /// Get the bitboard with the "down" (backwards-slanting "\") diagonal for this square.
342    ///
343    /// # Examples
344    /// ```
345    /// use haitaka_types::*;
346    /// assert_eq!(Square::E5.down_diagonal(), bitboard! {
347    ///     X . . . . . . . .
348    ///     . X . . . . . . .
349    ///     . . X . . . . . .
350    ///     . . . X . . . . .
351    ///     . . . . X . . . .
352    ///     . . . . . X . . .
353    ///     . . . . . . X . .
354    ///     . . . . . . . X .
355    ///     . . . . . . . . X
356    /// });
357    /// assert_eq!(Square::A6.down_diagonal(), bitboard! {
358    ///     . . . X . . . . .
359    ///     . . . . X . . . .
360    ///     . . . . . X . . .
361    ///     . . . . . . X . .
362    ///     . . . . . . . X .
363    ///     . . . . . . . . X
364    ///     . . . . . . . . .
365    ///     . . . . . . . . .
366    ///     . . . . . . . . .
367    /// });    
368    /// assert_eq!(Square::A9.down_diagonal(), Square::I1.down_diagonal());
369    /// assert_eq!(Square::A1.down_diagonal(), Square::A1.bitboard());
370    /// assert_eq!(Square::I9.down_diagonal(), Square::I9.bitboard());
371    /// ```
372    #[inline(always)]
373    pub const fn down_diagonal(self) -> BitBoard {
374        let rank = self as usize % 9;
375        let file = self as usize / 9;
376        POS_DIA[file + rank]
377    }
378
379    /// Add a file and rank offset to the given square.
380    ///
381    /// Since square A1 is the topmost-rightmost square,
382    /// positive offsets correspond to a down- and leftwards
383    /// direction. Note that A1 means file 1 and rank A.
384    ///
385    /// # Panics
386    /// Panic if the offset would put the square out of bounds.
387    /// See [`Square::try_offset`] for a non-panicking variant.
388    ///
389    /// # Examples
390    /// ```
391    /// # use haitaka_types::*;
392    /// assert_eq!(Square::A1.offset(2, 1), Square::B3);
393    /// assert_eq!(Square::B3.offset(-2, -1), Square::A1);  
394    /// assert_eq!(Square::H1.offset(0, 1), Square::I1);
395    /// assert_eq!(Square::H9.offset(0, 1), Square::I9);
396    /// ```
397    pub const fn offset(self, file_offset: i8, rank_offset: i8) -> Square {
398        if let Some(sq) = self.try_offset(file_offset, rank_offset) {
399            sq
400        } else {
401            panic!("Offset puts square out of bounds");
402        }
403    }
404
405    /// Non-panicking version of [`Square::offset`].
406    ///
407    /// # Errors
408    /// See [`Square::offset`]'s panics.
409    ///
410    /// # Examples
411    /// ```
412    /// use haitaka_types::*;
413    /// assert_eq!(Square::A1.try_offset(1, 1), Some(Square::B2));
414    /// assert_eq!(Square::E5.try_offset(-1, -1), Some(Square::D4));
415    /// assert_eq!(Square::H9.try_offset(0, -1), Some(Square::G9));
416    /// assert_eq!(Square::C3.try_offset(2, 0), Some(Square::C5));
417    /// assert_eq!(Square::A1.try_offset(-1, 0), None); // File out of bounds
418    /// assert_eq!(Square::A1.try_offset(0, -1), None); // Rank out of bounds
419    /// assert_eq!(Square::I9.try_offset(1, 0), None); // File out of bounds
420    /// assert_eq!(Square::I9.try_offset(0, 1), None); // Rank out of bounds
421    /// ```
422    #[inline(always)]
423    pub const fn try_offset(self, file_offset: i8, rank_offset: i8) -> Option<Square> {
424        let file_index = self.file() as i8 + file_offset;
425        let rank_index = self.rank() as i8 + rank_offset;
426
427        if file_index < 0 || file_index >= 9 || rank_index < 0 || rank_index >= 9 {
428            return None;
429        }
430        Some(Square::new(
431            File::index_const(file_index as usize),
432            Rank::index_const(rank_index as usize),
433        ))
434    }
435
436    /// Flip the file of this square.
437    ///
438    /// Mirrors square in the central File::Five.
439    ///
440    /// # Examples
441    /// ```
442    /// # use haitaka_types::*;
443    /// assert_eq!(Square::A1.flip_file(), Square::A9);
444    /// ```
445    #[inline(always)]
446    pub const fn flip_file(self) -> Self {
447        Self::new(self.file().flip(), self.rank())
448    }
449
450    /// Flip the rank of this square.
451    ///
452    /// Mirrors square in the central Rank::E.
453    ///
454    /// # Examples
455    /// ```
456    /// # use haitaka_types::*;
457    /// assert_eq!(Square::A1.flip_rank(), Square::I1);
458    /// ```
459    #[inline(always)]
460    pub const fn flip_rank(self) -> Self {
461        Self::new(self.file(), self.rank().flip())
462    }
463
464    /// Flip both rank and file of this square.
465    ///
466    /// This rotates the square around the center square E5.
467    ///
468    /// # Examples
469    /// ```
470    /// # use haitaka_types::*;
471    /// assert_eq!(Square::A1.flip(), Square::I9);
472    /// assert_eq!(Square::E5.flip(), Square::E5);
473    /// ```
474    #[inline(always)]
475    pub const fn flip(self) -> Self {
476        Self::new(self.file().flip(), self.rank().flip())
477    }
478
479    /// Get a square relative to some color.
480    ///
481    /// This effectively _rotates_ the board if viewed from Gote's/White's
482    /// perspective. It flips both the rank and the file of the square.
483    ///
484    /// Note that the initial Shogi position has rotational symmetry.
485    /// This differs from the initial position in International Chess which has
486    /// mirror symmetry (flipping the ranks).
487    ///   
488    /// # Examples
489    /// ```
490    /// # use haitaka_types::*;
491    /// assert_eq!(Square::A1.relative_to(Color::White), Square::I9);
492    /// assert_eq!(Square::E5.relative_to(Color::White), Square::E5);
493    /// assert_eq!(Square::A1.relative_to(Color::Black), Square::A1);
494    /// ```
495    #[inline(always)]
496    pub const fn relative_to(self, color: Color) -> Self {
497        if let Color::Black = color {
498            self
499        } else {
500            Self::new(self.file().flip(), self.rank().flip())
501        }
502    }
503}