Skip to main content

chess_lab/logic/
board.rs

1use std::fmt::Display;
2
3use crate::{
4    core::{piece_movement, Color, Piece, PieceType, Position},
5    errors::{
6        FenError, PositionBetweenError, PositionEmptyError, PositionOccupiedError,
7        UnalignedPositionsError,
8    },
9    parsing::fen::parse_minified_fen,
10    utils::movements::{diagonal_movement, linear_movement},
11};
12
13/// A struct that represents a chess board
14/// The board is represented by bitboards of each piece (color and type)
15///
16#[derive(Debug, Clone)]
17pub struct Board {
18    /// Bitboard of white pawns
19    wpawns: u64,
20    /// Bitboard of black pawns
21    bpawns: u64,
22    /// Bitboard of white knights
23    wknights: u64,
24    /// Bitboard of black knights
25    bknights: u64,
26    /// Bitboard of white bishops
27    wbishops: u64,
28    /// Bitboard of black bishops
29    bbishops: u64,
30    /// Bitboard of white rooks
31    wrooks: u64,
32    /// Bitboard of black rooks
33    brooks: u64,
34    /// Bitboard of white queens
35    wqueens: u64,
36    /// Bitboard of black queens
37    bqueens: u64,
38    /// Bitboard of white kings
39    wkings: u64,
40    /// Bitboard of black kings
41    bkings: u64,
42}
43
44impl Default for Board {
45    /// Creates a new board with the default starting position
46    ///
47    /// # Returns
48    /// A new board with the default starting position
49    ///
50    fn default() -> Board {
51        Board {
52            wpawns: 0x000000000000FF00,
53            bpawns: 0x00FF000000000000,
54            wknights: 0x0000000000000042,
55            bknights: 0x4200000000000000,
56            wbishops: 0x0000000000000024,
57            bbishops: 0x2400000000000000,
58            wrooks: 0x0000000000000081,
59            brooks: 0x8100000000000000,
60            wqueens: 0x0000000000000008,
61            bqueens: 0x0800000000000000,
62            wkings: 0x0000000000000010,
63            bkings: 0x1000000000000000,
64        }
65    }
66}
67
68impl Board {
69    /// Creates a new board from a FEN string
70    ///
71    /// # Arguments
72    /// * `fen`: A FEN string representing the board
73    ///
74    /// # Returns
75    /// * `Ok(Board)`: A new board with the position represented by the FEN string
76    /// * `Err(FenError)`: If the FEN string is invalid
77    ///
78    pub fn new(fen: &str) -> Result<Board, FenError> {
79        Board::from_fen(fen)
80    }
81
82    /// Creates a new empty board
83    ///
84    /// # Returns
85    /// A new empty board
86    ///
87    pub fn empty() -> Board {
88        Board {
89            wpawns: 0,
90            bpawns: 0,
91            wknights: 0,
92            bknights: 0,
93            wbishops: 0,
94            bbishops: 0,
95            wrooks: 0,
96            brooks: 0,
97            wqueens: 0,
98            bqueens: 0,
99            wkings: 0,
100            bkings: 0,
101        }
102    }
103
104    /// Creates a new board from a FEN string
105    ///
106    /// # Arguments
107    /// * `fen`: A FEN string representing the board
108    ///
109    /// # Returns
110    /// A `Result<Board, FenError>` object
111    /// * `Ok(Board)`: A new board with the position represented by the FEN string
112    /// * `Err(FenError)`: If the FEN string is invalid
113    ///
114    pub fn from_fen(fen: &str) -> Result<Board, FenError> {
115        parse_minified_fen(fen)
116    }
117
118    /// Checks if a position is occupied by a piece
119    ///
120    /// # Arguments
121    /// * `pos`: The position to check
122    ///
123    /// # Returns
124    /// Whether the position is occupied by a piece
125    ///
126    pub fn is_ocupied(&self, pos: &Position) -> bool {
127        let bit = pos.to_bitboard();
128        (self.wpawns
129            | self.bpawns
130            | self.wknights
131            | self.bknights
132            | self.wbishops
133            | self.bbishops
134            | self.wrooks
135            | self.brooks
136            | self.wqueens
137            | self.bqueens
138            | self.wkings
139            | self.bkings)
140            & bit
141            != 0
142    }
143
144    /// Gets the piece at a position
145    ///
146    /// # Arguments
147    /// * `pos`: The position to get the piece
148    ///
149    /// # Returns
150    /// The piece at the position or None if the position is empty
151    ///
152    pub fn get_piece(&self, pos: &Position) -> Option<Piece> {
153        let bit = pos.to_bitboard();
154
155        let piece_data = [
156            (self.wpawns, Color::White, PieceType::Pawn),
157            (self.bpawns, Color::Black, PieceType::Pawn),
158            (self.wknights, Color::White, PieceType::Knight),
159            (self.bknights, Color::Black, PieceType::Knight),
160            (self.wbishops, Color::White, PieceType::Bishop),
161            (self.bbishops, Color::Black, PieceType::Bishop),
162            (self.wrooks, Color::White, PieceType::Rook),
163            (self.brooks, Color::Black, PieceType::Rook),
164            (self.wqueens, Color::White, PieceType::Queen),
165            (self.bqueens, Color::Black, PieceType::Queen),
166            (self.wkings, Color::White, PieceType::King),
167            (self.bkings, Color::Black, PieceType::King),
168        ];
169
170        match piece_data.iter().find(|(board, _, _)| *board & bit != 0) {
171            Some((_, color, piece_type)) => Some(Piece::new(*color, *piece_type)),
172            None => None,
173        }
174    }
175
176    /// Sets a piece at a position
177    ///
178    /// # Arguments
179    /// * `piece`: The piece to set
180    /// * `pos`: The position to set the piece
181    ///
182    /// # Returns
183    /// * `Ok(())`: If the piece was set successfully
184    /// * `Err(PositionOccupiedError)`: If the position is already occupied
185    ///
186    pub fn set_piece(&mut self, piece: Piece, pos: &Position) -> Result<(), PositionOccupiedError> {
187        if self.is_ocupied(pos) {
188            return Err(PositionOccupiedError::new(pos.clone()));
189        }
190        let bit = pos.to_bitboard();
191        match piece.piece_type {
192            PieceType::Pawn => match piece.color {
193                Color::White => self.wpawns |= bit,
194                Color::Black => self.bpawns |= bit,
195            },
196            PieceType::Knight => match piece.color {
197                Color::White => self.wknights |= bit,
198                Color::Black => self.bknights |= bit,
199            },
200            PieceType::Bishop => match piece.color {
201                Color::White => self.wbishops |= bit,
202                Color::Black => self.bbishops |= bit,
203            },
204            PieceType::Rook => match piece.color {
205                Color::White => self.wrooks |= bit,
206                Color::Black => self.brooks |= bit,
207            },
208            PieceType::Queen => match piece.color {
209                Color::White => self.wqueens |= bit,
210                Color::Black => self.bqueens |= bit,
211            },
212            PieceType::King => match piece.color {
213                Color::White => self.wkings |= bit,
214                Color::Black => self.bkings |= bit,
215            },
216        }
217        Ok(())
218    }
219
220    /// Deletes a piece at a position
221    ///
222    /// # Arguments
223    /// * `pos`: The position to delete the piece
224    ///
225    /// # Returns
226    /// * `Ok(Piece)`: The piece that was deleted
227    /// * `Err(PositionEmptyError)`: If the position is empty
228    ///
229    pub fn delete_piece(&mut self, pos: &Position) -> Result<Piece, PositionEmptyError> {
230        let piece = match self.get_piece(&pos) {
231            Some(piece) => piece,
232            None => return Err(PositionEmptyError::new(pos.clone())),
233        };
234
235        let bit = pos.to_bitboard();
236        match piece.piece_type {
237            PieceType::Pawn => match piece.color {
238                Color::White => self.wpawns &= !bit,
239                Color::Black => self.bpawns &= !bit,
240            },
241            PieceType::Knight => match piece.color {
242                Color::White => self.wknights &= !bit,
243                Color::Black => self.bknights &= !bit,
244            },
245            PieceType::Bishop => match piece.color {
246                Color::White => self.wbishops &= !bit,
247                Color::Black => self.bbishops &= !bit,
248            },
249            PieceType::Rook => match piece.color {
250                Color::White => self.wrooks &= !bit,
251                Color::Black => self.brooks &= !bit,
252            },
253            PieceType::Queen => match piece.color {
254                Color::White => self.wqueens &= !bit,
255                Color::Black => self.bqueens &= !bit,
256            },
257            PieceType::King => match piece.color {
258                Color::White => self.wkings &= !bit,
259                Color::Black => self.bkings &= !bit,
260            },
261        }
262        Ok(piece)
263    }
264
265    /// Finds all pieces of a certain type and color
266    ///
267    /// # Arguments
268    /// * `piece_type`: The type of the piece
269    /// * `color`: The color of the piece
270    ///
271    /// # Returns
272    /// A vector of positions of the pieces
273    ///
274    pub fn find(&self, piece_type: PieceType, color: Color) -> Vec<Position> {
275        let bitboard;
276        match piece_type {
277            PieceType::Pawn => match color {
278                Color::White => bitboard = self.wpawns,
279                Color::Black => bitboard = self.bpawns,
280            },
281            PieceType::Knight => match color {
282                Color::White => bitboard = self.wknights,
283                Color::Black => bitboard = self.bknights,
284            },
285            PieceType::Bishop => match color {
286                Color::White => bitboard = self.wbishops,
287                Color::Black => bitboard = self.bbishops,
288            },
289            PieceType::Rook => match color {
290                Color::White => bitboard = self.wrooks,
291                Color::Black => bitboard = self.brooks,
292            },
293            PieceType::Queen => match color {
294                Color::White => bitboard = self.wqueens,
295                Color::Black => bitboard = self.bqueens,
296            },
297            PieceType::King => match color {
298                Color::White => bitboard = self.wkings,
299                Color::Black => bitboard = self.bkings,
300            },
301        }
302        Position::from_bitboard(bitboard)
303    }
304
305    /// Finds all pieces of a certain color
306    ///
307    /// # Arguments
308    /// * `color`: The color of the pieces
309    ///
310    /// # Returns
311    /// A vector of positions of the pieces
312    ///
313    pub fn find_all(&self, color: Color) -> Vec<Position> {
314        let mut pieces = Vec::new();
315        pieces.append(&mut self.find(PieceType::Pawn, color));
316        pieces.append(&mut self.find(PieceType::Knight, color));
317        pieces.append(&mut self.find(PieceType::Bishop, color));
318        pieces.append(&mut self.find(PieceType::Rook, color));
319        pieces.append(&mut self.find(PieceType::Queen, color));
320        pieces.append(&mut self.find(PieceType::King, color));
321        pieces
322    }
323
324    /// Moves a piece from one position to another
325    ///
326    /// # Arguments
327    /// * `from`: The position to move the piece from
328    /// * `to`: The position to move the piece to
329    ///
330    /// # Returns
331    /// * `Ok(())`: If the piece was moved successfully
332    /// * `Err(PositionEmptyError)`: If the start position is empty
333    ///
334    pub fn move_piece(&mut self, from: &Position, to: &Position) -> Result<(), PositionEmptyError> {
335        let piece = self.delete_piece(from)?;
336
337        self.delete_piece(to).ok();
338
339        self.set_piece(piece, to).unwrap(); // safe unwrap
340        Ok(())
341    }
342
343    /// Checks if a position is attacked by a certain color
344    ///
345    /// # Arguments
346    /// * `pos`: The position to check
347    /// * `color`: The color of the attacking pieces
348    ///
349    /// # Returns
350    /// Whether the position is attacked or not
351    ///
352    pub fn is_attacked(&self, pos: Position, color: Color) -> bool {
353        let pieces = self.find_all(color);
354        for piece in pieces {
355            if self.is_attacking(&piece, &pos).unwrap() {
356                // safe unwrap (depends on find_all method)
357                return true;
358            }
359        }
360        false
361    }
362
363    /// Checks if a piece can move without considering checks
364    ///
365    /// # Arguments
366    /// * `start_pos`: The position of the piece to move
367    /// * `end_pos`: The position of the piece to capture
368    ///
369    /// # Returns
370    /// * `Ok(bool)`: Whether the piece can move acording to its movement rules
371    /// * `Err(PositionEmptyError)`: If the start position is empty
372    ///
373    pub fn can_move(
374        &self,
375        start_pos: &Position,
376        end_pos: &Position,
377    ) -> Result<bool, PositionEmptyError> {
378        let piece = self
379            .get_piece(start_pos)
380            .ok_or(PositionEmptyError::new(start_pos.clone()))?;
381
382        let captured_piece = self.get_piece(end_pos);
383        match captured_piece {
384            Some(captured_piece) => {
385                if captured_piece.color == piece.color {
386                    return Ok(false);
387                }
388            }
389            None => (),
390        }
391
392        if piece_movement(&piece, start_pos, end_pos) {
393            return match piece.piece_type {
394                PieceType::Pawn => {
395                    if start_pos.col == end_pos.col {
396                        return Ok(!self.piece_between(start_pos, end_pos).unwrap()
397                            && captured_piece.is_none()); // safe unwrap (depends on piece_movement)
398                    }
399                    Ok(captured_piece.is_some())
400                }
401                PieceType::Knight | PieceType::King => Ok(true),
402                PieceType::Bishop | PieceType::Rook | PieceType::Queen => {
403                    Ok(!self.piece_between(start_pos, end_pos).unwrap()) // safe unwrap (depends on piece_movement)
404                }
405            };
406        }
407        Ok(false)
408    }
409
410    /// Checks if a piece of a postion is attacking a position
411    /// The function does not check if the move is legal
412    /// It only checks if the piece is attacking the position according to its movement rules
413    ///
414    /// # Arguments
415    /// * `start_pos`: The position of the piece to move
416    /// * `end_pos`: The position of the piece to capture
417    ///
418    /// # Returns
419    /// * `Ok(bool)`: Whether the piece is attacking the position
420    /// * `Err(PositionEmptyError)`: If the start position is empty
421    ///
422    pub fn is_attacking(
423        &self,
424        start_pos: &Position,
425        end_pos: &Position,
426    ) -> Result<bool, PositionEmptyError> {
427        let piece = self
428            .get_piece(start_pos)
429            .ok_or(PositionEmptyError::new(start_pos.clone()))?;
430
431        if piece_movement(&piece, start_pos, end_pos) {
432            return match piece.piece_type {
433                PieceType::Pawn => Ok(diagonal_movement(start_pos, end_pos)),
434                PieceType::Knight | PieceType::King => Ok(true),
435                PieceType::Bishop | PieceType::Rook | PieceType::Queen => {
436                    Ok(!self.piece_between(start_pos, end_pos).unwrap()) // safe unwrap (depends on piece_movement)
437                }
438            };
439        }
440        Ok(false)
441    }
442
443    /// Checks if there is a piece between two positions
444    ///
445    /// # Arguments
446    /// * `from`: The starting position
447    /// * `to`: The ending position
448    ///
449    /// # Returns
450    /// * `Ok(bool)`: Whether there is a piece between the two positions
451    /// * `Err(PositionBetweenError)`: If the positions are not aligned
452    ///
453    pub fn piece_between(
454        &self,
455        from: &Position,
456        to: &Position,
457    ) -> Result<bool, PositionBetweenError> {
458        if !linear_movement(from, to) && !diagonal_movement(from, to) {
459            return Err(PositionBetweenError::from(UnalignedPositionsError::new(
460                from.clone(),
461                to.clone(),
462            )));
463        }
464
465        let direction = from.direction(to);
466        let mut pos = from.to_owned();
467
468        for _ in 0..7 {
469            pos = &pos + direction;
470            if pos == *to {
471                break;
472            }
473            if self.is_ocupied(&pos) {
474                return Ok(true);
475            }
476        }
477        Ok(false)
478    }
479}
480
481impl Display for Board {
482    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
483        let mut board = String::new();
484        for row in (0..8).rev() {
485            let mut empty = 0;
486            for col in 0..8 {
487                let pos = Position::new(col, row).unwrap(); // safe unwrap
488                let piece = self.get_piece(&pos);
489                match piece {
490                    None => {
491                        empty += 1;
492                    }
493                    Some(piece) => {
494                        if empty > 0 {
495                            board.push_str(&empty.to_string());
496                            empty = 0;
497                        }
498                        board.push_str(&piece.to_string());
499                    }
500                }
501            }
502            if empty > 0 {
503                board.push_str(&empty.to_string());
504            }
505            if row > 0 {
506                board.push('/');
507            }
508        }
509        write!(f, "{}", board)
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516    use crate::core::{Color, Piece, PieceType, Position};
517
518    #[test]
519    fn test_default() {
520        let board = Board::default();
521        assert_eq!(
522            board.to_string(),
523            "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
524        );
525    }
526
527    #[test]
528    fn test_from_fen() {
529        let board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR").unwrap();
530        assert_eq!(
531            board.to_string(),
532            "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
533        );
534    }
535
536    #[test]
537    fn test_to_fen() {
538        let board = Board::default();
539        assert_eq!(
540            board.to_string(),
541            "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR"
542        );
543    }
544
545    #[test]
546    fn test_set_piece() {
547        let mut board = Board::default();
548        let pos = Position::new(4, 2).unwrap();
549        let piece = Piece::new(Color::White, PieceType::Pawn);
550        board.set_piece(piece, &pos).unwrap();
551        assert_eq!(
552            board.to_string(),
553            "rnbqkbnr/pppppppp/8/8/8/4P3/PPPPPPPP/RNBQKBNR"
554        );
555    }
556
557    #[test]
558    fn test_set_piece_occupied() {
559        let mut board = Board::default();
560        let pos = Position::new(0, 1).unwrap();
561        let piece = Piece::new(Color::White, PieceType::Pawn);
562        let result = board.set_piece(piece, &pos);
563        assert!(result.is_err());
564    }
565
566    #[test]
567    fn test_delete_piece() {
568        let mut board = Board::default();
569        let pos = Position::new(0, 0).unwrap();
570        board.delete_piece(&pos).unwrap();
571        assert_eq!(
572            board.to_string(),
573            "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/1NBQKBNR"
574        );
575    }
576
577    #[test]
578    fn test_get_piece() {
579        let board = Board::default();
580        let pos = Position::new(0, 0).unwrap();
581        let piece = board.get_piece(&pos).unwrap();
582        assert_eq!(piece.to_string(), "R");
583    }
584
585    #[test]
586    fn test_is_ocupied() {
587        let board = Board::default();
588        let pos = Position::new(0, 0).unwrap();
589        assert!(board.is_ocupied(&pos));
590
591        let pos = Position::new(0, 2).unwrap();
592        assert!(!board.is_ocupied(&pos));
593    }
594
595    #[test]
596    fn test_find() {
597        let board = Board::default();
598        let pieces = board.find(PieceType::Pawn, Color::White);
599        assert_eq!(pieces.len(), 8);
600    }
601
602    #[test]
603    fn test_find_all() {
604        let board = Board::default();
605        let pieces = board.find_all(Color::White);
606        assert_eq!(pieces.len(), 16);
607    }
608
609    #[test]
610    fn test_move_piece() {
611        let mut board = Board::default();
612        let from = Position::new(4, 1).unwrap();
613        let to = Position::new(4, 3).unwrap();
614        board.move_piece(&from, &to).unwrap();
615        assert_eq!(
616            board.to_string(),
617            "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR"
618        );
619    }
620
621    #[test]
622    fn test_is_attacked() {
623        let board = Board::default();
624
625        let pos = Position::from_string("e3").unwrap();
626        assert!(board.is_attacked(pos, Color::White));
627
628        let pos = Position::from_string("e4").unwrap();
629        assert!(!board.is_attacked(pos, Color::White));
630    }
631
632    #[test]
633    fn test_piece_between() {
634        let board = Board::default();
635        let from = Position::new(0, 0).unwrap();
636        let to = Position::new(0, 6).unwrap();
637        assert!(board.piece_between(&from, &to).unwrap());
638    }
639}