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}