magpie/othello/position.rs
1use crate::othello::{
2 Bitboard,
3 constants::{FILES, POSITIONS, POSITIONS_AS_NOTATION, RANKS},
4};
5
6#[cfg(feature = "serde")]
7use serde::{Deserialize, Serialize};
8
9/// Represents a single position on a 8x8 board as a `u64`.
10///
11/// Unlike the similar [`Bitboard`] which places no restrictions on the bits it
12/// represents, this struct represents exactly a single set bit.
13///
14/// Bitboard representations are quite inconvenient in some contexts which is
15/// why some convenience functions are provided to convert between different
16/// formats. In these contexts, MSB denotes A1 while LSB denotes H8, as can be
17/// seen in the graphic below.
18///
19/// ```text
20/// A B C D E F G H
21/// +----+----+----+----+----+----+----+----+
22/// 1 | 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 |
23/// +----+----+----+----+----+----+----+----+
24/// 2 | 08 | 09 | 10 | 11 | 12 | 13 | 14 | 15 |
25/// +----+----+----+----+----+----+----+----+
26/// 3 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
27/// +----+----+----+----+----+----+----+----+
28/// 4 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
29/// +----+----+----+----+----+----+----+----+
30/// 5 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
31/// +----+----+----+----+----+----+----+----+
32/// 6 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
33/// +----+----+----+----+----+----+----+----+
34/// 7 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
35/// +----+----+----+----+----+----+----+----+
36/// 8 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
37/// +----+----+----+----+----+----+----+----+
38/// ```
39///
40/// [`Bitboard`]: crate::othello::Bitboard
41#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
42#[derive(Clone, Copy, Debug, Default)]
43pub struct Position(pub(crate) u64);
44
45impl Position {
46 /// Constructs a new Position from a bitboard but does not check if
47 /// a single bit is set.
48 ///
49 /// # Examples
50 /// ```rust
51 /// use magpie::othello::Position;
52 ///
53 /// let b: Position = (1 << 32).try_into().unwrap();
54 /// assert_eq!(b.raw(), (1 << 32));
55 /// ```
56 pub(crate) fn new_unchecked(bitboard: u64) -> Self {
57 Self(bitboard)
58 }
59
60 /// Retrieves the underlying u64.
61 ///
62 /// # Examples
63 /// ```rust
64 /// use magpie::othello::Position;
65 ///
66 /// let p: Position = (1 << 32).try_into().unwrap();
67 /// assert_eq!(p.raw(), (1 << 32));
68 /// ```
69 #[must_use]
70 pub fn raw(self) -> u64 {
71 self.0
72 }
73
74 /// Calculates the zero-indexed rank the position is referring to.
75 ///
76 /// How ranks and files are represented can be found in the top-level
77 /// documentation for [`Position`].
78 ///
79 /// # Examples
80 /// ```rust
81 /// use magpie::othello::Position;
82 ///
83 /// let rank_and_file = (3, 4);
84 /// let p = Position::try_from(rank_and_file).unwrap();
85 /// assert_eq!(p.rank(), rank_and_file.0);
86 /// assert_eq!(p.file(), rank_and_file.1);
87 /// ```
88 ///
89 /// [`Position`]: crate::othello::Position
90 #[must_use]
91 pub fn rank(self) -> u8 {
92 (self.0.leading_zeros() / 8).try_into().unwrap()
93 }
94
95 /// Calculates the zero-indexed file the position is referring to.
96 ///
97 /// How ranks and files are represented can be found in the top-level
98 /// documentation for [`Position`].
99 ///
100 /// # Examples
101 /// ```rust
102 /// use magpie::othello::Position;
103 ///
104 /// let rank_and_file = (3, 4);
105 /// let p = Position::try_from(rank_and_file).unwrap();
106 /// assert_eq!(p.rank(), rank_and_file.0);
107 /// assert_eq!(p.file(), rank_and_file.1);
108 /// ```
109 ///
110 /// [`Position`]: crate::othello::Position
111 #[must_use]
112 pub fn file(self) -> u8 {
113 (self.0.leading_zeros() % 8).try_into().unwrap()
114 }
115
116 /// Calculates a human-readable board position.
117 ///
118 /// How board positions are represented can be found in the top-level
119 /// documentation for [`Position`].
120 ///
121 /// # Examples
122 /// ```rust
123 /// use magpie::othello::Position;
124 ///
125 /// let notation = "A1";
126 /// let p = Position::try_from(notation).unwrap();
127 /// assert_eq!(p.to_notation().to_lowercase(), notation.to_lowercase());
128 /// ```
129 ///
130 /// [`Position`]: crate::othello::Position
131 #[must_use]
132 pub fn to_notation(self) -> String {
133 POSITIONS_AS_NOTATION[self.0.leading_zeros() as usize].to_string()
134 }
135}
136
137impl From<Position> for Bitboard {
138 fn from(position: Position) -> Self {
139 Bitboard::from(position.0)
140 }
141}
142
143impl From<Position> for u64 {
144 fn from(position: Position) -> Self {
145 position.0
146 }
147}
148
149impl TryFrom<(u8, u8)> for Position {
150 type Error = PositionError;
151
152 /// Constructs a position from a zero-indexed rank and file pair.
153 ///
154 /// Returns an error if either the rank or file does not fit
155 /// into a 8x8 board.
156 ///
157 /// How ranks and files are represented can be found in the top-level
158 /// documentation for [`Position`].
159 ///
160 /// # Examples
161 /// ```rust
162 /// use magpie::othello::Position;
163 ///
164 /// let rank_and_file = (3, 4);
165 /// let p = Position::try_from(rank_and_file).unwrap();
166 /// assert_eq!(p.rank(), rank_and_file.0);
167 /// assert_eq!(p.file(), rank_and_file.1);
168 /// ```
169 ///
170 /// [`Position`]: crate::othello::Position
171 fn try_from(pair: (u8, u8)) -> Result<Self, Self::Error> {
172 let (rank, file) = pair;
173 if rank > 7 || file > 7 {
174 Err(PositionError::InvalidPosition)
175 } else {
176 let bitboard = RANKS[rank as usize] & FILES[file as usize];
177 Ok(Position::new_unchecked(bitboard))
178 }
179 }
180}
181
182impl TryFrom<String> for Position {
183 type Error = PositionError;
184
185 /// Constructs a position from human-readable notation.
186 ///
187 /// Returns an error if the notation is invalid.
188 ///
189 /// The conversion is case-insensitive.
190 ///
191 /// How board positions are represented can be found in the top-level
192 /// documentation for [`Position`].
193 ///
194 /// # Examples
195 /// ```rust
196 /// use magpie::othello::Position;
197 ///
198 /// let notation = "A1";
199 /// let p = Position::try_from(notation).unwrap();
200 /// assert_eq!(p.to_notation().to_lowercase(), notation.to_lowercase());
201 /// ```
202 ///
203 /// [`Position`]: crate::othello::Position
204 fn try_from(text: String) -> Result<Self, Self::Error> {
205 Position::try_from(text.as_ref())
206 }
207}
208
209impl TryFrom<&str> for Position {
210 type Error = PositionError;
211
212 /// Constructs a position from human-readable notation.
213 ///
214 /// Returns an error if the notation is invalid.
215 ///
216 /// The conversion is case-insensitive.
217 ///
218 /// How board positions are represented can be found in the top-level
219 /// documentation for [`Position`].
220 ///
221 /// # Examples
222 /// ```rust
223 /// use magpie::othello::Position;
224 ///
225 /// let notation = "A1";
226 /// let p = Position::try_from(notation).unwrap();
227 /// assert_eq!(p.to_notation().to_lowercase(), notation.to_lowercase());
228 /// ```
229 ///
230 /// [`Position`]: crate::othello::Position
231 fn try_from(text: &str) -> Result<Self, Self::Error> {
232 let text = text.to_lowercase();
233 let bitboard = POSITIONS_AS_NOTATION
234 .iter()
235 .position(|position| position == &text)
236 .map(|index| POSITIONS[index])
237 .ok_or(PositionError::InvalidPosition)?;
238 Ok(Position::new_unchecked(bitboard))
239 }
240}
241
242impl TryFrom<u64> for Position {
243 type Error = PositionError;
244
245 /// Constructs a position from a `u64`.
246 ///
247 /// Returns an error if the `u64` does not have exactly one bit set.
248 ///
249 /// # Examples
250 /// ```rust
251 /// use magpie::othello::Position;
252 ///
253 /// let num = 1 << 32;
254 /// let p = Position::try_from(num).unwrap();
255 /// assert_eq!(p.raw(), num);
256 /// ```
257 fn try_from(bitboard: u64) -> Result<Self, Self::Error> {
258 // Equivalent to bitboard.count_ones() == 1
259 // Suggested improvement from Clippy that might
260 // lead to negligible performance gains ¯\_(ツ)_/¯
261 if bitboard.is_power_of_two() {
262 Ok(Position::new_unchecked(bitboard))
263 } else {
264 Err(PositionError::NotOneHotBitboard)
265 }
266 }
267}
268
269impl TryFrom<Bitboard> for Position {
270 type Error = PositionError;
271
272 /// Constructs a position from a [`Bitboard`].
273 ///
274 /// Returns an error if the [`Bitboard`] does not have exactly one bit set.
275 ///
276 /// # Examples
277 /// ```rust
278 /// use magpie::othello::{Bitboard, Position};
279 ///
280 /// let bitboard = Bitboard::from(1 << 32);
281 /// let p = Position::try_from(bitboard).unwrap();
282 /// assert_eq!(p.raw(), bitboard);
283 /// ```
284 ///
285 /// [`Bitboard`]: crate::othello::Bitboard
286 fn try_from(bitboard: Bitboard) -> Result<Self, Self::Error> {
287 Position::try_from(bitboard.raw())
288 }
289}
290
291/// This enum represents errors that may occur when handling Positions.
292#[derive(Debug, Eq, PartialEq, Hash)]
293pub enum PositionError {
294 /// Indicates that the bitboard did not contain exactly one set bit.
295 NotOneHotBitboard,
296 /// Indicates that the position could not be parsed.
297 InvalidPosition,
298}