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}