Skip to main content

chess_lab/core/
position.rs

1use std::{fmt::Display, ops};
2
3use crate::errors::{PositionInvalidError, PositionOutOfRangeError};
4
5/// Represents a position on the board.
6/// The position is represented by a column and a row.
7///
8/// # Examples
9/// ```
10/// use chess_lab::core::Position;
11///
12/// let pos = Position::new(0, 0).unwrap();
13///
14/// assert_eq!(pos.to_string(), "a1");
15///
16/// let pos = Position::from_string("a1").unwrap();
17///
18/// assert_eq!(pos.col, 0);
19/// assert_eq!(pos.row, 0);
20/// ```
21///
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub struct Position {
24    /// The column of the position (between 0 and 7)
25    pub col: u8,
26    /// The row of the position (between 0 and 7)
27    pub row: u8,
28}
29
30impl Position {
31    /// Creates a new position
32    ///
33    /// # Arguments
34    /// * `col`: The column of the position (between 0 and 7)
35    /// * `row`: The row of the position (between 0 and 7)
36    ///
37    /// # Returns
38    /// * `Ok(Position)`: The new position
39    /// * `Err(PositionOutOfRangeError)`: If the position is out of range
40    ///
41    /// # Examples
42    /// ```
43    /// use chess_lab::core::Position;
44    ///
45    /// let pos = Position::new(0, 0).unwrap();
46    ///
47    /// assert_eq!(pos.col, 0);
48    /// assert_eq!(pos.row, 0);
49    /// ```
50    ///
51    pub fn new(col: u8, row: u8) -> Result<Position, PositionOutOfRangeError> {
52        if col >= 8 || row >= 8 {
53            return Err(PositionOutOfRangeError::new(col, row));
54        }
55        Ok(Position { col, row })
56    }
57
58    /// Creates a new position from a string
59    ///
60    /// # Arguments
61    /// * `s`: The string representation of the position
62    ///
63    /// # Returns
64    /// * `Ok(Position)`: The new position
65    /// * `Err(PositionInvalidError)`: If the string is invalid
66    ///
67    /// # Examples
68    /// ```
69    /// use chess_lab::core::Position;
70    ///
71    /// let pos = Position::from_string("a1").unwrap();
72    ///
73    /// assert_eq!(pos.col, 0);
74    /// assert_eq!(pos.row, 0);
75    /// ```
76    ///
77    pub fn from_string(s: &str) -> Result<Position, PositionInvalidError> {
78        if s.len() != 2 {
79            return Err(PositionInvalidError::new(s.to_string()));
80        }
81
82        let col = s.chars().nth(0).unwrap() as u8 - 'a' as u8;
83        let row = s.chars().nth(1).unwrap() as u8 - '1' as u8;
84        Position::new(col, row).map_err(|_| PositionInvalidError::new(s.to_string()))
85    }
86
87    /// Converts the position to a string
88    ///
89    /// # Returns
90    /// The string representation of the position
91    ///
92    /// # Examples
93    /// ```
94    /// use chess_lab::core::Position;
95    ///
96    /// let pos = Position::new(0, 0).unwrap();
97    ///
98    /// assert_eq!(pos.to_bitboard(), 0x0000000000000001);
99    /// ```
100    ///
101    pub fn to_bitboard(&self) -> u64 {
102        1 << (self.row * 8 + self.col)
103    }
104
105    /// Converts a bitboard to a list of positions
106    ///
107    /// # Arguments
108    /// * `bitboard`: The bitboard to convert
109    ///
110    /// # Returns
111    /// A list of positions
112    ///
113    /// # Examples
114    /// ```
115    /// use chess_lab::core::Position;
116    ///
117    /// let positions = Position::from_bitboard(0x0000000000000001);
118    ///
119    /// assert_eq!(positions.len(), 1);
120    /// assert_eq!(positions[0].to_string(), "a1");
121    /// ```
122    ///
123    pub fn from_bitboard(bitboard: u64) -> Vec<Position> {
124        let mut positions = Vec::new();
125        let mut bitboard = bitboard;
126        while bitboard != 0 {
127            let pos = bitboard.trailing_zeros() as u8;
128            positions.push(Position::new(pos % 8, pos / 8).unwrap());
129            bitboard &= bitboard - 1;
130        }
131        positions
132    }
133
134    /// Gets the direction between two positions
135    ///
136    /// # Arguments
137    /// * `other`: The other position
138    ///
139    /// # Returns
140    /// The direction between the two positions
141    ///
142    /// # Examples
143    /// ```
144    /// use chess_lab::core::Position;
145    ///
146    /// let pos1 = Position::new(0, 0).unwrap();
147    /// let pos2 = Position::new(1, 1).unwrap();
148    ///
149    /// let direction = pos1.direction(&pos2);
150    ///
151    /// assert_eq!(direction, (1, 1));
152    /// ```
153    pub fn direction(&self, other: &Position) -> (i8, i8) {
154        let mut col = other.col as i8 - self.col as i8;
155        let mut row = other.row as i8 - self.row as i8;
156
157        col = if col == 0 { 0 } else { col / col.abs() };
158        row = if row == 0 { 0 } else { row / row.abs() };
159        (col, row)
160    }
161}
162
163impl ops::Add<(i8, i8)> for &Position {
164    type Output = Position;
165
166    /// Adds a certain offset to the position
167    ///
168    /// # Arguments
169    /// * `other`: The offset to add
170    ///
171    /// # Returns
172    /// The new position
173    ///
174    /// # Examples
175    /// ```
176    /// use chess_lab::core::Position;
177    ///
178    /// let pos = Position::new(0, 0).unwrap();
179    /// let new_pos = &pos + (1, 1);
180    ///
181    /// assert_eq!(new_pos.col, 1);
182    /// assert_eq!(new_pos.row, 1);
183    /// ```
184    ///
185    fn add(self, other: (i8, i8)) -> Position {
186        Position::new(
187            (self.col as i8 + other.0) as u8,
188            (self.row as i8 + other.1) as u8,
189        )
190        .unwrap()
191    }
192}
193
194impl ops::Sub<&Position> for &Position {
195    type Output = (i8, i8);
196
197    /// Gets the offset between two positions
198    ///
199    /// # Arguments
200    /// * `other`: The other position
201    ///
202    /// # Returns
203    /// The offset between the two positions
204    ///
205    /// # Examples
206    /// ```
207    /// use chess_lab::core::Position;
208    ///
209    /// let pos1 = Position::new(0, 0).unwrap();
210    /// let pos2 = Position::new(1, 1).unwrap();
211    ///
212    /// let offset = &pos1 - &pos2;
213    ///
214    /// assert_eq!(offset, (-1, -1));
215    /// ```
216    ///
217    fn sub(self, other: &Position) -> (i8, i8) {
218        (
219            self.col as i8 - other.col as i8,
220            self.row as i8 - other.row as i8,
221        )
222    }
223}
224
225impl ops::Sub<(i8, i8)> for &Position {
226    type Output = Position;
227
228    /// Subtracts a certain offset from the position
229    ///
230    /// # Arguments
231    /// * `other`: The offset to subtract
232    ///
233    /// # Returns
234    /// The new position
235    ///
236    /// # Examples
237    /// ```
238    /// use chess_lab::core::Position;
239    ///
240    /// let pos = Position::new(1, 1).unwrap();
241    /// let new_pos = &pos - (1, 1);
242    ///
243    /// assert_eq!(new_pos.col, 0);
244    /// assert_eq!(new_pos.row, 0);
245    /// ```
246    ///
247    fn sub(self, other: (i8, i8)) -> Position {
248        Position {
249            col: (self.col as i8 - other.0) as u8,
250            row: (self.row as i8 - other.1) as u8,
251        }
252    }
253}
254
255impl Display for Position {
256    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        write!(
258            f,
259            "{}{}",
260            ('a' as u8 + self.col) as char,
261            ('1' as u8 + self.row) as char
262        )
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_position() {
272        let pos = Position::new(0, 0).unwrap();
273        assert_eq!(pos.to_string(), "a1");
274        let pos = Position::new(7, 7).unwrap();
275        assert_eq!(pos.to_string(), "h8");
276        let pos = Position::from_string("a1").unwrap();
277        assert_eq!(pos.col, 0);
278        assert_eq!(pos.row, 0);
279        let pos = Position::from_string("h8").unwrap();
280        assert_eq!(pos.col, 7);
281        assert_eq!(pos.row, 7);
282    }
283
284    #[test]
285    fn test_position_invalid() {
286        let pos = Position::new(8, 0);
287        assert!(pos.is_err());
288        let pos = Position::new(0, 8);
289        assert!(pos.is_err());
290        let pos = Position::from_string("i1");
291        assert!(pos.is_err());
292        let pos = Position::from_string("a9");
293        assert!(pos.is_err());
294        let pos = Position::from_string("a");
295        assert!(pos.is_err());
296        let pos = Position::from_string("a12");
297        assert!(pos.is_err());
298    }
299
300    #[test]
301    fn test_position_to_bitboard() {
302        let pos = Position::new(0, 0).unwrap();
303        assert_eq!(pos.to_bitboard(), 0x0000000000000001);
304        let pos = Position::new(7, 7).unwrap();
305        assert_eq!(pos.to_bitboard(), 0x8000000000000000);
306    }
307
308    #[test]
309    fn test_position_from_bitboard() {
310        let positions = Position::from_bitboard(0x0000000000000001);
311        let pos = positions.first().unwrap();
312        assert_eq!(pos.to_string(), "a1");
313        assert_eq!(positions.len(), 1);
314        let positions = Position::from_bitboard(0x8000000000000000);
315        let pos = positions.first().unwrap();
316        assert_eq!(pos.to_string(), "h8");
317        assert_eq!(positions.len(), 1);
318        let positions = Position::from_bitboard(0xFFFFFFFFFFFFFFFF);
319        assert_eq!(positions.len(), 64);
320    }
321
322    #[test]
323    fn test_position_sub() {
324        let pos1 = Position::new(1, 1).unwrap();
325        let pos2 = Position::new(0, 0).unwrap();
326        let pos3 = &pos1 - &pos2;
327        assert_eq!(pos3.0, 1);
328        assert_eq!(pos3.1, 1);
329    }
330
331    #[test]
332    fn test_position_add_tuple() {
333        let pos1 = Position::new(0, 0).unwrap();
334        let pos2 = (1, 1);
335        let pos3 = &pos1 + pos2;
336        assert_eq!(pos3.to_string(), "b2");
337    }
338
339    #[test]
340    fn test_position_sub_tuple() {
341        let pos1 = Position::new(1, 1).unwrap();
342        let pos2 = (1, 1);
343        let pos3 = &pos1 - pos2;
344        assert_eq!(pos3.to_string(), "a1");
345    }
346
347    #[test]
348    fn test_direction() {
349        let pos1 = Position::new(0, 0).unwrap();
350        let pos2 = Position::new(1, 1).unwrap();
351        let dir = pos1.direction(&pos2);
352        assert_eq!(dir, (1, 1));
353    }
354}