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}