├── .gitignore ├── Cargo.toml ├── README.md ├── problematic_positions └── src ├── bin └── uci_client.rs ├── board.rs ├── lib.rs ├── pieces.rs ├── search.rs ├── tests.rs ├── uci.rs └── ui.rs /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sunfish" 3 | version = "0.1.0" 4 | authors = ["Recursing "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | log = "0.4" 9 | simplelog = "0.9" 10 | 11 | 12 | [profile.test] 13 | opt-level = 3 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sunfish_rs 2 | Rust "port" of the sunfish simple chess engine 3 | 4 | # Credits: 5 | - [The original sunfish](https://github.com/thomasahle/sunfish) 6 | - [yuri91](https://github.com/yuri91) for making a first rust port that inspired this 7 | 8 | # How to play: 9 | Challenge it on [lichess](https://lichess.org/@/sunfish_rs) and tell me what you think! 10 | 11 | ### TODO: 12 | - Improve time managment, maybe rewriting search to be iterative instead of recursive 13 | - Support endgame values (ideally tapered eval), might be tricky to do with incremental updates 14 | - Add more tests, need to test many more positions and add more unit tests 15 | - Benchmarking, maybe build a micro benchmarking framework? See https://github.com/bheisler/criterion.rs/issues/306 16 | - [x] Make Square enums more compact, currently they use twice as much memory as the python chars :/ 17 | -------------------------------------------------------------------------------- /problematic_positions: -------------------------------------------------------------------------------- 1 | For no reason drops a pawn? 2 | 5k2/8/3pn1pp/8/6N1/6KP/8/8 b - - 1 53 3 | 4 | Missed "simple" tactic. Maybe just requires more depth 5 | 3rr2k/1p2bp1p/5P2/3p3N/p1pP3P/PnP2b2/1PB2P2/1K4RR w - - 2 26 6 | 7 | Missed mate in 3? Should not take rook, maybe just needs more depth 8 | r1bq1b1r/ppp4p/2n3p1/4p3/3Pp3/4B1P1/PPP1QPkP/R3K2R b KQ - 1 14 9 | 10 | https://lichess.org/hkxEEodG#195 missed mate in 2 in 7 seconds?? 11 | -------------------------------------------------------------------------------- /src/bin/uci_client.rs: -------------------------------------------------------------------------------- 1 | use simplelog::{Config, LevelFilter, WriteLogger}; 2 | use std::fs::OpenOptions; 3 | 4 | use sunfish::uci::uci_loop; 5 | 6 | fn set_global_logger() { 7 | let file = OpenOptions::new() 8 | .append(true) 9 | .create(true) 10 | .open("sunfish_log.log") 11 | .expect("Can't create log file in directory"); 12 | let _ = WriteLogger::init(LevelFilter::Trace, Config::default(), file); 13 | } 14 | 15 | fn main() { 16 | set_global_logger(); 17 | uci_loop(); 18 | } 19 | -------------------------------------------------------------------------------- /src/board.rs: -------------------------------------------------------------------------------- 1 | use crate::pieces::{Direction, Square}; 2 | use std::fmt::Debug; 3 | 4 | pub const PADDING: usize = 2; 5 | pub const BOARD_SIDE: usize = 8 + 2 * PADDING; 6 | pub const BOARD_SIZE: usize = BOARD_SIDE * BOARD_SIDE; 7 | 8 | pub const A8: usize = BOARD_SIDE * PADDING + PADDING; 9 | pub const H8: usize = A8 + 7; 10 | pub const A1: usize = A8 + 7 * BOARD_SIDE; 11 | const H1: usize = A1 + 7; 12 | 13 | #[derive(Clone, Copy, Hash, Eq, PartialEq, Debug)] 14 | pub struct BoardState { 15 | pub board: [Square; BOARD_SIZE], 16 | pub score: i32, 17 | pub my_castling_rights: (bool, bool), // first west, second east 18 | pub opponent_castling_rights: (bool, bool), // first west, second east 19 | pub en_passant_position: Option, // square where I can en passant 20 | pub king_passant_position: Option, // square where I could capture the king, used to treat castling as en passant 21 | } 22 | 23 | pub fn piece_moves( 24 | board_state: &BoardState, 25 | piece_moving: Square, 26 | start_position: usize, 27 | ) -> Vec { 28 | let mut reachable_squares: Vec = Vec::with_capacity(20); 29 | for move_direction in piece_moving.moves() { 30 | for end_position in (1..).map(|k| (start_position as i32 + move_direction * k) as usize) { 31 | let destination_square = board_state.board[end_position]; 32 | // Illegal moves 33 | 34 | // Hit board bounds or one of my pieces 35 | if destination_square == Square::Wall || destination_square.is_my_piece() { 36 | break; 37 | } 38 | 39 | // Illegal pawn moves TODO write explanations 40 | if piece_moving == Square::MyPawn { 41 | if (*move_direction == Direction::NORTH 42 | || *move_direction == Direction::NORTH + Direction::NORTH) 43 | && destination_square != Square::Empty 44 | { 45 | break; 46 | } 47 | if (*move_direction == Direction::NORTH + Direction::WEST 48 | || *move_direction == Direction::NORTH + Direction::EAST) 49 | && destination_square == Square::Empty 50 | && board_state.en_passant_position != Some(end_position) 51 | && board_state.king_passant_position != Some(end_position) 52 | { 53 | break; 54 | } 55 | if *move_direction == Direction::NORTH + Direction::NORTH 56 | && (start_position < (A1 as i32 + Direction::NORTH) as usize 57 | || board_state.board[(start_position as i32 + Direction::NORTH) as usize] 58 | != Square::Empty) 59 | { 60 | break; 61 | } 62 | } 63 | 64 | // Move is probably fine (TODO except king stuff) 65 | reachable_squares.push(end_position); 66 | 67 | // Stop pieces that don't slide 68 | if piece_moving == Square::MyPawn 69 | || piece_moving == Square::MyKnight 70 | || piece_moving == Square::MyKing 71 | { 72 | break; 73 | } 74 | 75 | // Stop sliding after capture 76 | if destination_square != Square::Empty { 77 | break; 78 | } 79 | } 80 | } 81 | reachable_squares 82 | } 83 | 84 | pub fn gen_moves(board_state: &BoardState) -> Vec<(usize, usize)> { 85 | let mut moves: Vec<(usize, usize)> = Vec::with_capacity(42); 86 | for (start_position, start_square) in board_state.board.iter().enumerate() { 87 | if !start_square.is_my_piece() { 88 | continue; 89 | } 90 | let piece_moving = start_square; 91 | for end_position in piece_moves(board_state, *piece_moving, start_position) { 92 | // Add castling if the rook can move to the king, east castling (long or short depending on color) 93 | moves.push((start_position, end_position)); 94 | if start_position == A1 95 | && board_state.board[(end_position as i32 + Direction::EAST) as usize] 96 | == Square::MyKing 97 | && board_state.my_castling_rights.0 98 | { 99 | moves.push(( 100 | (end_position as i32 + Direction::EAST) as usize, 101 | (end_position as i32 + Direction::WEST) as usize, 102 | )) 103 | } 104 | // Add castling if the rook can move to the king, west castling (long or short depending on color) 105 | else if start_position == H1 106 | && board_state.board[(end_position as i32 + Direction::WEST) as usize] 107 | == Square::MyKing 108 | && board_state.my_castling_rights.1 109 | { 110 | moves.push(( 111 | (end_position as i32 + Direction::WEST) as usize, 112 | (end_position as i32 + Direction::EAST) as usize, 113 | )) 114 | } 115 | } 116 | } 117 | moves 118 | } 119 | 120 | pub fn rotate(board_state: &mut BoardState) { 121 | let total_padding = PADDING * BOARD_SIDE + PADDING; 122 | for coordinate in total_padding..(BOARD_SIZE / 2) { 123 | let old_val = board_state.board[coordinate]; 124 | board_state.board[coordinate] = board_state.board[BOARD_SIZE - 1 - coordinate].swap_color(); 125 | board_state.board[BOARD_SIZE - 1 - coordinate] = old_val.swap_color(); 126 | } 127 | board_state.score = -board_state.score; 128 | std::mem::swap( 129 | &mut board_state.my_castling_rights, 130 | &mut board_state.opponent_castling_rights, 131 | ); 132 | board_state.en_passant_position = board_state 133 | .en_passant_position 134 | .map(|ep| BOARD_SIZE - 1 - ep); 135 | board_state.king_passant_position = board_state 136 | .king_passant_position 137 | .map(|kp| BOARD_SIZE - 1 - kp); 138 | } 139 | 140 | // Like rotate, but clears ep and kp 141 | pub fn nullmove(board_state: &BoardState) -> BoardState { 142 | let mut new_board = [Square::Empty; BOARD_SIZE]; 143 | for (coordinate, square) in new_board.iter_mut().enumerate() { 144 | *square = board_state.board[BOARD_SIZE - 1 - coordinate].swap_color(); 145 | } 146 | BoardState { 147 | board: new_board, 148 | score: -board_state.score, 149 | my_castling_rights: board_state.opponent_castling_rights, 150 | opponent_castling_rights: board_state.my_castling_rights, 151 | en_passant_position: None, 152 | king_passant_position: None, 153 | } 154 | } 155 | 156 | pub fn after_move(board_state: &BoardState, move_: &(usize, usize)) -> BoardState { 157 | let (start_position, end_position) = *move_; 158 | let start_square = board_state.board[start_position]; 159 | let mut new_board = board_state.board; 160 | let mut my_castling_rights = board_state.my_castling_rights; 161 | let mut opponent_castling_rights = board_state.opponent_castling_rights; 162 | let mut en_passant_position = None; 163 | let mut king_passant_position = None; 164 | 165 | // Actual move 166 | new_board[end_position] = start_square; 167 | new_board[start_position] = Square::Empty; 168 | 169 | // Castling rights, we move the rook or capture the opponent's 170 | if start_position == A1 { 171 | my_castling_rights = (false, my_castling_rights.1) 172 | } 173 | if start_position == H1 { 174 | my_castling_rights = (my_castling_rights.0, false) 175 | } 176 | if end_position == A8 { 177 | opponent_castling_rights = (opponent_castling_rights.0, false) 178 | } 179 | if end_position == H8 { 180 | opponent_castling_rights = (false, opponent_castling_rights.1) 181 | } 182 | 183 | // Castling 184 | if start_square == Square::MyKing { 185 | my_castling_rights = (false, false); 186 | if (start_position as i32 - end_position as i32).abs() == 2 { 187 | let final_rook_position: usize = (start_position + end_position) / 2; 188 | new_board[final_rook_position] = Square::MyRook; 189 | king_passant_position = Some(final_rook_position); 190 | if start_position > end_position { 191 | new_board[A1] = Square::Empty; 192 | } else { 193 | new_board[H1] = Square::Empty; 194 | } 195 | } 196 | } 197 | 198 | // Pawn promotion, double move and en passant capture 199 | if start_square == Square::MyPawn { 200 | let move_type = end_position as i32 - start_position as i32; 201 | if (A8 <= end_position) && (end_position <= H8) { 202 | new_board[end_position] = Square::MyQueen 203 | } else if move_type == 2 * Direction::NORTH { 204 | en_passant_position = Some((start_position as i32 + Direction::NORTH) as usize) 205 | } 206 | 207 | // en passant capture (diagonal move to empty position) 208 | if board_state.en_passant_position == Some(end_position) { 209 | new_board[end_position + Direction::SOUTH as usize] = Square::Empty; 210 | } 211 | } 212 | 213 | let mut new_board_state = BoardState { 214 | board: new_board, 215 | score: board_state.score + move_value(board_state, &move_), 216 | my_castling_rights, 217 | opponent_castling_rights, 218 | king_passant_position, 219 | en_passant_position, 220 | }; 221 | rotate(&mut new_board_state); 222 | new_board_state 223 | } 224 | 225 | pub fn can_check(board_state: &BoardState, move_: &(usize, usize)) -> bool { 226 | let (start_position, end_position) = *move_; 227 | let moved_piece = board_state.board[start_position]; 228 | if !moved_piece.is_my_piece() { 229 | panic!(); 230 | } 231 | for reachable_square in piece_moves(board_state, moved_piece, end_position) { 232 | if board_state.board[reachable_square] == Square::OpponentKing { 233 | return true; 234 | } 235 | } 236 | false 237 | } 238 | 239 | pub fn move_value(board_state: &BoardState, move_: &(usize, usize)) -> i32 { 240 | let (start_position, end_position) = *move_; 241 | let moving_piece = board_state.board[start_position]; 242 | if !moving_piece.is_my_piece() { 243 | panic!(); 244 | } 245 | 246 | // Actual move 247 | let mut temp_score = 248 | moving_piece.midgame_value(end_position) - moving_piece.midgame_value(start_position); 249 | 250 | // Score for captures 251 | if board_state.board[end_position].is_opponent_piece() { 252 | // Add to the board score the value of the captured piece in the rotated board 253 | temp_score += board_state.board[end_position] 254 | .swap_color() 255 | .midgame_value(BOARD_SIZE - 1 - end_position); 256 | } 257 | 258 | // Castling check detection 259 | match board_state.king_passant_position { 260 | None => {} 261 | Some(position) => { 262 | // If I'm moving to a position the opponent king just passed through while castling, I can capture it 263 | // E.g. any of E1, F1, G1 for white short castling, the king_passant_position would be F1 264 | if (end_position as i32 - position as i32).abs() < 2 { 265 | temp_score += Square::MyKing.midgame_value(BOARD_SIZE - 1 - end_position); 266 | } 267 | } 268 | } 269 | 270 | // Wierd pawn and king stuff (castling, promotions and en passant) 271 | match moving_piece { 272 | Square::MyKing => { 273 | // Castling, update the score with the new rook position 274 | if (end_position as i32 - start_position as i32).abs() == 2 { 275 | temp_score += Square::MyRook.midgame_value((start_position + end_position) / 2); 276 | temp_score -= Square::MyRook.midgame_value(if end_position < start_position { 277 | A1 278 | } else { 279 | H1 280 | }); 281 | } 282 | } 283 | Square::MyPawn => { 284 | if A8 <= end_position && end_position <= H8 { 285 | //Promotion 286 | temp_score += Square::MyQueen.midgame_value(end_position) 287 | - Square::MyPawn.midgame_value(end_position) //Always promote to queen 288 | } else if board_state.en_passant_position == Some(end_position) { 289 | //Capture a pawn en passant 290 | // TODO explain 291 | temp_score += 292 | Square::MyPawn.midgame_value(BOARD_SIZE - 1 - (end_position + BOARD_SIDE)) 293 | } 294 | } 295 | _ => {} 296 | } 297 | temp_score 298 | } 299 | 300 | pub fn static_score(board: [Square; BOARD_SIZE]) -> i32 { 301 | board 302 | .iter() 303 | .enumerate() 304 | .map(|(index, piece)| { 305 | if piece.is_my_piece() { 306 | piece.midgame_value(index) 307 | } else if piece.is_opponent_piece() { 308 | -piece.swap_color().midgame_value(BOARD_SIZE - 1 - index) 309 | } else { 310 | 0 311 | } 312 | }) 313 | .sum() 314 | } 315 | 316 | const INITIAL_BOARD: [Square; BOARD_SIZE] = [ 317 | // Padding 318 | Square::Wall, 319 | Square::Wall, 320 | Square::Wall, 321 | Square::Wall, 322 | Square::Wall, 323 | Square::Wall, 324 | Square::Wall, 325 | Square::Wall, 326 | Square::Wall, 327 | Square::Wall, 328 | Square::Wall, 329 | Square::Wall, 330 | Square::Wall, 331 | Square::Wall, 332 | Square::Wall, 333 | Square::Wall, 334 | Square::Wall, 335 | Square::Wall, 336 | Square::Wall, 337 | Square::Wall, 338 | Square::Wall, 339 | Square::Wall, 340 | Square::Wall, 341 | Square::Wall, 342 | // Eigth rank 343 | Square::Wall, 344 | Square::Wall, 345 | Square::OpponentRook, 346 | Square::OpponentKnight, 347 | Square::OpponentBishop, 348 | Square::OpponentQueen, 349 | Square::OpponentKing, 350 | Square::OpponentBishop, 351 | Square::OpponentKnight, 352 | Square::OpponentRook, 353 | Square::Wall, 354 | Square::Wall, 355 | // Seventh rank 356 | Square::Wall, 357 | Square::Wall, 358 | Square::OpponentPawn, 359 | Square::OpponentPawn, 360 | Square::OpponentPawn, 361 | Square::OpponentPawn, 362 | Square::OpponentPawn, 363 | Square::OpponentPawn, 364 | Square::OpponentPawn, 365 | Square::OpponentPawn, 366 | Square::Wall, 367 | Square::Wall, 368 | // Sixth rank 369 | Square::Wall, 370 | Square::Wall, 371 | Square::Empty, 372 | Square::Empty, 373 | Square::Empty, 374 | Square::Empty, 375 | Square::Empty, 376 | Square::Empty, 377 | Square::Empty, 378 | Square::Empty, 379 | Square::Wall, 380 | Square::Wall, 381 | // Fifth rank 382 | Square::Wall, 383 | Square::Wall, 384 | Square::Empty, 385 | Square::Empty, 386 | Square::Empty, 387 | Square::Empty, 388 | Square::Empty, 389 | Square::Empty, 390 | Square::Empty, 391 | Square::Empty, 392 | Square::Wall, 393 | Square::Wall, 394 | // Fourth rank 395 | Square::Wall, 396 | Square::Wall, 397 | Square::Empty, 398 | Square::Empty, 399 | Square::Empty, 400 | Square::Empty, 401 | Square::Empty, 402 | Square::Empty, 403 | Square::Empty, 404 | Square::Empty, 405 | Square::Wall, 406 | Square::Wall, 407 | // Third rank 408 | Square::Wall, 409 | Square::Wall, 410 | Square::Empty, 411 | Square::Empty, 412 | Square::Empty, 413 | Square::Empty, 414 | Square::Empty, 415 | Square::Empty, 416 | Square::Empty, 417 | Square::Empty, 418 | Square::Wall, 419 | Square::Wall, 420 | // Second rank 421 | Square::Wall, 422 | Square::Wall, 423 | Square::MyPawn, 424 | Square::MyPawn, 425 | Square::MyPawn, 426 | Square::MyPawn, 427 | Square::MyPawn, 428 | Square::MyPawn, 429 | Square::MyPawn, 430 | Square::MyPawn, 431 | Square::Wall, 432 | Square::Wall, 433 | // First rank 434 | Square::Wall, 435 | Square::Wall, 436 | Square::MyRook, 437 | Square::MyKnight, 438 | Square::MyBishop, 439 | Square::MyQueen, 440 | Square::MyKing, 441 | Square::MyBishop, 442 | Square::MyKnight, 443 | Square::MyRook, 444 | Square::Wall, 445 | Square::Wall, 446 | // Padding 447 | Square::Wall, 448 | Square::Wall, 449 | Square::Wall, 450 | Square::Wall, 451 | Square::Wall, 452 | Square::Wall, 453 | Square::Wall, 454 | Square::Wall, 455 | Square::Wall, 456 | Square::Wall, 457 | Square::Wall, 458 | Square::Wall, 459 | Square::Wall, 460 | Square::Wall, 461 | Square::Wall, 462 | Square::Wall, 463 | Square::Wall, 464 | Square::Wall, 465 | Square::Wall, 466 | Square::Wall, 467 | Square::Wall, 468 | Square::Wall, 469 | Square::Wall, 470 | Square::Wall, 471 | ]; 472 | 473 | pub const INITIAL_BOARD_STATE: BoardState = BoardState { 474 | board: INITIAL_BOARD, 475 | score: 0, 476 | my_castling_rights: (true, true), 477 | opponent_castling_rights: (true, true), 478 | en_passant_position: None, 479 | king_passant_position: None, 480 | }; 481 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod board; 2 | pub mod pieces; 3 | pub mod search; 4 | pub mod tests; 5 | pub mod uci; 6 | pub mod ui; 7 | -------------------------------------------------------------------------------- /src/pieces.rs: -------------------------------------------------------------------------------- 1 | use crate::board::{BOARD_SIDE, BOARD_SIZE, PADDING}; 2 | 3 | pub struct Direction {} 4 | 5 | impl Direction { 6 | pub const NORTH: i32 = -(BOARD_SIDE as i32); 7 | pub const EAST: i32 = 1; 8 | pub const SOUTH: i32 = BOARD_SIDE as i32; 9 | pub const WEST: i32 = -1; 10 | } 11 | 12 | #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] 13 | #[repr(u8)] 14 | pub enum Square { 15 | MyPawn = 0x01, 16 | MyKnight = 0x02, 17 | MyBishop = 0x03, 18 | MyRook = 0x04, 19 | MyQueen = 0x05, 20 | MyKing = 0x06, 21 | OpponentPawn = 0x11, 22 | OpponentKnight = 0x12, 23 | OpponentBishop = 0x13, 24 | OpponentRook = 0x14, 25 | OpponentQueen = 0x15, 26 | OpponentKing = 0x16, 27 | Empty = 0xFE, 28 | Wall = 0xFF, // Here to simplify detection of out of board moves 29 | } 30 | 31 | impl Square { 32 | pub fn is_my_piece(self) -> bool { 33 | matches!( 34 | self, 35 | Square::MyPawn 36 | | Square::MyKing 37 | | Square::MyRook 38 | | Square::MyKnight 39 | | Square::MyBishop 40 | | Square::MyQueen 41 | ) 42 | } 43 | 44 | pub fn is_opponent_piece(self) -> bool { 45 | matches!( 46 | self, 47 | Square::OpponentPawn 48 | | Square::OpponentKing 49 | | Square::OpponentRook 50 | | Square::OpponentKnight 51 | | Square::OpponentBishop 52 | | Square::OpponentQueen 53 | ) 54 | } 55 | 56 | pub fn swap_color(self) -> Square { 57 | match self { 58 | Square::Empty => Square::Empty, 59 | Square::Wall => Square::Wall, 60 | Square::MyPawn => Square::OpponentPawn, 61 | Square::MyKing => Square::OpponentKing, 62 | Square::MyRook => Square::OpponentRook, 63 | Square::MyKnight => Square::OpponentKnight, 64 | Square::MyBishop => Square::OpponentBishop, 65 | Square::MyQueen => Square::OpponentQueen, 66 | Square::OpponentPawn => Square::MyPawn, 67 | Square::OpponentKing => Square::MyKing, 68 | Square::OpponentRook => Square::MyRook, 69 | Square::OpponentKnight => Square::MyKnight, 70 | Square::OpponentBishop => Square::MyBishop, 71 | Square::OpponentQueen => Square::MyQueen, 72 | } 73 | } 74 | 75 | pub fn moves(self) -> &'static [i32] { 76 | match self { 77 | Square::MyPawn => &[ 78 | Direction::NORTH, 79 | Direction::NORTH + Direction::NORTH, 80 | Direction::NORTH + Direction::WEST, 81 | Direction::NORTH + Direction::EAST, 82 | ], 83 | Square::MyKnight => &[ 84 | Direction::NORTH + Direction::NORTH + Direction::EAST, 85 | Direction::NORTH + Direction::NORTH + Direction::WEST, 86 | Direction::WEST + Direction::WEST + Direction::NORTH, 87 | Direction::WEST + Direction::WEST + Direction::SOUTH, 88 | Direction::SOUTH + Direction::SOUTH + Direction::WEST, 89 | Direction::SOUTH + Direction::SOUTH + Direction::EAST, 90 | Direction::EAST + Direction::EAST + Direction::SOUTH, 91 | Direction::EAST + Direction::EAST + Direction::NORTH, 92 | ], 93 | Square::MyBishop => &[ 94 | Direction::NORTH + Direction::EAST, 95 | Direction::NORTH + Direction::WEST, 96 | Direction::WEST + Direction::SOUTH, 97 | Direction::SOUTH + Direction::EAST, 98 | ], 99 | Square::MyRook => &[ 100 | Direction::NORTH, 101 | Direction::WEST, 102 | Direction::SOUTH, 103 | Direction::EAST, 104 | ], 105 | Square::MyQueen | Square::MyKing => &[ 106 | Direction::NORTH, 107 | Direction::WEST, 108 | Direction::SOUTH, 109 | Direction::EAST, 110 | Direction::NORTH + Direction::EAST, 111 | Direction::NORTH + Direction::WEST, 112 | Direction::WEST + Direction::SOUTH, 113 | Direction::SOUTH + Direction::EAST, 114 | ], 115 | _ => panic!(), 116 | } 117 | } 118 | 119 | pub fn midgame_value(self, position: usize) -> i32 { 120 | debug_assert!( 121 | position >= BOARD_SIDE * PADDING + PADDING 122 | && position < BOARD_SIZE - BOARD_SIDE * PADDING - PADDING 123 | && position % BOARD_SIDE >= PADDING 124 | && position % BOARD_SIDE < BOARD_SIDE - PADDING 125 | ); 126 | 127 | // Piece square tables: piece value in different positions 128 | // Values from https://github.com/official-stockfish/Stockfish/blob/05f7d59a9a27d9f8bce8bde4e9fed7ecefeb03b9 129 | 130 | // From stockfish /src/types.h#L182, 131 | let piece_value = match self { 132 | Square::MyPawn => 136, 133 | Square::MyKnight => 782, 134 | Square::MyBishop => 830, 135 | Square::MyRook => 1289, 136 | Square::MyQueen => 2529, 137 | Square::MyKing => 32000, 138 | _ => panic!(), 139 | }; 140 | 141 | // From stockfish /src/psqt.cpp#L31 142 | let piece_position_value = match self { 143 | Square::MyPawn => &[ 144 | 0, 0, 0, 0, 0, 0, 0, 0, // Last rank, no pawns 145 | 15, 31, 20, 14, 23, 11, 37, 24, // 146 | -1, -3, 15, 26, 1, 10, -7, -9, // 147 | 8, -1, -5, 13, 24, 11, -10, 3, // 148 | -9, -18, 8, 32, 43, 25, -4, -16, // 149 | -9, -13, -40, 22, 26, -40, 1, -22, // 150 | 2, 0, 15, 3, 11, 22, 11, -1, // 151 | 0, 0, 0, 0, 0, 0, 0, 0, 152 | ], 153 | Square::MyKnight => &[ 154 | -200, -80, -53, -32, -32, -53, -80, -200, // 155 | -67, -21, 6, 37, 37, 6, -21, -67, // 156 | -11, 28, 63, 55, 55, 63, 28, -11, // 157 | -29, 13, 42, 52, 52, 42, 13, -29, // 158 | -28, 5, 41, 47, 47, 41, 5, -28, // 159 | -64, -20, 4, 19, 19, 4, -20, -64, // 160 | -79, -39, -24, -9, -9, -24, -39, -79, // 161 | -169, -96, -80, -79, -79, -80, -96, -169, // 162 | ], 163 | Square::MyBishop => &[ 164 | -48, -3, -12, -25, -25, -12, -3, -48, // 165 | -21, -19, 10, -6, -6, 10, -19, -21, // 166 | -17, 4, -1, 8, 8, -1, 4, -17, // 167 | -7, 30, 23, 28, 28, 23, 30, -7, // 168 | 1, 8, 26, 37, 37, 26, 8, 1, // 169 | -8, 24, -3, 15, 15, -3, 24, -8, // 170 | -18, 7, 14, 3, 3, 14, 7, -18, // 171 | -44, -4, -11, -28, -28, -11, -4, -44, // 172 | ], 173 | Square::MyRook => &[ 174 | -22, -24, -6, 4, 4, -6, -24, -22, // 175 | -8, 6, 10, 12, 12, 10, 6, -8, // 176 | -24, -4, 4, 10, 10, 4, -4, -24, // 177 | -24, -12, -1, 6, 6, -1, -12, -24, // 178 | -13, -5, -4, -6, -6, -4, -5, -13, // 179 | -21, -7, 3, -1, -1, 3, -7, -21, // 180 | -18, -10, -5, 9, 9, -5, -10, -18, // 181 | -24, -13, -7, 2, 2, -7, -13, -24, // 182 | ], 183 | Square::MyQueen => &[ 184 | -2, -2, 1, -2, -2, 1, -2, -2, // 185 | -5, 6, 10, 8, 8, 10, 6, -5, // 186 | -4, 10, 6, 8, 8, 6, 10, -4, // 187 | 0, 14, 12, 5, 5, 12, 14, 0, // 188 | 4, 5, 9, 8, 8, 9, 5, 4, // 189 | -3, 6, 13, 7, 7, 13, 6, -3, // 190 | -3, 5, 8, 12, 12, 8, 5, -3, // 191 | 3, -5, -5, 4, 4, -5, -5, 3, // 192 | ], 193 | Square::MyKing => &[ 194 | 6, 8, 4, 0, 0, 4, 8, 6, // 195 | 8, 12, 6, 2, 2, 6, 12, 8, // 196 | 12, 15, 8, 3, 3, 8, 15, 12, // 197 | 14, 17, 11, 6, 6, 11, 17, 15, // 198 | 16, 19, 13, 10, 10, 13, 19, 16, // 199 | 19, 25, 16, 12, 12, 16, 25, 19, // 200 | 27, 30, 24, 18, 18, 24, 30, 27, // 201 | 27, 32, 27, 19, 19, 27, 32, 27, // 202 | ], 203 | _ => &[0; 64], 204 | }; 205 | let real_position = position - PADDING * BOARD_SIDE; 206 | let row_number = real_position / BOARD_SIDE; 207 | piece_value + piece_position_value[real_position - PADDING * (2 * row_number + 1)] 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use std::cmp::max; 3 | use std::collections::HashMap; 4 | use std::time::{Duration, Instant}; 5 | 6 | use crate::board::{after_move, can_check, gen_moves, move_value, nullmove, BoardState}; 7 | use crate::pieces::Square; 8 | 9 | pub const MATE_UPPER: i32 = 32_000 + 8 * 2529; // TODO move somewhere else, do we need MATE_UPPER? 10 | pub const MATE_LOWER: i32 = 32_000 - 8 * 2529; 11 | const TRANSPOSITION_TABLE_SIZE: usize = 1_000_000; // TODO explain, make more space efficient 12 | const QUIESCENCE_SEARCH_LIMIT: i32 = 130; 13 | const EVAL_ROUGHNESS: i32 = 10; // TODO do we need this? 14 | const STOP_SEARCH: i32 = MATE_UPPER * 101; 15 | 16 | #[derive(Clone, Copy)] 17 | pub struct Entry { 18 | // pub for debugging TODO refactor 19 | lower: i32, 20 | upper: i32, 21 | } 22 | 23 | const DEFAULT_ENTRY: Entry = Entry { 24 | lower: -MATE_UPPER, 25 | upper: MATE_UPPER, 26 | }; 27 | 28 | pub struct Searcher { 29 | pub score_transposition_table: HashMap<(BoardState, i32, bool), Entry>, 30 | pub move_transposition_table: HashMap, 31 | pub nodes: u32, 32 | now: Instant, 33 | duration: Duration, 34 | } 35 | 36 | impl Default for Searcher { 37 | fn default() -> Self { 38 | Searcher { 39 | score_transposition_table: HashMap::with_capacity(TRANSPOSITION_TABLE_SIZE), 40 | move_transposition_table: HashMap::with_capacity(TRANSPOSITION_TABLE_SIZE), 41 | nodes: 0, 42 | now: Instant::now(), 43 | duration: Duration::new(4, 0), 44 | } 45 | } 46 | } 47 | 48 | impl Searcher { 49 | fn bound(&mut self, board_state: &BoardState, gamma: i32, depth: i32, root: bool) -> i32 { 50 | self.nodes += 1; 51 | 52 | // Sunfish is a king-capture engine, so we should always check if we 53 | // still have a king. Notice since this is the only termination check, 54 | // the remaining code has to be comfortable with being mated, stalemated 55 | // or able to capture the opponent king. 56 | if board_state.score <= -MATE_LOWER { 57 | return -MATE_UPPER; 58 | } 59 | 60 | // Look into the table if we have already searched this position before. 61 | // We also need to be sure, that the stored search was over the same 62 | // nodes as the current search. 63 | // Depth <= 0 is Quiescence Search. Here any position is searched as deeply as is needed 64 | // for calmness, and so there is no reason to keep different depths in the 65 | // transposition table. 66 | 67 | let entry = *self 68 | .score_transposition_table 69 | .get(&(*board_state, max(depth, 0), root)) 70 | .unwrap_or(&DEFAULT_ENTRY); 71 | 72 | if entry.lower >= gamma 73 | && (!root || self.move_transposition_table.get(board_state).is_some()) 74 | // TODO do this last check before calling root, also remove root parameter 75 | { 76 | return entry.lower; 77 | } else if entry.upper < gamma { 78 | return entry.upper; 79 | } 80 | 81 | if self.now.elapsed() > self.duration { 82 | return STOP_SEARCH; 83 | } 84 | 85 | let mut best = -MATE_UPPER; 86 | // First try not moving at all 87 | if depth > 0 88 | && !root 89 | // TODO maybe base it on the board score? 90 | && (board_state.board.iter().any(|&s| matches!(s, Square::MyRook 91 | | Square::MyKnight 92 | | Square::MyBishop 93 | | Square::MyQueen))) 94 | { 95 | let score = -self.bound(&nullmove(board_state), 1 - gamma, depth - 3, false); 96 | if score == -STOP_SEARCH { 97 | return STOP_SEARCH; 98 | } 99 | best = std::cmp::max(best, score); 100 | } else if depth <= 0 { 101 | // For QSearch we have a different kind of null-move 102 | let score = board_state.score; 103 | best = std::cmp::max(best, score); 104 | } 105 | 106 | if best <= gamma { 107 | if let Some(killer_move) = self.move_transposition_table.get(board_state).copied() { 108 | // Then killer move. We search it twice, but the tp will fix things for 109 | // us. Note, we don't have to check for legality, since we've already 110 | // done it before. Also note that in QS the killer must be a capture, 111 | // otherwise we will be non deterministic. 112 | if depth > 0 || move_value(board_state, &killer_move) >= QUIESCENCE_SEARCH_LIMIT { 113 | let score = -self.bound( 114 | &after_move(board_state, &killer_move), 115 | 1 - gamma, 116 | depth - 1, 117 | false, 118 | ); 119 | if score == -STOP_SEARCH { 120 | return STOP_SEARCH; 121 | } 122 | best = std::cmp::max(best, score); 123 | // should I add it again to the move_transposition_table? 124 | // self.move_transposition_table.insert(*board_state, killer_move); 125 | } 126 | } 127 | } 128 | 129 | if best < gamma { 130 | // Then all the other moves 131 | let others = gen_moves(board_state); 132 | let check_bonus = |m| { 133 | if can_check(board_state, m) { 134 | QUIESCENCE_SEARCH_LIMIT / 2 135 | } else { 136 | 0 137 | } 138 | }; 139 | let mut move_vals: Vec<_> = others 140 | .iter() 141 | .map(|m| (-move_value(board_state, m) - check_bonus(m), m)) 142 | .collect(); 143 | move_vals.sort_unstable(); 144 | for (val, m) in move_vals { 145 | if depth > 0 146 | || (-val >= QUIESCENCE_SEARCH_LIMIT && (board_state.score - val > best)) 147 | { 148 | let score = 149 | -self.bound(&after_move(board_state, m), 1 - gamma, depth - 1, false); 150 | if score == -STOP_SEARCH { 151 | return STOP_SEARCH; 152 | } 153 | best = std::cmp::max(best, score); 154 | if best >= gamma { 155 | // Save the move for pv construction and killer heuristic 156 | if self.move_transposition_table.len() >= TRANSPOSITION_TABLE_SIZE { 157 | self.move_transposition_table.clear(); 158 | } 159 | self.move_transposition_table.insert(*board_state, *m); 160 | break; 161 | } 162 | } else { 163 | break; 164 | } 165 | } 166 | } 167 | 168 | // Stalemate checking is a bit tricky: Say we failed low, because 169 | // we can't (legally) move and so the (real) score is -infty. 170 | // At the next depth we are allowed to just return r, -infty <= r < gamma, 171 | // which is normally fine. 172 | // However, what if gamma = -10 and we don't have any legal moves? 173 | // Then the score is actaully a draw and we should fail high! 174 | // Thus, if best < gamma and best < 0 we need to double check what we are doing. 175 | // This doesn't prevent sunfish from making a move that results in stalemate, 176 | // but only if depth == 1, so that's probably fair enough. 177 | // (Btw, at depth 1 we can also mate without realizing.) 178 | if best < gamma && best < 0 && depth > 0 { 179 | let is_dead = |pos: BoardState| { 180 | gen_moves(&pos) 181 | .iter() 182 | .any(|m| move_value(&pos, m) >= MATE_LOWER) 183 | }; 184 | if gen_moves(board_state) 185 | .iter() 186 | .all(|m| is_dead(after_move(board_state, m))) 187 | { 188 | let in_check = is_dead(nullmove(board_state)); 189 | best = if in_check { -MATE_UPPER } else { 0 }; 190 | } 191 | } 192 | 193 | // Update score_transposition_table 194 | if self.score_transposition_table.len() >= TRANSPOSITION_TABLE_SIZE { 195 | self.score_transposition_table.clear(); 196 | } 197 | if best >= gamma { 198 | self.score_transposition_table.insert( 199 | (*board_state, depth, root), 200 | Entry { 201 | lower: best, 202 | upper: entry.upper, 203 | }, 204 | ); 205 | } else if best < gamma { 206 | self.score_transposition_table.insert( 207 | (*board_state, depth, root), 208 | Entry { 209 | lower: entry.lower, 210 | upper: best, 211 | }, 212 | ); 213 | } 214 | 215 | best 216 | } 217 | 218 | // Iterative deepening MTD-bi search 219 | pub fn search( 220 | &mut self, 221 | board_state: BoardState, 222 | duration: Duration, 223 | ) -> ((usize, usize), i32, i32) { 224 | self.nodes = 0; 225 | let mut reached_depth; 226 | self.now = Instant::now(); 227 | self.duration = duration; 228 | let mut last_move = ((0, 0), 0, 0); 229 | 230 | // Bound depth to avoid infinite recursion in finished games 231 | for depth in 1..99 { 232 | // Realistically will reach depths around 6-12, except endgames 233 | let mut lower = -MATE_UPPER; 234 | let mut upper = MATE_UPPER; 235 | while lower < upper - EVAL_ROUGHNESS { 236 | let gamma = (lower + upper + 1) / 2; 237 | let score = self.bound(&board_state, gamma, depth, true); 238 | if score == STOP_SEARCH { 239 | lower = STOP_SEARCH; 240 | break; 241 | } 242 | if score >= gamma { 243 | lower = score; 244 | } else { 245 | upper = score; 246 | } 247 | } 248 | if lower == STOP_SEARCH { 249 | break; 250 | } 251 | let score = self.bound(&board_state, lower, depth, true); 252 | if score == STOP_SEARCH { 253 | break; 254 | } 255 | reached_depth = depth; 256 | info!( 257 | "Reached depth {: <2} score {: <5} nodes {: <7} time {:?}", 258 | depth, 259 | score, 260 | self.nodes, 261 | self.now.elapsed() 262 | ); 263 | 264 | // If the game hasn't finished we can retrieve our move from the 265 | // transposition table. 266 | 267 | last_move = ( 268 | *self 269 | .move_transposition_table 270 | .get(&board_state) 271 | .expect("move not in table"), 272 | self.score_transposition_table 273 | .get(&(board_state, reached_depth, true)) 274 | .expect("score not in table") 275 | .lower, 276 | reached_depth, 277 | ); 278 | 279 | if self.now.elapsed() > self.duration || score > MATE_LOWER { 280 | // Don't waste time if a mate is found 281 | break; 282 | } 283 | } 284 | 285 | last_move 286 | } 287 | 288 | // Done to prevent move repetitions 289 | pub fn set_eval_to_zero(&mut self, board_state: &BoardState) { 290 | // TODO there's probably a better way 291 | for depth in 1..30 { 292 | self.score_transposition_table 293 | .insert((*board_state, depth, false), Entry { lower: 0, upper: 0 }); 294 | } 295 | } 296 | } 297 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | 3 | use crate::board::{after_move, gen_moves, INITIAL_BOARD_STATE}; 4 | use crate::search::{Searcher, MATE_LOWER}; 5 | use crate::ui::{from_fen, parse_move, render_board, render_move}; 6 | use std::time::{Duration, Instant}; 7 | 8 | #[test] 9 | fn sicilian() { 10 | // Test FEN loading is coerent with move making 11 | let mut board_state = INITIAL_BOARD_STATE; 12 | 13 | let sicilian_moves = vec![ 14 | "e2e4", "f2f4", "g1f3", "g1f3", "d2d4", "f4e5", "f3d4", "f3e5", "d1d4", "d2d3", "b1c3", 15 | "h2h3", "c1e3", 16 | ]; 17 | let sicilian_fens = vec![ 18 | "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", 19 | "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", 20 | "rnbqkbnr/pp1ppppp/8/2p5/4P3/8/PPPP1PPP/RNBQKBNR w KQkq c6 0 2", 21 | "rnbqkbnr/pp1ppppp/8/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2", 22 | "r1bqkbnr/pp1ppppp/2n5/2p5/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", 23 | "r1bqkbnr/pp1ppppp/2n5/2p5/3PP3/5N2/PPP2PPP/RNBQKB1R b KQkq d3 0 3", 24 | "r1bqkbnr/pp1ppppp/2n5/8/3pP3/5N2/PPP2PPP/RNBQKB1R w KQkq - 0 4", 25 | "r1bqkbnr/pp1ppppp/2n5/8/3NP3/8/PPP2PPP/RNBQKB1R b KQkq - 0 4", 26 | "r1bqkbnr/pp1ppppp/8/8/3nP3/8/PPP2PPP/RNBQKB1R w KQkq - 0 5", 27 | "r1bqkbnr/pp1ppppp/8/8/3QP3/8/PPP2PPP/RNB1KB1R b KQkq - 0 5", 28 | "r1bqkbnr/pp1p1ppp/4p3/8/3QP3/8/PPP2PPP/RNB1KB1R w KQkq - 0 6", 29 | "r1bqkbnr/pp1p1ppp/4p3/8/3QP3/2N5/PPP2PPP/R1B1KB1R b KQkq - 1 6", 30 | "r1bqkbnr/1p1p1ppp/p3p3/8/3QP3/2N5/PPP2PPP/R1B1KB1R w KQkq - 0 7", 31 | ]; 32 | let sicilian_possible_moves = vec![ 33 | vec![ 34 | "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", "d2d3", "d2d4", "e2e3", "e2e4", "f2f3", 35 | "f2f4", "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "g1h3", "g1f3", 36 | ], 37 | vec![ 38 | "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", "d2d3", "d2d4", "e2e3", "e2e4", "f2f3", 39 | "f2f4", "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "g1h3", "g1f3", 40 | ], 41 | vec![ 42 | "e4e5", "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", "d2d3", "d2d4", "f2f3", "f2f4", 43 | "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "d1e2", "d1f3", "d1g4", "d1h5", "e1e2", 44 | "f1e2", "f1d3", "f1c4", "f1b5", "f1a6", "g1h3", "g1f3", "g1e2", 45 | ], 46 | vec![ 47 | "f4f5", "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", "d2d3", "d2d4", "e2e3", "e2e4", 48 | "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "e1f2", "e1g3", "e1h4", "g1h3", "g1f3", 49 | ], 50 | vec![ 51 | "e4e5", "f3g5", "f3e5", "f3d4", "f3g1", "f3h4", "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", 52 | "c2c4", "d2d3", "d2d4", "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "d1e2", "e1e2", 53 | "f1e2", "f1d3", "f1c4", "f1b5", "f1a6", "h1g1", 54 | ], 55 | vec![ 56 | "f4f5", "f4e5", "f3g5", "f3e5", "f3d4", "f3g1", "f3h4", "a2a3", "a2a4", "b2b3", "b2b4", 57 | "c2c3", "c2c4", "d2d3", "d2d4", "e2e3", "e2e4", "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", 58 | "b1a3", "e1f2", "e1g3", "e1h4", "h1g1", 59 | ], 60 | vec![ 61 | "e4e5", "f3g5", "f3e5", "f3d4", "f3d2", "f3g1", "f3h4", "a2a3", "a2a4", "b2b3", "b2b4", 62 | "c2c3", "c2c4", "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "b1d2", "c1d2", "c1e3", 63 | "c1f4", "c1g5", "c1h6", "d1d2", "d1d3", "d1d4", "d1e2", "e1e2", "e1d2", "f1e2", "f1d3", 64 | "f1c4", "f1b5", "f1a6", "h1g1", 65 | ], 66 | vec![ 67 | "f3g5", "f3e5", "f3d4", "f3g1", "f3h4", "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", 68 | "d2d3", "d2d4", "e2e3", "e2e4", "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "e1f2", 69 | "e1g3", "e1h4", "h1g1", 70 | ], 71 | vec![ 72 | "e4e5", "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", "f2f3", "f2f4", "g2g3", "g2g4", 73 | "h2h3", "h2h4", "b1c3", "b1a3", "b1d2", "c1d2", "c1e3", "c1f4", "c1g5", "c1h6", "d1d2", 74 | "d1d3", "d1d4", "d1e2", "d1f3", "d1g4", "d1h5", "e1e2", "e1d2", "f1e2", "f1d3", "f1c4", 75 | "f1b5", "f1a6", "h1g1", 76 | ], 77 | vec![ 78 | "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", "d2d3", "d2d4", "e2e3", "e2e4", "g2g3", 79 | "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "e1f2", "e1g3", "e1h4", "h1g1", 80 | ], 81 | vec![ 82 | "d4d5", "d4d6", "d4d7", "d4c4", "d4b4", "d4a4", "d4d3", "d4d2", "d4d1", "d4e5", "d4f6", 83 | "d4g7", "d4c5", "d4b6", "d4a7", "d4c3", "d4e3", "e4e5", "a2a3", "a2a4", "b2b3", "b2b4", 84 | "c2c3", "c2c4", "f2f3", "f2f4", "g2g3", "g2g4", "h2h3", "h2h4", "b1c3", "b1a3", "b1d2", 85 | "c1d2", "c1e3", "c1f4", "c1g5", "c1h6", "e1e2", "e1d1", "e1d2", "f1e2", "f1d3", "f1c4", 86 | "f1b5", "f1a6", "h1g1", 87 | ], 88 | vec![ 89 | "d3d4", "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", "e2e3", "e2e4", "g2g3", "g2g4", 90 | "h2h3", "h2h4", "b1c3", "b1a3", "b1d2", "c1d2", "c1e3", "c1f4", "c1g5", "c1h6", "d1d2", 91 | "e1f2", "e1g3", "e1h4", "e1d2", "e1c3", "e1b4", "e1a5", "h1g1", 92 | ], 93 | vec![ 94 | "d4d5", "d4d6", "d4d7", "d4c4", "d4b4", "d4a4", "d4d3", "d4d2", "d4d1", "d4e5", "d4f6", 95 | "d4g7", "d4c5", "d4b6", "d4a7", "d4e3", "e4e5", "c3d5", "c3b5", "c3a4", "c3b1", "c3d1", 96 | "c3e2", "a2a3", "a2a4", "b2b3", "b2b4", "f2f3", "f2f4", "g2g3", "g2g4", "h2h3", "h2h4", 97 | "a1b1", "c1d2", "c1e3", "c1f4", "c1g5", "c1h6", "e1e2", "e1d1", "e1d2", "f1e2", "f1d3", 98 | "f1c4", "f1b5", "f1a6", "h1g1", 99 | ], 100 | ]; 101 | 102 | for ((fen, next_move), mut move_list) in sicilian_fens 103 | .iter() 104 | .zip(sicilian_moves) 105 | .zip(sicilian_possible_moves) 106 | { 107 | assert_eq!(render_board(&board_state), render_board(&from_fen(fen))); 108 | assert_eq!(&board_state, &from_fen(fen)); 109 | 110 | // Compare sorted vecs to ignore move ordering 111 | move_list.sort(); 112 | let mut generated_moves = gen_moves(&board_state) 113 | .iter() 114 | .map(render_move) 115 | .collect::>(); 116 | generated_moves.sort(); 117 | assert_eq!(move_list, generated_moves); 118 | board_state = after_move(&board_state, &parse_move(next_move)); 119 | } 120 | } 121 | 122 | #[test] 123 | fn moves() { 124 | let move_fens = vec![ 125 | "r1b1k2r/3n1p1p/p2PpnpR/qpp1p3/5P2/2N5/PPPQB1P1/1K1R2N1 w kq - 0 16", 126 | "7k/7p/8/1p5R/1P6/2Pb4/1r4PK/8 w - - 1 42", 127 | "8/5p1p/2p1p1pk/4Q3/7P/5qP1/r4P2/2R3K1 b - - 0 34", 128 | "rnbqkb1r/1p2pppp/p4n2/3pP3/2B5/2N2N2/PP3PPP/R1BQK2R w KQkq - 0 8", 129 | "rnbqk2r/1p1n1ppp/p3p3/2b1P3/8/1BN2N2/PP3PPP/R1BQK2R w KQkq - 2 11", 130 | "rnbqk2r/1p1n1ppp/p3p3/2b1P3/8/1BN2N2/PP3PPP/R1BQ1RK1 b kq - 3 11", 131 | "4rrk1/pp2b1pp/3p4/2pP3n/3N4/2NP1P2/PP1K1P1P/R6R w - c6 0 19", 132 | ]; 133 | 134 | let possible_moves = vec![ 135 | vec![ 136 | "h6h7", "h6g6", "h6h5", "h6h4", "h6h3", "h6h2", "h6h1", "f4f5", "f4e5", "c3d5", "c3b5", 137 | "c3a4", "c3e4", "a2a3", "a2a4", "b2b3", "b2b4", "d2d3", "d2d4", "d2d5", "d2e3", "d2c1", 138 | "d2e1", "e2f3", "e2g4", "e2h5", "e2d3", "e2c4", "e2b5", "e2f1", "g2g3", "g2g4", "b1a1", 139 | "b1c1", "d1c1", "d1e1", "d1f1", "g1h3", "g1f3", 140 | ], 141 | vec![ 142 | "h5h6", "h5h7", "h5g5", "h5f5", "h5e5", "h5d5", "h5c5", "h5b5", "h5h4", "h5h3", "c3c4", 143 | "g2g3", "g2g4", "h2h3", "h2h1", "h2g3", "h2g1", 144 | ], 145 | vec![ 146 | "h7h8", "h7g7", "h7f7", "h7e7", "h7d7", "h7c7", "h7h6", "h7h5", "h7h4", "h7h3", "h7h2", 147 | "h7h1", "c6c7", "c6b6", "c6c5", "c6c4", "c6c3", "c6d6", "c6e6", "c6f6", "c6g6", "c6h6", 148 | "c6d7", "c6e8", "c6b7", "c6a8", "c6b5", "c6a4", "c6d5", "c6e4", "a3a4", "a3b4", "a3b2", 149 | "b3b4", "f3f4", "c2c3", "c2c4", 150 | ], 151 | vec![ 152 | "e5e6", "e5f6", "c4d5", "c4b5", "c4a6", "c4b3", "c4d3", "c4e2", "c4f1", "c3d5", "c3b5", 153 | "c3a4", "c3b1", "c3e2", "c3e4", "f3g5", "f3d4", "f3d2", "f3g1", "f3h4", "a2a3", "a2a4", 154 | "b2b3", "b2b4", "g2g3", "g2g4", "h2h3", "h2h4", "a1b1", "c1d2", "c1e3", "c1f4", "c1g5", 155 | "c1h6", "d1d2", "d1d3", "d1d4", "d1d5", "d1e2", "d1c2", "d1b3", "d1a4", "e1e2", "e1f1", 156 | "e1d2", "h1g1", "h1f1", "e1g1", 157 | ], 158 | vec![ 159 | "b3c4", "b3d5", "b3e6", "b3a4", "b3c2", "c3d5", "c3b5", "c3a4", "c3b1", "c3e2", "c3e4", 160 | "f3g5", "f3d4", "f3d2", "f3g1", "f3h4", "a2a3", "a2a4", "g2g3", "g2g4", "h2h3", "h2h4", 161 | "a1b1", "c1d2", "c1e3", "c1f4", "c1g5", "c1h6", "d1d2", "d1d3", "d1d4", "d1d5", "d1d6", 162 | "d1d7", "d1e2", "d1c2", "e1e2", "e1f1", "e1d2", "h1g1", "h1f1", "e1g1", 163 | ], 164 | vec![ 165 | "f4g5", "f4h6", "f4e5", "f4d6", "f4c7", "f4e3", "f4d2", "f4c1", "f4g3", "f4h2", "h3h4", 166 | "a2a3", "a2a4", "b2b3", "b2b4", "c2c3", "c2c4", "e2d4", "e2c3", "e2c1", "e2g3", "g2g3", 167 | "g2g4", "a1b1", "a1c1", "d1b1", "d1d2", "d1c1", "e1f2", "e1g3", "e1h4", "e1d2", "e1c3", 168 | "e1b4", "e1a5", "g1f3", "h1h2", 169 | ], 170 | vec![ 171 | "d5c6", "d4e6", "d4c6", "d4b5", "d4b3", "d4c2", "d4e2", "d4f5", "c3b5", "c3a4", "c3b1", 172 | "c3d1", "c3e2", "c3e4", "f3f4", "a2a3", "a2a4", "b2b3", "b2b4", "d2c2", "d2d1", "d2e2", 173 | "d2e3", "d2c1", "d2e1", "h2h3", "h2h4", "a1b1", "a1c1", "a1d1", "a1e1", "a1f1", "a1g1", 174 | "h1g1", "h1f1", "h1e1", "h1d1", "h1c1", "h1b1", 175 | ], 176 | ]; 177 | 178 | for (fen, movelist) in move_fens.iter().zip(possible_moves) { 179 | let board_state = from_fen(fen); 180 | assert_eq!( 181 | movelist, 182 | gen_moves(&board_state) 183 | .iter() 184 | .map(render_move) 185 | .collect::>(), 186 | ); 187 | } 188 | } 189 | 190 | #[test] 191 | fn mates() { 192 | // Since search exits early on mate found, can be used for benchmarking 193 | let mate_fens = vec![ 194 | "1r1r1n1k/4qpnP/p1b1p1pQ/P2pP1N1/2pP2P1/1pP5/1P3PK1/RB5R w - - 7 31", 195 | "r5qr/p1R1B1p1/Q3p3/4Pp2/4n1k1/1P2P1Pp/P4P1P/5RK1 w - - 3 22", 196 | "r5qr/p1R1B3/4p1k1/4P1p1/4pR2/1P2P1Pp/P3Q2P/6K1 w - - 2 26", 197 | "2b1r1k1/5p2/1pp4Q/4p3/6p1/r2B1P2/2P3PP/q1B1K1NR w K - 0 22", 198 | "r1bq1b1r/ppp4p/2n3p1/4p3/3Pp3/4B1P1/PPP1QP1P/R3K2k w Q - 0 15", 199 | "2kr1b1r/R3qp1p/bQ5p/2Pp4/3P4/5N2/5PPP/5K1R w - - 0 20", 200 | "r2qkb1r/ppp2ppp/2n2n2/8/2BP1P2/1Q3b2/PP4PP/RNB1K2R w KQkq - 0 9", 201 | "3N4/p6k/b3N1pp/3pp3/R4p2/7P/P1r3PB/6K1 b - - 1 34", 202 | "2rR4/p4k2/1p2p2Q/5p2/5P2/8/PPP3q1/1KB4R b - - 0 27", 203 | "3r1r1k/1p2Nppp/p4n2/P1p1p3/4P3/6Pq/2P1NP2/R1B1QRK1 b - - 2 18", 204 | ]; 205 | 206 | let mate_solutions = vec![ 207 | "h6g7", "a6e2", "f4f6", "d3h7", "e2f1", "b6a6", "c4f7", "f7f8", "b7f7", "c3b5", 208 | ]; 209 | 210 | let time_for_mate = Duration::new(10, 0); // Max time to solve, should take much less N.B. compile as --release 211 | 212 | let mates_start_time = Instant::now(); 213 | for (puzzle, solution) in mate_fens.iter().zip(mate_solutions) { 214 | let mut searcher = Searcher::default(); 215 | // println!("{}", render_board(&from_fen(puzzle))); 216 | let mate_start_time = Instant::now(); 217 | let (top_move, score, depth) = searcher.search(from_fen(puzzle), time_for_mate); 218 | println!( 219 | "Reached depth {} in {:?} nodes {} score {}", 220 | depth, 221 | mate_start_time.elapsed(), 222 | searcher.nodes, 223 | score 224 | ); 225 | assert_eq!(render_move(&top_move), solution); 226 | assert!(score > MATE_LOWER); 227 | } 228 | println!( 229 | "mates solved in {}ms, should not take more than 5000ms", 230 | mates_start_time.elapsed().as_millis() 231 | ); 232 | } 233 | 234 | #[test] 235 | fn puzzles() { 236 | let puzzle_fens = vec![ 237 | "r5k1/1q3ppp/4p3/2p1P3/N7/2P5/P1R1QPPP/6K1 b - - 0 27", 238 | "r3r1k1/pp1n2p1/2pq2p1/3p1p2/3P4/1NP2PnP/PP4P1/R1Q1RNK1 b - - 2 22", 239 | "r3k2r/1p3ppp/1qnbpn2/pP1p4/3P1P2/2PB1Q2/P2N2PP/R1B2RK1 b kq - 0 12", 240 | "r1bq1rk1/1p3pp1/p2p1n1p/2b1p3/2PnP3/P1NB4/1P1QNPPP/R1B2RK1 b - - 0 12", 241 | "1r4k1/pp1r1p1p/2pp1P1Q/6P1/8/3q4/P5BP/4R1K1 b - - 1 27", 242 | "r2q1rk1/1p3ppp/p2bb3/3pn3/8/P1N1Q3/1PP1BPPP/R1B2RK1 b - - 9 16", 243 | "r2q1rk1/1b2b1pp/p1p1p3/2npPp2/3N1P2/2N1B3/PPP3PP/2RQ1RK1 w - - 0 1", 244 | "r2qkb1r/5ppp/2np1n2/1N2p1B1/2b1P3/2N2P2/PPP3PP/R2QR1K1 b kq - 0 1", 245 | "3r1rk1/1p4pp/2p2p2/p1b1BQ2/1qP5/1B1P3P/PP4P1/R6K w - - 0 22", 246 | "8/6pk/3r1qpp/4N2P/3PQ3/8/5PP1/6K1 w - - 1 41", 247 | "2kr3r/pp2nppp/4p3/2p1Nq2/P7/2P5/1PnB1PPP/R2QR1K1 w - - 1 19", 248 | ]; 249 | 250 | let puzzle_solutions = vec![ 251 | "g2g8", "b6d7", "f3e5", "e5g6", "e6b3", "e4e5", "b2b4", "f5g4", "e5c3", "h5g6", "g2g4", 252 | ]; 253 | let time_for_puzzle = Duration::from_millis(1600); 254 | for (puzzle, solution) in puzzle_fens.iter().zip(puzzle_solutions) { 255 | let mut searcher = Searcher::default(); 256 | let solve_start_time = Instant::now(); 257 | 258 | let (top_move, score, depth) = searcher.search(from_fen(puzzle), time_for_puzzle); 259 | println!( 260 | "Reached depth {} with score {} with nodes {} in {:?}", 261 | depth, 262 | score, 263 | searcher.nodes, 264 | solve_start_time.elapsed() 265 | ); 266 | println!("puzzle {} solution {}", puzzle, solution); 267 | assert_eq!(render_move(&top_move), solution); 268 | } 269 | } 270 | -------------------------------------------------------------------------------- /src/uci.rs: -------------------------------------------------------------------------------- 1 | use log::{info, trace, warn}; 2 | use std::time::Duration; 3 | 4 | use crate::board::{after_move, gen_moves, A8, BOARD_SIZE, H8, INITIAL_BOARD_STATE}; 5 | use crate::pieces::Square; 6 | use crate::search::Searcher; 7 | use crate::ui::{parse_move, render_move}; 8 | 9 | fn read_line() -> String { 10 | let mut line = String::new(); 11 | std::io::stdin().read_line(&mut line).unwrap(); 12 | line.pop(); 13 | line 14 | } 15 | 16 | pub fn uci_loop() { 17 | println!("Sunfish_rs"); 18 | let mut board_state = INITIAL_BOARD_STATE; 19 | let mut am_black = false; 20 | loop { 21 | let mut searcher = Searcher::default(); 22 | let next_command = read_line(); 23 | trace!("Received command {}", next_command); 24 | match next_command.split(' ').next().unwrap() { 25 | "quit" => return, 26 | "uci" => println!("uciok"), 27 | "isready" => println!("readyok"), 28 | "ucinewgame" => board_state = INITIAL_BOARD_STATE, 29 | "position" => { 30 | //position startpos moves d2d4 d7d5 e2e4 d5e4 31 | info!("loading moves"); 32 | let moves: Vec<&str> = next_command.split(' ').collect(); 33 | if moves.len() == 2 && moves[1] != "startpos" { 34 | warn!("UNKNOWN FORMAT!"); 35 | panic!(); 36 | } else if moves.len() > 2 37 | && (moves[0] != "position" || moves[1] != "startpos" || moves[2] != "moves") 38 | { 39 | warn!("UNKNOWN FORMAT!"); 40 | panic!(); 41 | } 42 | board_state = INITIAL_BOARD_STATE; 43 | am_black = false; 44 | for move_ in moves.iter().skip(3) { 45 | let mut parsed_move = parse_move(move_); 46 | if am_black { 47 | parsed_move.0 = BOARD_SIZE - 1 - parsed_move.0; 48 | parsed_move.1 = BOARD_SIZE - 1 - parsed_move.1; 49 | }; 50 | if !gen_moves(&board_state).contains(&parsed_move) { 51 | warn!( 52 | "Trying to make an illegal move {:?}, will probably fail", 53 | parsed_move 54 | ); 55 | } 56 | board_state = after_move(&board_state, &parsed_move); 57 | searcher.set_eval_to_zero(&board_state); 58 | am_black = !am_black; 59 | } 60 | // print_board(&board_state); 61 | } 62 | "go" => { 63 | // TODO: refactor time management, should be somewhere else 64 | 65 | // Command format is going to be: 66 | // go wtime 391360 btime 321390 winc 8000 binc 8000 67 | let infos: Vec<&str> = next_command.split(' ').collect(); 68 | 69 | // Just try to copy opponent time management 70 | let time_difference: i32 = if infos.len() < 9 { 71 | 4_000 // If I have no information, assume I have 4 seconds, used also for first move 72 | } else if am_black { 73 | infos[4].parse::().expect("Failed to btime") 74 | - infos[2].parse::().expect("Failed to parse wtime") 75 | } else { 76 | infos[2].parse::().expect("Failed to parse wtime") 77 | - infos[4].parse::().expect("Failed to parse btime") 78 | }; 79 | 80 | let increment: i32 = if infos.len() < 9 { 81 | 0 // Assume no increment 82 | } else if am_black { 83 | infos[8].parse::().expect("Failed to parse binc") 84 | } else { 85 | infos[6].parse::().expect("Failed to parse winc") 86 | }; 87 | 88 | let mut nanos_for_move: i64 = 89 | i64::from(time_difference + increment - 3_000) * 1_000_000; 90 | 91 | if nanos_for_move < (increment * 800_000).into() { 92 | nanos_for_move = (increment * 800_000).into(); 93 | } 94 | 95 | if nanos_for_move > 40_000_000 { 96 | nanos_for_move = 40_000_000; 97 | } 98 | 99 | if nanos_for_move > 1_700_000_000 { 100 | nanos_for_move -= 200_000_000 // Account for lag 101 | } else { 102 | nanos_for_move = 500_000_000 // Minimum reasonable move time 103 | } 104 | 105 | let time_for_move = Duration::new( 106 | nanos_for_move as u64 / 1_000_000_000, 107 | (nanos_for_move % 1_000_000_000) as u32, 108 | ); 109 | info!( 110 | "Computing move giving time {:?} with {}s difference and {}s increment", 111 | time_for_move, 112 | time_difference / 1000, 113 | increment / 1000, 114 | ); 115 | // TODO parse_movetime 116 | let (mut top_move, _score, _depth) = searcher.search(board_state, time_for_move); 117 | let is_promotion = (A8 <= top_move.1 && top_move.1 <= H8) 118 | && board_state.board[top_move.0] == Square::MyPawn; 119 | if am_black { 120 | top_move.0 = BOARD_SIZE - 1 - top_move.0; 121 | top_move.1 = BOARD_SIZE - 1 - top_move.1; 122 | }; 123 | if is_promotion { 124 | println!("bestmove {}q ponder e7e5", render_move(&top_move)); 125 | } else { 126 | println!("bestmove {} ponder e7e5", render_move(&top_move)); 127 | } 128 | info!("Sending bestmove {}", render_move(&top_move)); 129 | info!( 130 | "Searched {} nodes, reached depth {}, estimate score {}, tables at {} and {}", 131 | searcher.nodes, 132 | _depth, 133 | _score, 134 | searcher.move_transposition_table.len(), 135 | searcher.score_transposition_table.len() 136 | ); 137 | } 138 | _ => { 139 | warn!("UNKNOWN COMMAND {}", next_command); 140 | println!("Unknown command:{}", next_command); 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/ui.rs: -------------------------------------------------------------------------------- 1 | use crate::board::{rotate, static_score, BoardState, A1, A8, BOARD_SIDE, BOARD_SIZE, PADDING}; 2 | use crate::pieces::Square; 3 | 4 | pub fn parse_move(move_: &str) -> (usize, usize) { 5 | let from = parse_coordinates(&move_[..2]); 6 | let to = parse_coordinates(&move_[2..]); 7 | (from, to) 8 | } 9 | 10 | pub fn parse_coordinates(coordinates: &str) -> usize { 11 | let mut chars = coordinates.chars(); 12 | let file = chars.next().expect("Failed to parse coordinates"); 13 | let rank = chars.next().expect("Failed to parse coordinates"); 14 | A1 + (file as i32 - 'a' as i32) as usize 15 | - BOARD_SIDE as usize * ((rank as i32 - '1' as i32) as usize) 16 | } 17 | 18 | pub fn render_move(move_: &(usize, usize)) -> String { 19 | render_coordinates(move_.0) + &render_coordinates(move_.1) 20 | } 21 | 22 | fn render_coordinates(position: usize) -> String { 23 | let rank = b'8' - ((position - A8) as u8 / BOARD_SIDE as u8); 24 | let file = (position - A8) as u8 % BOARD_SIDE as u8 + b'a'; 25 | [file as char, rank as char].iter().collect() 26 | } 27 | 28 | impl Square { 29 | pub fn to_unicode(self) -> char { 30 | match self { 31 | Square::MyRook => '♜', 32 | Square::MyKnight => '♞', 33 | Square::MyBishop => '♝', 34 | Square::MyQueen => '♛', 35 | Square::MyKing => '♚', 36 | Square::MyPawn => '♟', 37 | Square::OpponentRook => '♖', 38 | Square::OpponentKnight => '♘', 39 | Square::OpponentBishop => '♗', 40 | Square::OpponentQueen => '♕', 41 | Square::OpponentKing => '♔', 42 | Square::OpponentPawn => '♙', 43 | Square::Empty => '·', 44 | Square::Wall => 'X', 45 | } 46 | } 47 | } 48 | 49 | pub fn render_board(board_state: &BoardState) -> String { 50 | let mut rendered_board: String = String::from(""); 51 | 52 | for (i, row) in board_state 53 | .board 54 | .chunks(BOARD_SIDE) 55 | .skip(PADDING) 56 | .take(8) 57 | .enumerate() 58 | { 59 | rendered_board.push_str(&format!(" {} ", 8 - i)); 60 | for p in row.iter().skip(PADDING).take(8) { 61 | rendered_board.push_str(&format!(" {}", p.to_unicode())); 62 | } 63 | rendered_board.push_str("\n"); 64 | } 65 | rendered_board.push_str(" a b c d e f g h \n\n"); 66 | rendered_board.push_str(&format!("Static score: {}\n", board_state.score)); 67 | if static_score(board_state.board) != board_state.score { 68 | rendered_board.push_str(&format!( 69 | "STATIC SCORE ERROR, SHOULD BE: {}\n", 70 | static_score(board_state.board), 71 | )); 72 | } 73 | if board_state.en_passant_position.is_some() { 74 | rendered_board.push_str(&format!( 75 | "En passant is {:?}\n", 76 | board_state.en_passant_position 77 | )); 78 | } 79 | rendered_board.push_str(&format!( 80 | "Castling rights are {:?} {:?}\n", 81 | board_state.my_castling_rights, board_state.opponent_castling_rights, 82 | )); 83 | rendered_board 84 | } 85 | 86 | // https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation#Definition 87 | pub fn from_fen(fen: &str) -> BoardState { 88 | let mut new_board = [Square::Empty; BOARD_SIZE]; 89 | let fields = fen.split(' ').collect::>(); 90 | let mut board_string: String = fields[0].into(); 91 | let (turn, castling, en_passant, _halfmoves, _fullmoves) = 92 | (fields[1], fields[2], fields[3], fields[4], fields[5]); 93 | 94 | for (dv, dc) in "0123456789".chars().enumerate() { 95 | board_string = board_string.replace(dc, &"_".repeat(dv)); 96 | } 97 | 98 | let board_lines: Vec> = board_string 99 | .split('/') 100 | .map(|s| s.chars().collect()) 101 | .collect(); 102 | 103 | for rank in 0..BOARD_SIDE { 104 | for file in 0..BOARD_SIDE { 105 | let position = rank * BOARD_SIDE + file; 106 | new_board[position] = if rank < PADDING 107 | || file < PADDING 108 | || BOARD_SIDE - rank <= PADDING 109 | || BOARD_SIDE - file <= PADDING 110 | { 111 | Square::Wall 112 | } else { 113 | match board_lines[rank - PADDING][file - PADDING] { 114 | 'P' => Square::MyPawn, 115 | 'N' => Square::MyKnight, 116 | 'B' => Square::MyBishop, 117 | 'R' => Square::MyRook, 118 | 'Q' => Square::MyQueen, 119 | 'K' => Square::MyKing, 120 | 'p' => Square::OpponentPawn, 121 | 'n' => Square::OpponentKnight, 122 | 'b' => Square::OpponentBishop, 123 | 'r' => Square::OpponentRook, 124 | 'q' => Square::OpponentQueen, 125 | 'k' => Square::OpponentKing, 126 | _ => Square::Empty, 127 | } 128 | } 129 | } 130 | } 131 | 132 | let en_passant_position = if en_passant == "-" { 133 | None 134 | } else { 135 | Some(parse_coordinates(en_passant)) 136 | }; 137 | 138 | let my_castling_rights = (castling.contains('Q'), castling.contains('K')); 139 | let opponent_castling_rights = (castling.contains('k'), castling.contains('q')); 140 | 141 | let mut boardstate = BoardState { 142 | board: new_board, 143 | score: static_score(new_board), 144 | my_castling_rights, 145 | opponent_castling_rights, 146 | en_passant_position, 147 | king_passant_position: None, // is not useful for legal board states 148 | }; 149 | 150 | if turn == "b" { 151 | rotate(&mut boardstate); 152 | } 153 | 154 | boardstate 155 | } 156 | --------------------------------------------------------------------------------