Skip to main content

chess_lab/core/
chess_move.rs

1use std::{collections::HashMap, fmt};
2
3use crate::errors::MoveInfoError;
4
5use super::{GameStatus, Piece, PieceType, Position};
6
7/// Represents the type of a [Move]
8///
9#[derive(Debug, Clone, PartialEq)]
10pub enum MoveType {
11    /// A normal move
12    Normal {
13        /// Whether the move is a capture
14        capture: bool,
15        /// The [PieceType] to promote to
16        promotion: Option<PieceType>,
17    },
18    /// A castle move
19    Castle {
20        /// The side of the board to castle on
21        side: CastleType,
22    },
23    /// An en passant move
24    EnPassant,
25}
26
27/// Represents the side of the board to castle on
28///
29#[derive(Debug, Clone, PartialEq)]
30pub enum CastleType {
31    /// The king side
32    KingSide,
33    /// The queen side
34    QueenSide,
35}
36
37/// Represents a move in a chess game
38///
39#[derive(Debug, Clone, PartialEq)]
40pub struct Move {
41    /// The [Piece] that is moving
42    pub piece: Piece,
43    /// The [Position] the piece is moving from
44    pub from: Position,
45    /// The [Position] the piece is moving to
46    pub to: Position,
47    /// The [type](Move) of the move
48    pub move_type: MoveType,
49    /// The type of the piece that is captured, if any
50    pub captured_piece: Option<PieceType>,
51    /// The position of the rook, if the move is a castle
52    pub rook_from: Option<Position>,
53    /// A tuple of booleans representing the ambiguity of the move
54    pub ambiguity: (bool, bool),
55    /// Whether the move puts the opponent in check
56    pub check: bool,
57    /// Whether the move puts the opponent in checkmate
58    pub checkmate: bool,
59}
60
61impl Move {
62    /// Creates a new [Move]
63    ///
64    /// # Arguments
65    /// * `piece`: The [Piece] that is moving
66    /// * `from`: The [Position] the piece is moving from
67    /// * `to`: The [Position] the piece is moving to
68    /// * `move_type`: The [type](MoveType) of the move
69    /// * `captured_piece`: The [Piece] that is captured, if any
70    /// * `rook_from`: The [Position] of the rook, if the move is a castle
71    /// * `ambiguity`: A tuple of booleans representing the ambiguity of the move
72    /// * `check`: Whether the move puts the opponent in check
73    /// * `checkmate`: Whether the move puts the opponent in checkmate
74    ///
75    /// # Returns
76    /// A `Result<Move, MoveInfoError>`
77    /// * `Ok(Move)`: The move if it is valid
78    /// * `Err(MoveInfoError)`: The error if the move is invalid
79    ///
80    /// # Example
81    /// ```
82    /// use chess_lab::core::{Color, PieceType, Piece, Position, Move, MoveType};
83    ///
84    /// let piece = Piece::new(Color::White, PieceType::Pawn);
85    /// let from = Position::new(4, 1).unwrap();
86    /// let to = Position::new(4, 3).unwrap();
87    /// let move_type = MoveType::Normal {
88    ///     capture: false,
89    ///     promotion: None,
90    /// };
91    /// let captured_piece = None;
92    /// let rook_from = None;
93    /// let ambiguity = (false, false);
94    /// let mv = Move::new(
95    ///     piece,
96    ///     from,
97    ///     to,
98    ///     move_type,
99    ///     captured_piece,
100    ///     rook_from,
101    ///     ambiguity,
102    ///     false,
103    ///     false
104    /// ).unwrap();
105    ///
106    /// assert_eq!(mv.to_string(), "e4");
107    /// ```
108    ///
109    pub fn new(
110        piece: Piece,
111        from: Position,
112        to: Position,
113        move_type: MoveType,
114        captured_piece: Option<PieceType>,
115        rook_from: Option<Position>,
116        ambiguity: (bool, bool),
117        check: bool,
118        checkmate: bool,
119    ) -> Result<Move, MoveInfoError> {
120        let mov = Move {
121            piece,
122            from,
123            to,
124            move_type: move_type.clone(),
125            captured_piece,
126            rook_from,
127            ambiguity,
128            check,
129            checkmate,
130        };
131        match &move_type {
132            MoveType::Normal {
133                capture: _,
134                promotion,
135            } => {
136                if promotion.is_some() {
137                    if piece.piece_type != PieceType::Pawn {
138                        return Err(MoveInfoError::new(
139                            String::from("The move is a promotion, but the piece is not a pawn"),
140                            mov,
141                        ));
142                    }
143                }
144            }
145            MoveType::Castle { side: _ } => {
146                if piece.piece_type != PieceType::King {
147                    return Err(MoveInfoError::new(
148                        String::from("The move is a castle, but the piece is not a king"),
149                        mov,
150                    ));
151                }
152                if rook_from.is_none() {
153                    return Err(MoveInfoError::new(
154                        String::from("The move is a castle, but no rook position is provided"),
155                        mov,
156                    ));
157                }
158            }
159            MoveType::EnPassant => {
160                if piece.piece_type != PieceType::Pawn {
161                    return Err(MoveInfoError::new(
162                        String::from("The move is an en passant, but the piece is not a pawn"),
163                        mov,
164                    ));
165                }
166            }
167        }
168        Ok(mov)
169    }
170}
171
172impl fmt::Display for Move {
173    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
174        let mut result = String::new();
175        if self.piece.piece_type != PieceType::Pawn {
176            result.push(self.piece.piece_type.to_char());
177        }
178        match &self.move_type {
179            MoveType::Castle { side } => {
180                result = match side {
181                    CastleType::KingSide => "O-O".to_string(),
182                    CastleType::QueenSide => "O-O-O".to_string(),
183                };
184            }
185            MoveType::Normal { capture, promotion } => {
186                let from_string = self.from.to_string();
187                if self.ambiguity.0 || (PieceType::Pawn == self.piece.piece_type && *capture) {
188                    result.push(from_string.chars().nth(0).unwrap());
189                }
190                if self.ambiguity.1 {
191                    result.push(from_string.chars().nth(1).unwrap());
192                }
193                if *capture {
194                    result.push('x');
195                }
196                result.push_str(&self.to.to_string());
197                if let Some(promotion) = promotion {
198                    result.push('=');
199                    result.push(promotion.to_char());
200                }
201            }
202            MoveType::EnPassant => {
203                result.push_str(&self.from.to_string());
204                result.push('x');
205                result.push_str(&self.to.to_string());
206            }
207        }
208        if self.checkmate {
209            result.push('#');
210        } else if self.check {
211            result.push('+');
212        }
213
214        write!(f, "{}", result)
215    }
216}
217
218/// Represents the information of a [Move]
219///
220#[derive(Debug, PartialEq, Eq, Clone)]
221pub struct MoveInfo {
222    /// The number of halfmoves since the last capture or pawn move
223    pub halfmove_clock: u32,
224    /// The number of fullmoves
225    pub fullmove_number: u32,
226    /// The en passant target square
227    pub en_passant: Option<Position>,
228    /// The castling rights
229    pub castling_rights: u8,
230    /// The status of the [Game](crate::logic::Game)
231    pub game_status: GameStatus,
232    /// A map of previous board positions and their occurrence counts
233    pub prev_positions: HashMap<String, u32>,
234}
235
236impl MoveInfo {
237    /// Creates a new [MoveInfo]
238    ///
239    /// # Arguments
240    /// * `halfmove_clock`: The number of halfmoves since the last capture or pawn move
241    /// * `fullmove_number`: The number of fullmoves
242    /// * `en_passant`: The en passant target square
243    /// * `castling_rights`: The castling rights
244    /// * `game_status`: The current [GameStatus]
245    ///
246    /// # Example
247    /// ```
248    /// use chess_lab::core::{GameStatus, MoveInfo};
249    /// use std::collections::HashMap;
250    ///
251    /// let move_info = MoveInfo::new(0, 1, None, 0, GameStatus::InProgress, HashMap::new());
252    ///
253    /// assert_eq!(move_info.halfmove_clock, 0);
254    /// assert_eq!(move_info.fullmove_number, 1);
255    /// assert_eq!(move_info.en_passant, None);
256    /// assert_eq!(move_info.castling_rights, 0);
257    /// assert_eq!(move_info.game_status, GameStatus::InProgress);
258    /// assert_eq!(move_info.prev_positions.len(), 0);
259    /// ```
260    ///
261    pub fn new(
262        halfmove_clock: u32,
263        fullmove_number: u32,
264        en_passant: Option<Position>,
265        castling_rights: u8,
266        game_status: GameStatus,
267        prev_positions: HashMap<String, u32>,
268    ) -> MoveInfo {
269        MoveInfo {
270            halfmove_clock,
271            fullmove_number,
272            en_passant,
273            castling_rights,
274            game_status,
275            prev_positions,
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283    use crate::core::{Color, PieceType};
284
285    #[test]
286    fn test_move_display_normal() {
287        let piece = Piece::new(Color::White, PieceType::Knight);
288        let from = Position::new(1, 0).unwrap(); // b1
289        let to = Position::new(2, 2).unwrap(); // c3
290        let move_type = MoveType::Normal {
291            capture: false,
292            promotion: None,
293        };
294        let mv = Move::new(
295            piece,
296            from,
297            to,
298            move_type,
299            None,
300            None,
301            (false, false),
302            false,
303            false,
304        )
305        .unwrap();
306        assert_eq!(mv.to_string(), "Nc3");
307    }
308
309    #[test]
310    fn test_move_display_capture() {
311        let piece = Piece::new(Color::Black, PieceType::Bishop);
312        let from = Position::new(2, 7).unwrap(); // c8
313        let to = Position::new(5, 4).unwrap(); // f5
314        let move_type = MoveType::Normal {
315            capture: true,
316            promotion: None,
317        };
318        let mv = Move::new(
319            piece,
320            from,
321            to,
322            move_type,
323            Some(PieceType::Pawn),
324            None,
325            (false, false),
326            true,
327            false,
328        )
329        .unwrap();
330        assert_eq!(mv.to_string(), "Bxf5+");
331    }
332
333    #[test]
334    fn test_move_display_kingside_castle() {
335        let piece = Piece::new(Color::White, PieceType::King);
336        let from = Position::new(4, 0).unwrap(); // e1
337        let to = Position::new(6, 0).unwrap(); // g1
338        let move_type = MoveType::Castle {
339            side: CastleType::KingSide,
340        };
341        let mv = Move::new(
342            piece,
343            from,
344            to,
345            move_type,
346            None,
347            Some(Position::new(7, 0).unwrap()), // h1
348            (false, false),
349            false,
350            false,
351        )
352        .unwrap();
353        assert_eq!(mv.to_string(), "O-O");
354    }
355
356    #[test]
357    fn test_move_display_queenside_castle() {
358        let piece = Piece::new(Color::Black, PieceType::King);
359        let from = Position::new(4, 7).unwrap(); // e8
360        let to = Position::new(2, 7).unwrap(); // c8
361        let move_type = MoveType::Castle {
362            side: CastleType::QueenSide,
363        };
364        let mv = Move::new(
365            piece,
366            from,
367            to,
368            move_type,
369            None,
370            Some(Position::new(0, 7).unwrap()), // a8
371            (false, false),
372            false,
373            false,
374        )
375        .unwrap();
376        assert_eq!(mv.to_string(), "O-O-O");
377    }
378
379    #[test]
380    fn test_promoting_non_pawn_error() {
381        let piece = Piece::new(Color::White, PieceType::Knight);
382        let from = Position::new(6, 7).unwrap(); // g8
383        let to = Position::new(7, 7).unwrap(); // h8
384        let move_type = MoveType::Normal {
385            capture: false,
386            promotion: Some(PieceType::Queen),
387        };
388        let mv_result = Move::new(
389            piece,
390            from,
391            to,
392            move_type,
393            None,
394            None,
395            (false, false),
396            false,
397            false,
398        );
399        assert!(mv_result.is_err());
400    }
401
402    #[test]
403    fn test_castling_non_king_error() {
404        let piece = Piece::new(Color::Black, PieceType::Queen);
405        let from = Position::new(4, 7).unwrap(); // e8
406        let to = Position::new(6, 7).unwrap(); // g8
407        let move_type = MoveType::Castle {
408            side: CastleType::KingSide,
409        };
410        let mv_result = Move::new(
411            piece,
412            from,
413            to,
414            move_type,
415            None,
416            Some(Position::new(7, 7).unwrap()), // h8
417            (false, false),
418            false,
419            false,
420        );
421        assert!(mv_result.is_err());
422    }
423
424    #[test]
425    fn test_castle_with_no_rook_error() {
426        let piece = Piece::new(Color::White, PieceType::King);
427        let from = Position::new(4, 0).unwrap(); // e1
428        let to = Position::new(2, 0).unwrap(); // c1
429        let move_type = MoveType::Castle {
430            side: CastleType::QueenSide,
431        };
432        let mv_result = Move::new(
433            piece,
434            from,
435            to,
436            move_type,
437            None,
438            None, // No rook position provided
439            (false, false),
440            false,
441            false,
442        );
443        assert!(mv_result.is_err());
444    }
445
446    #[test]
447    fn test_en_passant_non_pawn_error() {
448        let piece = Piece::new(Color::Black, PieceType::Bishop);
449        let from = Position::new(3, 4).unwrap(); // d5
450        let to = Position::new(4, 3).unwrap(); // e4
451        let move_type = MoveType::EnPassant;
452
453        let mv_result = Move::new(
454            piece,
455            from,
456            to,
457            move_type,
458            None,
459            None,
460            (false, false),
461            false,
462            false,
463        );
464        assert!(mv_result.is_err());
465    }
466}