chess_engine/
lib.rs

1#![no_std]
2#[macro_use]
3extern crate alloc;
4use alloc::{
5    string::{String, ToString},
6    vec::Vec,
7};
8
9use core::convert::TryFrom;
10
11mod board;
12pub use board::{Board, BoardBuilder};
13
14mod square;
15pub use square::{Square, EMPTY_SQUARE};
16
17mod piece;
18pub use piece::Piece;
19
20mod position;
21pub use position::*;
22
23pub const WHITE: Color = Color::White;
24pub const BLACK: Color = Color::Black;
25
26/// The result of a move being played on the board.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
28pub enum GameResult {
29    /// The game is not finished, and the game is still in play.
30    Continuing(Board),
31    /// One player, the victor, checkmated the other.
32    /// This stores the color of the winner.
33    Victory(Color),
34    /// The game is drawn. This can be a result of the current player
35    /// having no legal moves and not being in check, or because
36    /// both players have insufficient material on the board.
37    ///
38    /// Insufficient material consists of:
39    /// 1. The player only has a king
40    /// 2. The player only has a king and a knight
41    /// 3. The player only has a king and two knights
42    /// 4. The player only has a king and a bishop
43    /// 5. The player only has a king and two bishops
44    ///
45    /// In a regular game of chess, threefold repetition also triggers
46    /// a stalemate, but this engine does not have builtin support for
47    /// threefold repetition detection yet.
48    Stalemate,
49    /// An illegal move was made. This can include many things,
50    /// such as moving a piece through another piece, attempting
51    /// to capture an allied piece, moving non-orthogonally or
52    /// non-diagonally, or non-knight-like according the rules
53    /// governing the movement of the piece. Additionally,
54    /// moves that put the player in check, (for example, moving a pinned piece),
55    /// are also illegal.
56    IllegalMove(Move),
57}
58
59/// The color of a piece.
60#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
61pub enum Color {
62    White,
63    Black,
64}
65
66impl core::fmt::Display for Color {
67    fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
68        write!(
69            f,
70            "{}",
71            match self {
72                Self::White => "White",
73                Self::Black => "Black",
74            }
75        )
76    }
77}
78
79/// A color can be inverted using the `!` operator.
80/// `!Color::White` becomes `Color::Black` and vice versa.
81impl core::ops::Not for Color {
82    type Output = Self;
83    fn not(self) -> Self {
84        match self {
85            Self::White => Self::Black,
86            Self::Black => Self::White,
87        }
88    }
89}
90
91/// A move that can be applied to a board.
92/// When applied to a board, the board assumes that the move is
93/// being applied for the current turn's player.
94#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
95pub enum Move {
96    /// If the current player is white, move the king to the C1 square, and the kingside rook to
97    /// the D1 square. If the current player is black, however, move the king to the C8 square,
98    /// and the kingside rook to the D8 square.
99    ///
100    /// Castling can only be performed if
101    /// 1. The king has not moved at all since the game began
102    /// 2. The respective rook (kingside or queenside) has also not moved
103    /// 3. The square adjacent to the king on the respective side is not threatened by an enemy piece
104    ///
105    /// If all of these conditions are satisfied, castling is a legal move
106    QueenSideCastle,
107    /// If the current player is white, move the king to the G1 square, and the kingside rook to
108    /// the F1 square. If the current player is black, however, move the king to the G8 square,
109    /// and the kingside rook to the F8 square.
110    KingSideCastle,
111    /// Move a piece from one square to another.
112    /// This can allow the player to capture another piece, by
113    /// simply moving a piece to the position of an enemy piece.
114    ///
115    /// Additionally, this can be used to [en-passant capture](https://en.wikipedia.org/wiki/En_passant),
116    /// even though the en-passant square itself does not contain any capturable pieces.
117    ///
118    /// En-passant captures MUST be performed with a pawn, upon an enemy pawn
119    /// that has just surpassed it by move two squares. An en-passant capture
120    /// must also be performed the turn immediately after the enemy pawn surpasses
121    /// the allied pawn. After the one turn a player has to en-passant capture, the
122    /// en-passant square is forgotten and can no longer be used.
123    Piece(Position, Position),
124    /// When played by another player, it awards victory to the other.
125    Resign,
126}
127
128/// Try to parse a Move from a string.
129///
130/// Possible valid formats include:
131/// - `"resign"`
132/// - `"resigns"`
133/// - `"castle queenside"`
134/// - `"O-O-O"` (correct notation)
135/// - `"o-o-o"` (incorrect notation, but will accept)
136/// - `"0-0-0"` (incorrect notation, but will accept)
137/// - `"castle kingside"`
138/// - `"O-O"` (correct notation)
139/// - `"o-o"` (incorrect notation, but will accept)
140/// - `"0-0"` (incorrect notation, but will accept)
141/// - `"e2e4"`
142/// - `"e2 e4"`
143/// - `"e2 to e4"`
144///
145/// Parsing a move such as `"knight to e4"` or `"Qxe4"` will NOT work.
146impl TryFrom<String> for Move {
147    type Error = String;
148
149    fn try_from(repr: String) -> Result<Self, Self::Error> {
150        let repr = repr.trim().to_string();
151
152        Ok(match repr.as_str() {
153            "resign" | "resigns" => Self::Resign,
154            "queenside castle" | "castle queenside" | "O-O-O" | "0-0-0" | "o-o-o" => {
155                Self::QueenSideCastle
156            }
157            "kingside castle" | "castle kingside" | "O-O" | "0-0" | "o-o" => Self::KingSideCastle,
158            other => {
159                let words = other.split_whitespace().collect::<Vec<&str>>();
160
161                if words.len() == 1 && words[0].len() == 4 {
162                    Self::Piece(
163                        Position::pgn(&words[0][..2])?,
164                        Position::pgn(&words[0][2..4])?,
165                    )
166                } else if words.len() == 2 {
167                    Self::Piece(Position::pgn(&words[0])?, Position::pgn(&words[1])?)
168                } else if words.len() == 3 && words[1] == "to" {
169                    Self::Piece(Position::pgn(&words[0])?, Position::pgn(&words[2])?)
170                } else {
171                    return Err(format!("invalid move format `{}`", other));
172                }
173            }
174        })
175    }
176}
177
178impl Move {
179    /// Try to parse a Move from a string.
180    ///
181    /// Possible valid formats include:
182    /// - `"resign"`
183    /// - `"resigns"`
184    /// - `"castle queenside"`
185    /// - `"O-O-O"` (correct notation)
186    /// - `"o-o-o"` (incorrect notation, but will accept)
187    /// - `"0-0-0"` (incorrect notation, but will accept)
188    /// - `"castle kingside"`
189    /// - `"O-O"` (correct notation)
190    /// - `"o-o"` (incorrect notation, but will accept)
191    /// - `"0-0"` (incorrect notation, but will accept)
192    /// - `"e2e4"`
193    /// - `"e2 e4"`
194    /// - `"e2 to e4"`
195    ///
196    /// Parsing a move such as `"knight to e4"` or `"Qxe4"` will NOT work.
197    pub fn parse(repr: String) -> Result<Self, String> {
198        Self::try_from(repr)
199    }
200}
201
202impl core::fmt::Display for Move {
203    fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
204        match self {
205            // Move::EnPassant(from) => write!(f, "ep {}", from),
206            Move::Piece(from, to) => write!(f, "{} to {}", from, to),
207            Move::KingSideCastle => write!(f, "O-O"),
208            Move::QueenSideCastle => write!(f, "O-O-O"),
209            Move::Resign => write!(f, "Resign"),
210        }
211    }
212}
213
214/// Evaluate a board and extract information, such as the best and worst moves.
215pub trait Evaluate: Sized {
216    /// Get the value of the board for a given color.
217    /// This subtracts the opponents value, and accounts for piece positions
218    /// and material value.
219    fn value_for(&self, color: Color) -> f64;
220
221    /// Get the current player's color.
222    fn get_current_player_color(&self) -> Color;
223
224    /// Get the legal moves for the current player.
225    fn get_legal_moves(&self) -> Vec<Move>;
226
227    /// Apply a move to the board for evaluation.
228    fn apply_eval_move(&self, m: Move) -> Self;
229
230    /// Get the best move for the current player with `depth` number of moves
231    /// of lookahead.
232    ///
233    /// This method returns
234    /// 1. The best move
235    /// 2. The number of boards evaluated to come to a conclusion
236    /// 3. The rating of the best move
237    ///
238    /// It's best not to use the rating value by itself for anything, as it
239    /// is relative to the other player's move ratings as well.
240    fn get_best_next_move(&self, depth: i32) -> (Move, u64, f64) {
241        let legal_moves = self.get_legal_moves();
242        let mut best_move_value = -999999.0;
243        let mut best_move = Move::Resign;
244
245        let color = self.get_current_player_color();
246
247        let mut board_count = 0;
248        for m in &legal_moves {
249            let child_board_value = self.apply_eval_move(*m).minimax(
250                depth,
251                -1000000.0,
252                1000000.0,
253                false,
254                color,
255                &mut board_count,
256            );
257            if child_board_value >= best_move_value {
258                best_move = *m;
259                best_move_value = child_board_value;
260            }
261        }
262
263        (best_move, board_count, best_move_value)
264    }
265
266    /// Get the best move for the current player with `depth` number of moves
267    /// of lookahead.
268    ///
269    /// This method returns
270    /// 1. The best move
271    /// 2. The number of boards evaluated to come to a conclusion
272    /// 3. The rating of the best move
273    ///
274    /// It's best not to use the rating value by itself for anything, as it
275    /// is relative to the other player's move ratings as well.
276    fn get_worst_next_move(&self, depth: i32) -> (Move, u64, f64) {
277        let legal_moves = self.get_legal_moves();
278        let mut best_move_value = -999999.0;
279        let mut best_move = Move::Resign;
280
281        let color = self.get_current_player_color();
282
283        let mut board_count = 0;
284        for m in &legal_moves {
285            let child_board_value = self.apply_eval_move(*m).minimax(
286                depth,
287                -1000000.0,
288                1000000.0,
289                true,
290                !color,
291                &mut board_count,
292            );
293
294            if child_board_value >= best_move_value {
295                best_move = *m;
296                best_move_value = child_board_value;
297            }
298        }
299
300        (best_move, board_count, best_move_value)
301    }
302
303    /// Perform minimax on a certain position, and get the minimum or maximum value
304    /// for a board. To get the best move, you minimize the values of the possible outcomes from your
305    /// own position, and maximize the values of the replies made by the other player.
306    ///
307    /// In other words, choose moves with the assumption that your opponent will make the
308    /// best possible replies to your moves. Moves that are seemingly good, but are easily countered,
309    /// are categorically eliminated by this algorithm.
310    fn minimax(
311        &self,
312        depth: i32,
313        mut alpha: f64,
314        mut beta: f64,
315        is_maximizing: bool,
316        getting_move_for: Color,
317        board_count: &mut u64,
318    ) -> f64 {
319        *board_count += 1;
320
321        if depth == 0 {
322            return self.value_for(getting_move_for);
323        }
324
325        let legal_moves = self.get_legal_moves();
326        let mut best_move_value;
327
328        if is_maximizing {
329            best_move_value = -999999.0;
330
331            for m in &legal_moves {
332                let child_board_value = self.apply_eval_move(*m).minimax(
333                    depth - 1,
334                    alpha,
335                    beta,
336                    !is_maximizing,
337                    getting_move_for,
338                    board_count,
339                );
340
341                if child_board_value > best_move_value {
342                    best_move_value = child_board_value;
343                }
344
345                if best_move_value > alpha {
346                    alpha = best_move_value
347                }
348
349                if beta <= alpha {
350                    return best_move_value;
351                }
352            }
353        } else {
354            best_move_value = 999999.0;
355
356            for m in &legal_moves {
357                let child_board_value = self.apply_eval_move(*m).minimax(
358                    depth - 1,
359                    alpha,
360                    beta,
361                    !is_maximizing,
362                    getting_move_for,
363                    board_count,
364                );
365                if child_board_value < best_move_value {
366                    best_move_value = child_board_value;
367                }
368
369                if best_move_value < beta {
370                    beta = best_move_value
371                }
372
373                if beta <= alpha {
374                    return best_move_value;
375                }
376            }
377        }
378
379        best_move_value
380    }
381}