Skip to main content

figrid_board/
coord.rs

1use std::{fmt::Display, str::FromStr};
2
3use crate::Error;
4
5/// State of a coordinate (position) on the board.
6#[derive(Clone, Copy, PartialEq, Eq, Debug)]
7#[repr(u8)]
8pub enum CoordState {
9    Empty,
10    Black,
11    White,
12}
13
14impl CoordState {
15    #[inline(always)]
16    pub fn is_empty(&self) -> bool {
17        *self == CoordState::Empty
18    }
19
20    #[inline(always)]
21    pub fn is_stone(&self) -> bool {
22        !self.is_empty()
23    }
24
25    #[inline(always)]
26    pub fn is_black(&self) -> bool {
27        *self == CoordState::Black
28    }
29
30    #[inline(always)]
31    pub fn is_white(&self) -> bool {
32        *self == CoordState::White
33    }
34}
35
36/// Coordinate on the board of size `SZ`, or a null coordinate not on the board.
37/// For example, coordinate of "a1" is `Coord {x: 0, y: 0}`.
38///
39/// Null coordinate is allowed for pass moves and possible wrapper of `Rec`
40/// to store swapping info. Designed for making different amount of black and white
41/// stones possible without storing color info (just check if the index is even).
42#[derive(Clone, Copy, PartialEq, Eq, Debug)]
43pub struct Coord<const SZ: usize> {
44    x: u8,
45    y: u8,
46}
47
48/// The `x` or `y` value of a null coordinate.
49pub(crate) const COORD_NULL_VAL: u8 = 0xff;
50
51pub type Coord15 = Coord<15>;
52pub type Coord20 = Coord<20>;
53
54impl<const SZ: usize> Coord<SZ> {
55    const SIZE: u8 = if SZ >= 5 && SZ <= 26 {
56        SZ as u8
57    } else {
58        panic!()
59    };
60
61    const COORD_NULL: Self = Self {
62        x: COORD_NULL_VAL,
63        y: COORD_NULL_VAL,
64    };
65
66    /// Returns a null coordinate.
67    ///
68    /// ```
69    /// assert!(figrid_board::Coord15::new().is_null());
70    /// ```
71    #[inline(always)]
72    pub fn new() -> Self {
73        Self::COORD_NULL
74    }
75
76    /// Returns a coordinate object if `x` and `y` are within the board size,
77    /// otherwise returns a null coordinate.
78    ///
79    /// ```
80    /// let coord = figrid_board::Coord15::from(3, 9);
81    /// assert!(coord.is_real());
82    /// assert_eq!(format!("{coord}"), "d10");
83    ///
84    /// let coord = figrid_board::Coord15::from(9, 15);
85    /// assert!(coord.is_null());
86    /// assert_eq!(format!("{coord}"), "-");
87    /// ```
88    #[inline(always)]
89    pub fn from(x: u8, y: u8) -> Self {
90        if x < Self::SIZE && y < Self::SIZE {
91            Self { x, y }
92        } else {
93            Self::COORD_NULL
94        }
95    }
96
97    /// Use it carefully, it allows building invalid coordinate of which
98    /// the x and y can be set to values other than COORD_NULL_VAL (UB!)
99    #[inline(always)]
100    pub(crate) unsafe fn build_unchecked(x: u8, y: u8) -> Self {
101        Self { x, y }
102    }
103
104    /// Check if it is a null coordinate.
105    #[inline(always)]
106    pub fn is_null(&self) -> bool {
107        *self == Self::COORD_NULL
108    }
109
110    /// Check if it is a real coordinate on the board.
111    #[inline(always)]
112    pub fn is_real(&self) -> bool {
113        !self.is_null()
114    }
115
116    /// Returns the coordinate (x, y), or `None` if it is null.
117    ///
118    /// ```
119    /// let coord: figrid_board::Coord15 = "h8".parse().unwrap();
120    /// assert_eq!(coord.get(), Some((7, 7)));
121    ///
122    /// let coord = figrid_board::Coord15::new();
123    /// assert_eq!(coord.get(), None);
124    /// ```
125    #[inline(always)]
126    pub fn get(&self) -> Option<(u8, u8)> {
127        if self.is_real() {
128            Some((self.x, self.y))
129        } else {
130            None
131        }
132    }
133
134    /// Returns the coordinate (x, y), panics if it is null.
135    #[inline(always)]
136    pub fn unwrap(&self) -> (u8, u8) {
137        self.get().unwrap()
138    }
139
140    /// Returns the coordinate (x, y), which contains invalid values if it is null.
141    ///
142    /// # Safety
143    ///
144    /// The caller must make sure that this coordinate is not null.
145    #[inline(always)]
146    pub unsafe fn get_unchecked(&self) -> (u8, u8) {
147        (self.x, self.y)
148    }
149
150    /// Set the coordinate if `x` and `y` are within the board size,
151    /// otherwise returns `Err(figrid_board::Error::InvalidCoord)`.
152    ///
153    /// ```
154    /// let mut coord = figrid_board::Coord15::new();
155    /// assert_eq!(coord.set(9, 15), Err(figrid_board::Error::InvalidCoord));
156    /// assert_eq!(coord.get(), None);
157    /// assert_eq!(coord.set(9, 9), Ok(()));
158    /// assert_eq!(format!("{coord}"), "j10");
159    /// ```
160    #[inline(always)]
161    pub fn set(&mut self, x: u8, y: u8) -> Result<(), Error> {
162        if x < Self::SIZE && y < Self::SIZE {
163            self.x = x;
164            self.y = y;
165            Ok(())
166        } else {
167            Err(Error::InvalidCoord)
168        }
169    }
170
171    /// Set it to a null coordinate.
172    #[inline(always)]
173    pub fn set_null(&mut self) {
174        *self = Self::COORD_NULL;
175    }
176
177    /// Use it carefully, it allows setting invalid coordinate of which
178    /// the x and y can be values other than COORD_NULL_VAL (UB!)
179    #[inline(always)]
180    #[allow(dead_code)]
181    pub(crate) unsafe fn set_unchecked(&mut self, x: u8, y: u8) {
182        self.x = x;
183        self.y = y;
184    }
185}
186
187impl<const SZ: usize> Default for Coord<SZ> {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193impl<const SZ: usize> FromStr for Coord<SZ> {
194    type Err = Error;
195    fn from_str(s: &str) -> Result<Self, Self::Err> {
196        Ok(Self::parse_str(s).ok_or(Error::ParseError)?.0)
197    }
198}
199
200impl<const SZ: usize> Display for Coord<SZ> {
201    #[inline(always)]
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        if self.is_real() {
204            write!(f, "{}{}", coord_x_letter(self.x), self.y + 1)
205        } else {
206            write!(f, "-")
207        }
208    }
209}
210
211impl<const SZ: usize> Coord<SZ> {
212    /// Used by `FromStr` implementation of `Coord` and the parser of `Record`.
213    /// Returns a coordinate and the parsed string length on success.
214    #[inline]
215    pub(crate) fn parse_str(str_coords: &str) -> Option<(Self, usize)> {
216        const ALPHABET: &str = "abcdefghijklmnopqrstuvwxyz";
217        let alphabet = &ALPHABET[..SZ];
218
219        let mut len_checked: usize = 0;
220        loop {
221            let str_rem = &str_coords[len_checked..];
222            let mut itr = str_rem.chars().enumerate();
223            // finds a possible coord. i: index in str_rem, x: possible coord x
224            let loc_alpha = itr.find_map(|(i, c)| alphabet.find(c).map(|x| (i, x)));
225            if let Some((i, x)) = loc_alpha {
226                len_checked += i + 1;
227                // parse the possible coord y
228                let mut num: Option<u32> = None;
229                for (_, ch) in itr {
230                    if !ch.is_ascii_digit() {
231                        break;
232                    }
233                    if num.is_none() {
234                        num = ch.to_digit(10);
235                    } else {
236                        num.replace(10 * num.unwrap() + ch.to_digit(10).unwrap());
237                    }
238                    len_checked += 1;
239                }
240                if let Some(n) = num {
241                    if n == 0 || n > SZ as u32 {
242                        continue;
243                    }
244                    return Some((
245                        Self {
246                            x: x as u8,
247                            y: (n - 1) as u8,
248                        },
249                        len_checked,
250                    ));
251                }
252            } else {
253                return None;
254            }
255        }
256    }
257}
258
259#[inline(always)]
260pub(crate) fn coord_x_letter(x: u8) -> char {
261    if x < 26 {
262        // SAFETY: x < 26
263        unsafe { char::from_u32_unchecked(('a' as u32) + x as u32) }
264    } else {
265        '?'
266    }
267}
268
269/// Possible rotation operations.
270#[derive(Clone, Copy, PartialEq, Eq, Debug)]
271#[repr(u8)]
272pub enum Rotation {
273    Original,          // 0 00, don't rotate
274    Clockwise,         // 0 01, rotate 90 degrees
275    CentralSymmetric,  // 0 10, equals rotate 180 degrees clockwise
276    Counterclockwise,  // 0 11, equals rotate 270 degrees clockwise
277    FlipHorizontal,    // 1 00, reflect by vertical line in the middle
278    FlipLeftDiagonal,  // 1 01, equals FlipHorizontal | Clockwise
279    FlipVertical,      // 1 10, equals FlipHorizontal | CentralSymmetric
280    FlipRightDiagonal, // 1 11, equals FlipHorizontal | Counterclockwise
281}
282
283impl<const SZ: usize> Coord<SZ> {
284    /// Returns the coordinate rotated by `rotation`.
285    ///
286    /// ```
287    /// use figrid_board::{ Coord15, Rotation::* };
288    /// let coord = Coord15::from(0, 1); // a2
289    /// assert_eq!(coord.rotate(Original), coord);
290    /// assert_eq!(coord.rotate(Clockwise).unwrap(), (1, 14));
291    /// assert_eq!(coord.rotate(CentralSymmetric).unwrap(), (14, 13));
292    /// assert_eq!(coord.rotate(Counterclockwise).unwrap(), (13, 0));
293    /// assert_eq!(coord.rotate(FlipHorizontal).unwrap(), (14, 1));
294    /// assert_eq!(coord.rotate(FlipLeftDiagonal).unwrap(), (1, 0));
295    /// assert_eq!(coord.rotate(FlipVertical).unwrap(), (0, 13));
296    /// assert_eq!(coord.rotate(FlipRightDiagonal).unwrap(), (13, 14));
297    /// assert_eq!(coord.rotate(FlipRightDiagonal).rotate(FlipRightDiagonal.reverse()), coord);
298    /// assert_eq!(coord.rotate(FlipVertical.add(CentralSymmetric)),
299    ///     coord.rotate(FlipHorizontal));
300    /// ```
301    #[inline(always)]
302    pub fn rotate(&self, rotation: Rotation) -> Coord<SZ> {
303        let (mut x, y) = if let Some(coord) = self.get() {
304            coord
305        } else {
306            return *self;
307        };
308
309        let bnd = SZ as u8 - 1u8;
310        let (fl, ro) = rotation.fl_ro();
311
312        // flip before rotate
313        if fl == 1_u8 {
314            x = bnd - x;
315        }
316        let (x, y) = match ro {
317            0b01_u8 => (y, bnd - x),
318            0b10_u8 => (bnd - x, bnd - y),
319            0b11_u8 => (bnd - y, x),
320            _ => (x, y),
321        };
322        // SAFETY: assumes the algorithm is correct
323        unsafe { Coord::<SZ>::build_unchecked(x, y) }
324    }
325
326    /// Returns the coordinate translated by `x` and `y` offsets, or `None` if out of range.
327    ///
328    /// ```
329    /// let coord = figrid_board::Coord15::from(1, 2);
330    /// assert_eq!(coord.offset(-1i8, 3i8).unwrap(), "a6".parse().unwrap());
331    /// assert_eq!(coord.offset(-2i8, 3i8), None);
332    /// ```
333    #[inline(always)]
334    pub fn offset(&self, offset_x: i8, offset_y: i8) -> Option<Coord<SZ>> {
335        let (x, y) = if let Some(coord) = self.get() {
336            coord
337        } else {
338            return Some(*self);
339        };
340        let (x_new, y_new) = (
341            (x as i8).checked_add(offset_x)?,
342            (y as i8).checked_add(offset_y)?,
343        );
344        if x_new < 0i8 || y_new < 0i8 {
345            return None;
346        }
347        let (x_new, y_new) = (x_new as u8, y_new as u8);
348        if x_new < Self::SIZE && y_new < Self::SIZE {
349            Some(Coord { x: x_new, y: y_new })
350        } else {
351            None
352        }
353    }
354}
355
356use Rotation::*;
357impl Rotation {
358    /// Returns a new rotation operation equal to doing `self` and then `later`.
359    #[inline]
360    pub fn add(&self, later: Self) -> Self {
361        // r1 + r2 = (fl1 + ro1) + (fl2 + ro2) = fl1 + (ro1 + fl2) + ro2
362        // when fl2 = 0: ro1 + fl2 = fl2 + ro1
363        // when fl2 = 1: ro1 + fl2 = fl2 + (0b100 - ro1)
364        let (fl1, ro1) = self.fl_ro();
365        let (fl2, ro2) = later.fl_ro();
366
367        let fl = (fl1 + fl2) & 0b1_u8;
368        let ro = if fl2 == 1 {
369            0b100_u8 - ro1 + ro2
370        } else {
371            ro1 + ro2
372        };
373        let ro = ro & 0b11_u8;
374        Self::from_fl_ro(fl, ro)
375    }
376
377    /// Returns the reverse operation of this operation.
378    #[inline]
379    pub fn reverse(&self) -> Self {
380        let (fl, mut ro) = self.fl_ro();
381        if fl == 0_u8 {
382            ro = (0b100_u8 - ro) & 0b11_u8;
383        }
384        Self::from_fl_ro(fl, ro)
385    }
386
387    #[inline(always)]
388    fn fl_ro(&self) -> (u8, u8) {
389        let fl = match *self {
390            FlipHorizontal | FlipLeftDiagonal | FlipVertical | FlipRightDiagonal => 0b1_u8,
391            _ => 0b0_u8,
392        };
393        let ro = match *self {
394            Clockwise | FlipLeftDiagonal => 0b01_u8,
395            CentralSymmetric | FlipVertical => 0b10_u8,
396            Counterclockwise | FlipRightDiagonal => 0b11_u8,
397            _ => 0b00_u8,
398        };
399        (fl, ro)
400    }
401
402    #[inline(always)]
403    fn from_fl_ro(fl: u8, ro: u8) -> Self {
404        match (fl << 2_u8) | ro {
405            0b001_u8 => Clockwise,
406            0b010_u8 => CentralSymmetric,
407            0b011_u8 => Counterclockwise,
408            0b100_u8 => FlipHorizontal,
409            0b101_u8 => FlipLeftDiagonal,
410            0b110_u8 => FlipVertical,
411            0b111_u8 => FlipRightDiagonal,
412            _ => Original,
413        }
414    }
415}