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 {}