Skip to main content

ai_hex/
board.rs

1use std::fmt;
2use std::str::FromStr;
3
4pub const MIN_SIZE: u8 = 9;
5pub const MAX_SIZE: u8 = 24;
6pub const STRIDE: u16 = MAX_SIZE as u16;
7pub const MAX_CELLS: usize = MAX_SIZE as usize * MAX_SIZE as usize;
8
9#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
10pub struct BoardSize(u8);
11
12impl BoardSize {
13    pub fn new(size: u8) -> Result<Self, BoardSizeError> {
14        if (MIN_SIZE..=MAX_SIZE).contains(&size) {
15            Ok(Self(size))
16        } else {
17            Err(BoardSizeError(size))
18        }
19    }
20
21    pub const fn get(self) -> u8 {
22        self.0
23    }
24
25    #[cfg(test)]
26    pub(crate) const fn for_test(size: u8) -> Self {
27        assert!(size > 0 && size <= MAX_SIZE);
28        Self(size)
29    }
30
31    pub const fn cell_count(self) -> u16 {
32        self.0 as u16 * self.0 as u16
33    }
34
35    pub(crate) const fn contains(self, cell: Cell) -> bool {
36        cell.file() < self.0 && cell.rank() < self.0
37    }
38}
39
40impl Default for BoardSize {
41    fn default() -> Self {
42        Self(13)
43    }
44}
45
46#[derive(Clone, Copy, Debug, Eq, PartialEq)]
47pub struct BoardSizeError(u8);
48
49impl fmt::Display for BoardSizeError {
50    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
51        write!(
52            formatter,
53            "board size must be from {MIN_SIZE} through {MAX_SIZE}"
54        )
55    }
56}
57
58impl std::error::Error for BoardSizeError {}
59
60#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
61pub struct Cell(u16);
62
63impl Cell {
64    pub const fn new(file: u8, rank: u8) -> Option<Self> {
65        if file < MAX_SIZE && rank < MAX_SIZE {
66            Some(Self::from_coords(file, rank))
67        } else {
68            None
69        }
70    }
71
72    pub(crate) const fn from_coords(file: u8, rank: u8) -> Self {
73        Self(rank as u16 * STRIDE + file as u16)
74    }
75
76    pub(crate) const fn from_index(index: u16) -> Self {
77        Self(index)
78    }
79
80    pub(crate) fn from_dense(index: u16, size: BoardSize) -> Self {
81        let width = u16::from(size.get());
82        Self::from_coords((index % width) as u8, (index / width) as u8)
83    }
84
85    pub const fn file(self) -> u8 {
86        (self.0 % STRIDE) as u8
87    }
88
89    pub const fn rank(self) -> u8 {
90        (self.0 / STRIDE) as u8
91    }
92
93    pub const fn index(self) -> u16 {
94        self.0
95    }
96}
97
98impl fmt::Display for Cell {
99    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
100        let file = char::from(b'a' + self.file());
101        write!(formatter, "{file}{}", self.rank() + 1)
102    }
103}
104
105#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
106pub enum Move {
107    Place(Cell),
108    Swap,
109}
110
111impl Move {
112    pub const fn place(file: u8, rank: u8) -> Option<Self> {
113        match Cell::new(file, rank) {
114            Some(cell) => Some(Self::Place(cell)),
115            None => None,
116        }
117    }
118
119    pub const fn cell(self) -> Option<Cell> {
120        match self {
121            Self::Place(cell) => Some(cell),
122            Self::Swap => None,
123        }
124    }
125
126    pub(crate) const fn order_key(self) -> u16 {
127        match self {
128            Self::Place(cell) => cell.index(),
129            Self::Swap => u16::MAX,
130        }
131    }
132}
133
134impl fmt::Display for Move {
135    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            Self::Place(cell) => cell.fmt(formatter),
138            Self::Swap => formatter.write_str("swap"),
139        }
140    }
141}
142
143impl FromStr for Move {
144    type Err = ParseMoveError;
145
146    fn from_str(value: &str) -> Result<Self, Self::Err> {
147        if value.eq_ignore_ascii_case("swap") {
148            return Ok(Self::Swap);
149        }
150        let bytes = value.as_bytes();
151        if !(2..=3).contains(&bytes.len()) || !bytes[0].is_ascii_alphabetic() {
152            return Err(ParseMoveError);
153        }
154        let file = bytes[0].to_ascii_lowercase().wrapping_sub(b'a');
155        if file >= MAX_SIZE {
156            return Err(ParseMoveError);
157        }
158        let rank = value[1..].parse::<u8>().map_err(|_| ParseMoveError)?;
159        if !(1..=MAX_SIZE).contains(&rank) {
160            return Err(ParseMoveError);
161        }
162        Ok(Self::Place(Cell::from_coords(file, rank - 1)))
163    }
164}
165
166#[derive(Clone, Copy, Debug, Eq, PartialEq)]
167pub struct ParseMoveError;
168
169impl fmt::Display for ParseMoveError {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        formatter.write_str("expected a cell from a1 through x24, or swap")
172    }
173}
174
175impl std::error::Error for ParseMoveError {}