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}