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}