From 8f328a280925b90980c3a2ff69891e85b00e5bf3 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:48:29 -0600 Subject: [PATCH 01/47] 1/6/24 6:48 PM Made some tiny changes to the search function, but it got +4 wins over the previous version! --- Cargo.toml | 2 +- src/bot.rs | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 00ea054..2c96e3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maxwell" -version = "3.0.8" +version = "3.0.9" edition = "2021" [dependencies] diff --git a/src/bot.rs b/src/bot.rs index ba51174..3fe6424 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -124,9 +124,8 @@ impl Bot { } self.time_to_think = - if my_time == 0.0 { // This means we're in a "go depth X" command - 0.0 - } else if self.config.time_management { + if self.config.time_management + && my_time > 0.0 { let time_percentage = if board.moves.len() / 2 <= 6 { PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES } else { @@ -254,9 +253,9 @@ impl Bot { return data.evaluation; } - let is_pv = alpha != beta - 1; + let not_pv = alpha == beta - 1; - if !is_pv + if not_pv && depth > 0 && depth_left > 0 && board.get_last_move().capture == NO_PIECE as u8 @@ -344,7 +343,8 @@ impl Bot { && i >= 3 && depth_left >= 3 && m.capture == NO_PIECE as u8 { - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1 - 1, -alpha - 1, -alpha, number_of_extensions); + // Changing this to depth_left - 3 instead of depth_left - 2 brought it up by 1 win, but I'm not sure if that's worth it + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 2, -alpha - 1, -alpha, number_of_extensions); needs_full_search = evaluation > alpha; } @@ -385,7 +385,8 @@ impl Bot { } } - if best_move_this_search != NULL_MOVE { + if not_pv + && best_move_this_search != NULL_MOVE { self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); } From 469a5c70b9f06d523df24439cf4190e60268c258 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sun, 7 Jan 2024 16:12:44 -0600 Subject: [PATCH 02/47] 1/7/24 4:12 PM --- README.md | 3 ++- src/board.rs | 9 ++++++--- src/bot.rs | 16 +++++++++++++--- src/main.rs | 5 +++++ src/piece_square_tables.rs | 2 +- src/utils.rs | 4 ++++ 6 files changed, 31 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index c4f4bd8..72145ee 100644 --- a/README.md +++ b/README.md @@ -60,4 +60,5 @@ - [The Chess Programming Wiki](https://www.chessprogramming.org/Main_Page) - [BBC Engine Development](https://www.youtube.com/playlist?list=PLmN0neTso3Jxh8ZIylk74JpwfiWNI76Cs) - [Lynx](https://github.com/lynx-chess/Lynx/) - - [Weiawaga](https://github.com/Heiaha/Weiawaga/) \ No newline at end of file + - [Weiawaga](https://github.com/Heiaha/Weiawaga/) + - [Fruit 2.1](https://github.com/Warpten/Fruit-2.1/) \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index f53e361..bf02056 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1,5 +1,5 @@ use crate::value_holder::ValueHolder; -use crate::utils::{pop_lsb, print_bitboard, coordinate_to_index}; +use crate::utils::{pop_lsb, get_lsb, print_bitboard, coordinate_to_index}; use crate::piece_square_tables::{get_base_worth_of_piece, get_full_worth_of_piece, ROOK_WORTH, BISHOP_WORTH}; use crate::precalculated_move_data::*; use crate::move_data::*; @@ -948,8 +948,8 @@ impl Board { let white_attacked_squares = self.attacked_squares_bitboards[1].count_ones() as i32; let black_attacked_squares = self.attacked_squares_bitboards[0].count_ones() as i32; - let white_king_index = pop_lsb(&mut (self.piece_bitboards[WHITE_KING].clone())) as usize; - let black_king_index = pop_lsb(&mut (self.piece_bitboards[BLACK_KING].clone())) as usize; + let white_king_index = get_lsb(self.piece_bitboards[WHITE_KING]) as usize; + let black_king_index = get_lsb(self.piece_bitboards[BLACK_KING]) as usize; let weak_squares_around_white_king = (( self.precalculated_move_data.king_attacks[white_king_index] @@ -961,6 +961,9 @@ impl Board { & self.attacked_squares_bitboards[1] ).count_ones() as f32 * (1.0 - endgame)) as i32; + // let weak_lines_from_white_king = (self.calculate_queen_attack_bitboard(white_king_index).count_ones() as f32 * (1.0 - endgame)) as i32; + // let weak_lines_from_black_king = (self.calculate_queen_attack_bitboard(black_king_index).count_ones() as f32 * (1.0 - endgame)) as i32; + ((white_material + white_attacked_squares * 10 - weak_squares_around_white_king * 20 + white_pawn_evaluation) - (black_material + black_attacked_squares * 10 - weak_squares_around_black_king * 20 + black_pawn_evaluation)) * self.perspective() } diff --git a/src/bot.rs b/src/bot.rs index 3fe6424..30e8d14 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -12,6 +12,7 @@ use crate::Board; pub const MAX_SEARCH_EXTENSIONS: u8 = 16; pub const FUTILITY_PRUNING_THESHOLD_PER_PLY: i32 = 60; pub const RAZORING_THRESHOLD_PER_PLY: i32 = 300; +pub const HISTORY_THRESHOLD: i32 = 1000; // TODO: tweak this pub const PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES: f32 = 0.025; // 2.5% pub const PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES: f32 = 0.07; // 7% @@ -176,6 +177,7 @@ impl Bot { || self.searched_one_move { self.best_move = self.best_move_this_iteration; self.evaluation = self.evaluation_this_iteration; + // last_evaluation = self.evaluation; // TODO: try this } self.println(format!("Depth: {}, Window: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}", @@ -342,9 +344,17 @@ impl Bot { if search_extension == 0 && i >= 3 && depth_left >= 3 - && m.capture == NO_PIECE as u8 { - // Changing this to depth_left - 3 instead of depth_left - 2 brought it up by 1 win, but I'm not sure if that's worth it - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 2, -alpha - 1, -alpha, number_of_extensions); + && m.capture == NO_PIECE as u8 + && !board.king_in_check(board.white_to_move) { + let reduction = 1; + + // if depth_left > 3 { + // let history_value = self.move_sorter.history[m.piece as usize][m.to as usize]; + // reduction += if history_value < HISTORY_THRESHOLD { 1 } else { 0 }; + // } + + // Subtracting one more ply from this brought it up by 1 win, but I'm not sure if that's worth it + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, number_of_extensions); needs_full_search = evaluation > alpha; } diff --git a/src/main.rs b/src/main.rs index b54a4b3..f7f0070 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,9 @@ /* TODO +big idea: + remove all constant variables, and put them into the BotConfig struct, + then write my own tuning / matchmaking program that will tweak the values + and play matches until it finds better values... + calculate my own magic numbers; currently "borrowing" Sebastian Lague's ^^ check out pin detection to speed up check detection? try to write a neural network to evaluate positions? :o diff --git a/src/piece_square_tables.rs b/src/piece_square_tables.rs index 25cfd90..7b647e4 100644 --- a/src/piece_square_tables.rs +++ b/src/piece_square_tables.rs @@ -11,7 +11,7 @@ pub const MIDDLEGAME_PAWN_TABLE: [i32; 64] = [ 0, 0, 0, 0, 0, 0, 0, 0, ]; -pub const ENDGAME_PAWN_TABLE: [i32; 64] = [ +pub const ENDGAME_PAWN_TABLE: [i32; 64] = [ // TODO: tweak this (maybe lower the values?) 0, 0, 0, 0, 0, 0, 0, 0, 100,100,100,100,100,100,100,100, 70, 70, 70, 70, 70, 70, 70, 70, diff --git a/src/utils.rs b/src/utils.rs index a514734..43d475f 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,6 +6,10 @@ pub fn pop_lsb(bitboard: &mut u64) -> u8 { i as u8 } +pub fn get_lsb(bitboard: u64) -> u8 { + bitboard.trailing_zeros() as u8 +} + pub fn index_to_coordinate(index: u8) -> String { format!("{}{}", match index % 8 { From 64eaab029fa77794467cc16e48ec74e495fb3c0f Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sun, 7 Jan 2024 21:30:07 -0600 Subject: [PATCH 03/47] Added TODOs everywhere --- src/board.rs | 11 +++++---- src/bot.rs | 65 +++++++++++++++++++++++++++++++++++++++------------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/src/board.rs b/src/board.rs index bf02056..b450af3 100644 --- a/src/board.rs +++ b/src/board.rs @@ -11,10 +11,9 @@ use colored::Colorize; pub const BITBOARD_COUNT: usize = PIECE_COUNT; pub const MAX_ENDGAME_MATERIAL: f32 = (ROOK_WORTH * 2 + BISHOP_WORTH * 2) as f32; -// TODO: still tweaking these :`D -pub const DOUBLED_PAWN_PENALTY: i32 = 35; -pub const ISOLATED_PAWN_PENALTY: i32 = 20; -pub const PASSED_PAWN_BOOST: [i32; 8] = [0, 15, 15, 30, 50, 90, 150, 0]; +pub const DOUBLED_PAWN_PENALTY: i32 = 35; // TODO +pub const ISOLATED_PAWN_PENALTY: i32 = 20; // TODO +pub const PASSED_PAWN_BOOST: [i32; 8] = [0, 15, 15, 30, 50, 90, 150, 0]; // TODO pub struct Board { pub precalculated_move_data: PrecalculatedMoveData, @@ -939,7 +938,7 @@ impl Board { } } - let pawn_evaluation_multiplier = (endgame + 0.3).clamp(0.3, 1.0); + let pawn_evaluation_multiplier = (endgame + 0.3).clamp(0.3, 1.0); // TODO white_pawn_evaluation = (white_pawn_evaluation as f32 * pawn_evaluation_multiplier) as i32; black_pawn_evaluation = (black_pawn_evaluation as f32 * pawn_evaluation_multiplier) as i32; @@ -951,6 +950,8 @@ impl Board { let white_king_index = get_lsb(self.piece_bitboards[WHITE_KING]) as usize; let black_king_index = get_lsb(self.piece_bitboards[BLACK_KING]) as usize; + // TODO: weak squares, weak lines, or none? + let weak_squares_around_white_king = (( self.precalculated_move_data.king_attacks[white_king_index] & self.attacked_squares_bitboards[0] diff --git a/src/bot.rs b/src/bot.rs index 30e8d14..81a1bfa 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,3 +1,9 @@ +/* TODO +test removing: + killer moves + history +*/ + use crate::STARTING_FEN; use crate::piece_square_tables::{PAWN_WORTH, QUEEN_WORTH}; use crate::pieces::{PAWN, PROMOTABLE, NO_PIECE}; @@ -9,10 +15,18 @@ use crate::move_data::{MoveData, NULL_MOVE}; use crate::opening_book::OpeningBook; use crate::Board; -pub const MAX_SEARCH_EXTENSIONS: u8 = 16; -pub const FUTILITY_PRUNING_THESHOLD_PER_PLY: i32 = 60; -pub const RAZORING_THRESHOLD_PER_PLY: i32 = 300; -pub const HISTORY_THRESHOLD: i32 = 1000; // TODO: tweak this +pub const MAX_SEARCH_EXTENSIONS: u8 = 16; // TODO +pub const STARTING_ASPIRATION_WINDOW: i32 = 40; // TODO + +pub const MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE: u8 = 3; // TODO + +pub const MAX_DEPTH_LEFT_FOR_RFP: u8 = 4; // TODO +pub const RFP_THESHOLD_PER_PLY: i32 = 60; // TODO + +pub const MAX_DEPTH_LEFT_FOR_RAZORING: u8 = 3; // TODO +pub const RAZORING_THRESHOLD_PER_PLY: i32 = 300; // TODO + +pub const HISTORY_THRESHOLD: i32 = 1000; // TODO pub const PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES: f32 = 0.025; // 2.5% pub const PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES: f32 = 0.07; // 7% @@ -31,10 +45,12 @@ pub struct BotConfig { impl BotConfig { pub fn from_args(args: Vec) -> Self { let _true = "true".to_string(); + let _false = "false".to_string(); + Self { // This is so ugly lol fen: Self::get_arg_value(&args, "fen").unwrap_or(STARTING_FEN.to_string()), debug_output: Self::get_arg_value(&args, "debug_output").unwrap_or(_true.clone()) == _true, - opening_book: Self::get_arg_value(&args, "opening_book").unwrap_or(_true.clone()) == _true, + opening_book: Self::get_arg_value(&args, "opening_book").unwrap_or(_false.clone()) == _false, time_management: Self::get_arg_value(&args, "time_management").unwrap_or(_true.clone()) == _true, } } @@ -151,7 +167,13 @@ impl Bot { self.move_sorter.clear(); - let mut window = 40; + /* TODO + the only thing to tweak here is the aspiration window: + either the window is outside the iterative deepening and it keeps values between iterations, + or it's inside the iterative deepening, and gets reset every iteration + */ + + let mut window = STARTING_ASPIRATION_WINDOW; self.think_timer = Instant::now(); for depth in 1..=depth_to_search { @@ -177,7 +199,7 @@ impl Bot { || self.searched_one_move { self.best_move = self.best_move_this_iteration; self.evaluation = self.evaluation_this_iteration; - // last_evaluation = self.evaluation; // TODO: try this + // last_evaluation = self.evaluation; } self.println(format!("Depth: {}, Window: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}", @@ -255,6 +277,7 @@ impl Bot { return data.evaluation; } + // TODO: test removing this let not_pv = alpha == beta - 1; if not_pv @@ -262,8 +285,15 @@ impl Bot { && depth_left > 0 && board.get_last_move().capture == NO_PIECE as u8 && !board.king_in_check(board.white_to_move) { + /* TODO: + test removing + null move pruning + reverse futility pruning + razoring + */ + // Null Move Pruning - if depth_left >= 3 + if depth_left >= MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE && board.try_null_move() { // let reduction = 3 - (depth_left - 3) / 2; // This didn't work at all lol let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, number_of_extensions); @@ -278,13 +308,13 @@ impl Bot { let static_eval = board.evaluate(); // Reverse Futility Pruning - if depth_left <= 4 - && static_eval - FUTILITY_PRUNING_THESHOLD_PER_PLY * (depth_left as i32) >= beta { + if depth_left <= MAX_DEPTH_LEFT_FOR_RFP + && static_eval - RFP_THESHOLD_PER_PLY * (depth_left as i32) >= beta { return static_eval; } // Razoring - if depth_left <= 3 + if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { depth_left -= 1; } @@ -321,6 +351,7 @@ impl Bot { let m = sorted_moves[i]; board.make_move(m); + // TOOD: test removing check extension, and promotion extension let mut search_extension = 0; if number_of_extensions < MAX_SEARCH_EXTENSIONS as u8 { if board.king_in_check(board.white_to_move) { @@ -342,19 +373,20 @@ impl Bot { let mut needs_full_search = true; if search_extension == 0 - && i >= 3 - && depth_left >= 3 + && i >= 3 // TODO + && depth_left >= 3 // TODO && m.capture == NO_PIECE as u8 && !board.king_in_check(board.white_to_move) { - let reduction = 1; + // let reduction = 1; + // TODO: test removing / adding in this // if depth_left > 3 { // let history_value = self.move_sorter.history[m.piece as usize][m.to as usize]; // reduction += if history_value < HISTORY_THRESHOLD { 1 } else { 0 }; // } - // Subtracting one more ply from this brought it up by 1 win, but I'm not sure if that's worth it - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, number_of_extensions); + // TODO: tweak the amount that it gets reduced + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 2, -alpha - 1, -alpha, number_of_extensions); needs_full_search = evaluation > alpha; } @@ -428,6 +460,7 @@ impl Bot { let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, u8::MAX); for m in sorted_moves { + // TODO: test removing this // Delta Pruning if !board.king_in_check(board.white_to_move) { let mut threshold = QUEEN_WORTH; From 8453f381fe1702dd0eb7c6c84b2e3f8ba22146ff Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Tue, 9 Jan 2024 09:02:20 -0600 Subject: [PATCH 04/47] 1/9/24 9:02 AM OK so this is a big one: I'm really not happy with how the last one was performing, I think it could've done alot better. So I've commented-out all the big features of the search function, and I'm adding them back slowly, doing decent amounts of testing (50 games of 30 seconds +1 increment, with the Perfect2021 opening book) And doing complete re-implementations of some features Features I've redone so far: Principal Variation Search Late Move Reductions Search Extensions (Check and promotion) And I did some testing for Aspiration Windows, but it seemed to make it play worse --- README.md | 7 +- src/bot.rs | 222 ++++++++++++++++++++++----------------------- src/main.rs | 25 ++++- src/move_sorter.rs | 21 +++-- 4 files changed, 145 insertions(+), 130 deletions(-) diff --git a/README.md b/README.md index 72145ee..bf86d9c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ #### Parameters - fen=\: Sets up the board by a fen string (Doesn't work for UCI games) (default=STARTING_FEN) - debug=\: Toggle debug output that gets outputed per ply (default=true) - - opening_book=\: Toggle opening book (default=true) + - opening_book=\: Toggle built-in opening book (default=false) - time_management=\: Toggle time management, if false the bot will use all the remaining time (default=true) #### UCI Interface - uci, isready, ucinewgame, position, go, stop, and quit commands @@ -61,4 +61,7 @@ - [BBC Engine Development](https://www.youtube.com/playlist?list=PLmN0neTso3Jxh8ZIylk74JpwfiWNI76Cs) - [Lynx](https://github.com/lynx-chess/Lynx/) - [Weiawaga](https://github.com/Heiaha/Weiawaga/) - - [Fruit 2.1](https://github.com/Warpten/Fruit-2.1/) \ No newline at end of file + - [Fruit 2.1](https://github.com/Warpten/Fruit-2.1/) + - [Perfect 2021 Opening Book](https://sites.google.com/site/computerschess/perfect-2021-books) + - [Cute Chess](https://cutechess.com/) + - [PVS Implementation](https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm) \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 81a1bfa..9066dab 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,9 +1,3 @@ -/* TODO -test removing: - killer moves - history -*/ - use crate::STARTING_FEN; use crate::piece_square_tables::{PAWN_WORTH, QUEEN_WORTH}; use crate::pieces::{PAWN, PROMOTABLE, NO_PIECE}; @@ -16,7 +10,7 @@ use crate::opening_book::OpeningBook; use crate::Board; pub const MAX_SEARCH_EXTENSIONS: u8 = 16; // TODO -pub const STARTING_ASPIRATION_WINDOW: i32 = 40; // TODO +pub const ASPIRATION_WINDOW: i32 = 25; // TODO pub const MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE: u8 = 3; // TODO @@ -50,7 +44,7 @@ impl BotConfig { Self { // This is so ugly lol fen: Self::get_arg_value(&args, "fen").unwrap_or(STARTING_FEN.to_string()), debug_output: Self::get_arg_value(&args, "debug_output").unwrap_or(_true.clone()) == _true, - opening_book: Self::get_arg_value(&args, "opening_book").unwrap_or(_false.clone()) == _false, + opening_book: Self::get_arg_value(&args, "opening_book").unwrap_or(_false.clone()) == _true, time_management: Self::get_arg_value(&args, "time_management").unwrap_or(_true.clone()) == _true, } } @@ -156,7 +150,8 @@ impl Bot { self.search_cancelled = false; - let last_evaluation = self.evaluation; + // let mut alpha = -i32::MAX; + // let mut beta = i32::MAX; self.best_move = NULL_MOVE; self.evaluation = 0; @@ -167,14 +162,6 @@ impl Bot { self.move_sorter.clear(); - /* TODO - the only thing to tweak here is the aspiration window: - either the window is outside the iterative deepening and it keeps values between iterations, - or it's inside the iterative deepening, and gets reset every iteration - */ - - let mut window = STARTING_ASPIRATION_WINDOW; - self.think_timer = Instant::now(); for depth in 1..=depth_to_search { self.searched_one_move = false; @@ -182,29 +169,29 @@ impl Bot { self.evaluation_this_iteration = 0; - loop { - let (alpha, beta) = (last_evaluation - window, last_evaluation + window); + // Aspiration Window + // let mut evaluation = self.alpha_beta_search(board, 0, depth, alpha, beta); - let evaluation = self.alpha_beta_search(board, 0, depth, alpha, beta, 0); + // if evaluation <= alpha || evaluation >= beta { + // evaluation = self.alpha_beta_search(board, 0, depth, -i32::MAX, i32::MAX); + // } - if alpha < evaluation && evaluation < beta { - break; - } + // alpha = evaluation - ASPIRATION_WINDOW; + // beta = evaluation + ASPIRATION_WINDOW; - window *= 4; - } + let _ = self.alpha_beta_search(board, 0, depth, -i32::MAX, i32::MAX, 0); if !self.search_cancelled || self.searched_one_move { self.best_move = self.best_move_this_iteration; self.evaluation = self.evaluation_this_iteration; - // last_evaluation = self.evaluation; } - self.println(format!("Depth: {}, Window: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}", + // , Window: {} + self.println(format!("Depth: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}", depth, - window, + // window, self.evaluation * board.perspective(), self.best_move.to_coordinates(), self.positions_searched, @@ -244,10 +231,10 @@ impl Bot { &mut self, board: &mut Board, depth: u8, - mut depth_left: u8, + depth_left: u8, mut alpha: i32, beta: i32, - number_of_extensions: u8, + total_extensions: u8, ) -> i32 { if self.should_cancel_search() { return 0; @@ -265,6 +252,7 @@ impl Bot { } if let Some(data) = self.transposition_table.lookup(board.zobrist.key, depth_left, depth, alpha, beta) { + // TODO: Can I produce a cutoff from here? self.positions_searched -= 1; self.transposition_hits += 1; @@ -278,47 +266,47 @@ impl Bot { } // TODO: test removing this - let not_pv = alpha == beta - 1; - - if not_pv - && depth > 0 - && depth_left > 0 - && board.get_last_move().capture == NO_PIECE as u8 - && !board.king_in_check(board.white_to_move) { - /* TODO: - test removing - null move pruning - reverse futility pruning - razoring - */ - - // Null Move Pruning - if depth_left >= MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE - && board.try_null_move() { - // let reduction = 3 - (depth_left - 3) / 2; // This didn't work at all lol - let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, number_of_extensions); - - board.undo_null_move(); - - if evaluation >= beta { - return evaluation; - } - } - - let static_eval = board.evaluate(); - - // Reverse Futility Pruning - if depth_left <= MAX_DEPTH_LEFT_FOR_RFP - && static_eval - RFP_THESHOLD_PER_PLY * (depth_left as i32) >= beta { - return static_eval; - } - - // Razoring - if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING - && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { - depth_left -= 1; - } - } + // let not_pv = alpha == beta - 1; + + // if not_pv + // && depth > 0 + // && depth_left > 0 + // && board.get_last_move().capture == NO_PIECE as u8 + // && !board.king_in_check(board.white_to_move) { + // /* TODO: + // test removing + // null move pruning + // reverse futility pruning + // razoring + // */ + + // // Null Move Pruning + // if depth_left >= MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE + // && board.try_null_move() { + // // let reduction = 3 - (depth_left - 3) / 2; // This didn't work at all lol + // let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, number_of_extensions); + + // board.undo_null_move(); + + // if evaluation >= beta { + // return evaluation; + // } + // } + + // let static_eval = board.evaluate(); + + // // Reverse Futility Pruning + // if depth_left <= MAX_DEPTH_LEFT_FOR_RFP + // && static_eval - RFP_THESHOLD_PER_PLY * (depth_left as i32) >= beta { + // return static_eval; + // } + + // // Razoring + // if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING + // && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { + // depth_left -= 1; + // } + // } if depth_left == 0 { return self.quiescence_search(board, alpha, beta); @@ -351,49 +339,51 @@ impl Bot { let m = sorted_moves[i]; board.make_move(m); - // TOOD: test removing check extension, and promotion extension - let mut search_extension = 0; - if number_of_extensions < MAX_SEARCH_EXTENSIONS as u8 { + let mut extension = 0; + if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { if board.king_in_check(board.white_to_move) { - search_extension = 1; - } else { - if m.piece == PAWN as u8 { - let rank = m.to / 8; - if rank == 1 || rank == 6 { - search_extension = 1; - } + extension = 1; + } else if m.piece == PAWN as u8 { + let rank = m.to / 8; + if rank == 1 || rank == 6 { + extension = 1; } } } - - - // Late Move Reduction / (Kind of) Principal Variation Search + // Principal Variation Search let mut evaluation = 0; let mut needs_full_search = true; - if search_extension == 0 - && i >= 3 // TODO - && depth_left >= 3 // TODO - && m.capture == NO_PIECE as u8 - && !board.king_in_check(board.white_to_move) { - // let reduction = 1; - - // TODO: test removing / adding in this - // if depth_left > 3 { - // let history_value = self.move_sorter.history[m.piece as usize][m.to as usize]; - // reduction += if history_value < HISTORY_THRESHOLD { 1 } else { 0 }; - // } + if i > 0 { + let mut reduction = 0; + + // Late Move Reduction + if i > 3 + && depth_left > 2 + && depth > 0 + && extension == 0 + && m.capture == NO_PIECE as u8 + && !board.king_in_check(board.white_to_move) { + reduction += 1; + + // TODO + // if depth_left > 3 { + // let history_value = self.move_sorter.history[m.piece as usize][m.to as usize]; + // reduction += if history_value < HISTORY_THRESHOLD { 1 } else { 0 }; + // } + } - // TODO: tweak the amount that it gets reduced - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 2, -alpha - 1, -alpha, number_of_extensions); - needs_full_search = evaluation > alpha; + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, total_extensions + extension); + needs_full_search = evaluation > alpha && evaluation < beta; } if needs_full_search { - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1 + search_extension, -beta, -alpha, number_of_extensions + search_extension); + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1, -beta, -alpha, total_extensions + extension); } + // let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1, -beta, -alpha); + board.undo_last_move(); @@ -405,10 +395,10 @@ impl Bot { if evaluation >= beta { self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); - if m.capture == NO_PIECE as u8 { - self.move_sorter.push_killer_move(m, depth); - self.move_sorter.history[m.piece as usize][m.to as usize] += (depth_left * depth_left) as i32; - } + // if m.capture == NO_PIECE as u8 { + // self.move_sorter.push_killer_move(m, depth); + // self.move_sorter.history[m.piece as usize][m.to as usize] += (depth_left * depth_left) as i32; + // } return beta; } @@ -427,8 +417,8 @@ impl Bot { } } - if not_pv - && best_move_this_search != NULL_MOVE { + // && not_pv + if best_move_this_search != NULL_MOVE { self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); } @@ -462,16 +452,16 @@ impl Bot { for m in sorted_moves { // TODO: test removing this // Delta Pruning - if !board.king_in_check(board.white_to_move) { - let mut threshold = QUEEN_WORTH; - if PROMOTABLE.contains(&m.flag) { - threshold += QUEEN_WORTH - PAWN_WORTH; - } - - if evaluation < alpha - threshold { - continue; - } - } + // if !board.king_in_check(board.white_to_move) { + // let mut threshold = QUEEN_WORTH; + // if PROMOTABLE.contains(&m.flag) { + // threshold += QUEEN_WORTH - PAWN_WORTH; + // } + + // if evaluation < alpha - threshold { + // continue; + // } + // } board.make_move(m); let evaluation = -self.quiescence_search(board, -beta, -alpha); diff --git a/src/main.rs b/src/main.rs index f7f0070..2dbb5ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,26 @@ +/* RESULTS +aspiration window: + Try this again when I have more features added back, and also try the more popular approach: + To drop the entire window if it falls out of bounds, and also with a smaller window + Rank Name Elo +/- Games Score Draw + 1 No window 31 63 100 54.5% 17.0% + 2 Hold window 0 65 100 50.0% 12.0% + 3 Reset window -31 62 100 45.5% 19.0% + +Score of PVS vs No PVS: 27 - 13 - 10 + +Score of PVS & LMR vs PVS: 32 - 10 - 8 + +Extensions: + Rank Name Elo +/- Games Score Draw + 1 All extensions 21 50 150 53.0% 20.7% + 2 Promotion extension -5 50 150 49.3% 21.3% + 3 No extensions -7 50 150 49.0% 20.7% + 4 Check extension -9 49 150 48.7% 22.7% +*/ + + + /* TODO big idea: remove all constant variables, and put them into the BotConfig struct, @@ -11,7 +34,6 @@ figure out how to implement "pondering" to think on opponent's time Ideas I've tried, but they didn't help, or made it play worse (Or I implemented them wrong :P) https://www.chessprogramming.org/Futility_Pruning -https://www.chessprogramming.org/Principal_Variation_Search https://www.chessprogramming.org/Internal_Iterative_Deepening Random ideas to try (from other engines and chessprogramming.org) @@ -23,7 +45,6 @@ https://www.chessprogramming.org/Static_Exchange_Evaluation Some random resources I found: (Not using them right now but they could be useful) https://analog-hors.github.io/site/magic-bitboards/ -https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm */ #![allow(dead_code)] diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 9329f4a..2711523 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -49,10 +49,11 @@ impl MoveSorter { let mut scores = vec![(0, 0); num_of_moves]; // board.calculate_attacked_squares(); - board.calculate_attacked_squares_for_color((!board.white_to_move) as usize); + // board.calculate_attacked_squares_for_color((!board.white_to_move) as usize); + // TODO: test both of these // let squares_i_attack = board.attacked_squares_bitboards[board.white_to_move as usize]; - let squares_opponent_attacks = board.attacked_squares_bitboards[!board.white_to_move as usize]; + // let squares_opponent_attacks = board.attacked_squares_bitboards[!board.white_to_move as usize]; for i in 0..num_of_moves { let m = moves[i]; @@ -66,12 +67,12 @@ impl MoveSorter { // score += (5 * get_full_worth_of_piece(m.capture as usize, m.to as usize, endgame) - get_full_worth_of_piece(m.piece as usize, m.from as usize, endgame)) + 8000; score += MVV_LVA[get_piece_type(m.piece as usize) * 6 + get_piece_type(m.capture as usize)] + 8000; } else { - if depth < MAX_KILLER_MOVE_PLY as u8 - && self.killer_moves[depth as usize].is_killer(m) { - score += 5000; - } + // if depth < MAX_KILLER_MOVE_PLY as u8 + // && self.killer_moves[depth as usize].is_killer(m) { + // score += 5000; + // } - score += self.history[m.piece as usize][m.to as usize]; + // score += self.history[m.piece as usize][m.to as usize]; } if m.flag == SHORT_CASTLE_FLAG @@ -83,9 +84,9 @@ impl MoveSorter { // score += get_full_worth_of_piece(m.piece as usize, m.to as usize, endgame); // } - if squares_opponent_attacks & (1 << m.to) != 0 { - score -= 2 * get_base_worth_of_piece(m.piece as usize); - } + // if squares_opponent_attacks & (1 << m.to) != 0 { + // score -= 2 * get_base_worth_of_piece(m.piece as usize); + // } if PROMOTABLE.contains(&m.flag) { score += get_base_worth_of_piece(build_piece(is_piece_white(m.piece as usize), m.flag as usize)) + 12000; From cde934485e2442c74872f57cce58eb29dfe732e1 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Wed, 10 Jan 2024 09:45:05 -0600 Subject: [PATCH 05/47] 1/10/24 9:45 AM Added tons of TODOs Re-implemented NMP --- .gitignore | 1 + Cargo.toml | 2 +- src/board.rs | 1 + src/bot.rs | 98 +++++++++++++++++++++++++++------------------------- src/main.rs | 13 ++++++- 5 files changed, 66 insertions(+), 49 deletions(-) diff --git a/.gitignore b/.gitignore index 6985cf1..d69461f 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb +sharpener diff --git a/Cargo.toml b/Cargo.toml index 2c96e3e..a9fce0b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "maxwell" -version = "3.0.9" +version = "3.1.0" edition = "2021" [dependencies] diff --git a/src/board.rs b/src/board.rs index b450af3..158e874 100644 --- a/src/board.rs +++ b/src/board.rs @@ -944,6 +944,7 @@ impl Board { self.calculate_attacked_squares(); + // TODO: try taking the square root of this? let white_attacked_squares = self.attacked_squares_bitboards[1].count_ones() as i32; let black_attacked_squares = self.attacked_squares_bitboards[0].count_ones() as i32; diff --git a/src/bot.rs b/src/bot.rs index 9066dab..9702616 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -265,48 +265,51 @@ impl Bot { return data.evaluation; } - // TODO: test removing this - // let not_pv = alpha == beta - 1; - - // if not_pv - // && depth > 0 - // && depth_left > 0 - // && board.get_last_move().capture == NO_PIECE as u8 - // && !board.king_in_check(board.white_to_move) { - // /* TODO: - // test removing - // null move pruning - // reverse futility pruning - // razoring - // */ - - // // Null Move Pruning - // if depth_left >= MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE - // && board.try_null_move() { - // // let reduction = 3 - (depth_left - 3) / 2; // This didn't work at all lol - // let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, number_of_extensions); - - // board.undo_null_move(); - - // if evaluation >= beta { - // return evaluation; - // } - // } - - // let static_eval = board.evaluate(); - - // // Reverse Futility Pruning - // if depth_left <= MAX_DEPTH_LEFT_FOR_RFP - // && static_eval - RFP_THESHOLD_PER_PLY * (depth_left as i32) >= beta { - // return static_eval; - // } - - // // Razoring - // if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING - // && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { - // depth_left -= 1; - // } - // } + let not_pv = alpha == beta - 1; + + if not_pv + && depth > 0 + && depth_left > 0 { // && board.get_last_move().capture == NO_PIECE as u8 This made it play worse + let static_eval = board.evaluate(); + + // Null Move Pruning + if depth_left >= MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE + && static_eval >= beta + && board.total_material_without_pawns > 0 // This doesn't work in king and pawn endgames because of zugzwang + && board.try_null_move() { + // let reduction = 3 - (depth_left - 3) / 2; // This didn't work at all lol + let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, total_extensions); + + board.undo_null_move(); + + if evaluation >= beta { + return evaluation; + } + } + + // TODO + // Reverse Futility Pruning + // if depth_left <= MAX_DEPTH_LEFT_FOR_RFP + // && static_eval - RFP_THESHOLD_PER_PLY * (depth_left as i32) >= beta { + // return static_eval; + // } + + // TODO + // Razoring + // if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING + // && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { + // depth_left -= 1; + // } + + // TODO + /* Internal Iterative Reductions + I don't need to test if there's a hash move here, because if there was + it would have already exited this search + */ + // if depth_left > 3 { + // depth_left -= 1; + // } + } if depth_left == 0 { return self.quiescence_search(board, alpha, beta); @@ -417,10 +420,11 @@ impl Bot { } } - // && not_pv - if best_move_this_search != NULL_MOVE { - self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); - } + // if !not_pv { // lol + /* I don't need to check for a null move here, because the first move in the list + will always be the best move so far */ + self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); + // } alpha } @@ -450,7 +454,7 @@ impl Bot { let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, u8::MAX); for m in sorted_moves { - // TODO: test removing this + // TODO // Delta Pruning // if !board.king_in_check(board.white_to_move) { // let mut threshold = QUEEN_WORTH; diff --git a/src/main.rs b/src/main.rs index 2dbb5ea..56edc36 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,11 +17,22 @@ Extensions: 2 Promotion extension -5 50 150 49.3% 21.3% 3 No extensions -7 50 150 49.0% 20.7% 4 Check extension -9 49 150 48.7% 22.7% + + +Null Move Pruning: + Rank Name Elo +/- Games Score Draw + 1 NMP static eval 40 42 200 55.8% 25.5% + 2 NMP 26 41 200 53.8% 26.5% + 3 NMP capture, static eval 16 41 200 52.3% 26.5% + 4 NMP capture -9 41 200 48.8% 28.5% + 5 No NMP -74 43 200 39.5% 23.0% */ /* TODO +add output per ply for perft search + big idea: remove all constant variables, and put them into the BotConfig struct, then write my own tuning / matchmaking program that will tweak the values @@ -118,7 +129,7 @@ fn main() { // UCI protocol "uci" => { - println!("id name Maxwell v3.0.8-1"); + println!("id name Maxwell v3.1.0"); println!("id author eboatwright"); println!("uciok"); From e43c914de1bc3d4b61b5f99e3a752c8e35bea750 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Thu, 11 Jan 2024 16:44:52 -0600 Subject: [PATCH 06/47] 1/11/24 4:44 PM Updated README Replaced get_base_worth_of_piece function with a const array Tested and implemented Killer moves and History Tested many other things including History Reductions Updated perft test to print out the number of positions after each move from the starting position --- README.md | 3 +- src/board.rs | 18 +++++----- src/bot.rs | 71 ++++++++++++++++++++++++++++++++------ src/main.rs | 40 ++------------------- src/move_sorter.rs | 36 ++++++++++--------- src/perft.rs | 21 +++++++---- src/piece_square_tables.rs | 21 +++++------ 7 files changed, 117 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index bf86d9c..66ceb55 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,11 @@ - Promotions - Checks -## Helpful Sources +## Helpful Sources & References - [Sebastian Lague's Chess Programming series](https://www.youtube.com/playlist?list=PLFt_AvWsXl0cvHyu32ajwh2qU1i6hl77c) - [The Chess Programming Wiki](https://www.chessprogramming.org/Main_Page) - [BBC Engine Development](https://www.youtube.com/playlist?list=PLmN0neTso3Jxh8ZIylk74JpwfiWNI76Cs) + - [Boychesser](https://github.com/analog-hors/Boychesser/) - [Lynx](https://github.com/lynx-chess/Lynx/) - [Weiawaga](https://github.com/Heiaha/Weiawaga/) - [Fruit 2.1](https://github.com/Warpten/Fruit-2.1/) diff --git a/src/board.rs b/src/board.rs index 158e874..4381f7b 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1,6 +1,6 @@ use crate::value_holder::ValueHolder; use crate::utils::{pop_lsb, get_lsb, print_bitboard, coordinate_to_index}; -use crate::piece_square_tables::{get_base_worth_of_piece, get_full_worth_of_piece, ROOK_WORTH, BISHOP_WORTH}; +use crate::piece_square_tables::{BASE_WORTHS_OF_PIECE_TYPE, get_full_worth_of_piece, ROOK_WORTH, BISHOP_WORTH}; use crate::precalculated_move_data::*; use crate::move_data::*; use crate::zobrist::Zobrist; @@ -91,7 +91,7 @@ impl Board { if piece_type != PAWN && piece_type != KING { - let piece_worth = get_base_worth_of_piece(piece); + let piece_worth = BASE_WORTHS_OF_PIECE_TYPE[piece_type]; board.total_material_without_pawns += piece_worth; } } @@ -253,15 +253,16 @@ impl Board { self.piece_bitboards[data.piece as usize] ^= 1 << data.to; } else { self.piece_bitboards[build_piece(piece_color == 1, data.flag as usize)] ^= 1 << data.to; - self.total_material_without_pawns += get_base_worth_of_piece(data.flag as usize); + self.total_material_without_pawns += BASE_WORTHS_OF_PIECE_TYPE[data.flag as usize]; } self.color_bitboards[piece_color] ^= 1 << data.from; self.color_bitboards[piece_color] ^= 1 << data.to; if data.capture != NO_PIECE as u8 { - if get_piece_type(data.capture as usize) != PAWN { - self.total_material_without_pawns -= get_base_worth_of_piece(data.capture as usize); + let capture_type = get_piece_type(data.capture as usize); + if capture_type != PAWN { + self.total_material_without_pawns -= BASE_WORTHS_OF_PIECE_TYPE[capture_type]; } if data.flag == EN_PASSANT_FLAG { @@ -365,15 +366,16 @@ impl Board { self.piece_bitboards[last_move.piece as usize] ^= 1 << last_move.to; } else { self.piece_bitboards[build_piece(piece_color == 1, last_move.flag as usize)] ^= 1 << last_move.to; - self.total_material_without_pawns -= get_base_worth_of_piece(last_move.flag as usize); + self.total_material_without_pawns -= BASE_WORTHS_OF_PIECE_TYPE[last_move.flag as usize]; } self.color_bitboards[piece_color] ^= 1 << last_move.from; self.color_bitboards[piece_color] ^= 1 << last_move.to; if last_move.capture != NO_PIECE as u8 { - if get_piece_type(last_move.capture as usize) != PAWN { - self.total_material_without_pawns += get_base_worth_of_piece(last_move.capture as usize); + let capture_type = get_piece_type(last_move.capture as usize); + if capture_type != PAWN { + self.total_material_without_pawns += BASE_WORTHS_OF_PIECE_TYPE[capture_type]; } if last_move.flag == EN_PASSANT_FLAG { diff --git a/src/bot.rs b/src/bot.rs index 9702616..157739c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,3 +1,52 @@ +/* RESULTS +Aspiration Window: + Try this again when I have more features added back, and also try the more popular approach: + To drop the entire window if it falls out of bounds, and also with a smaller window + Rank Name Elo +/- Games Score Draw + 1 No window 31 63 100 54.5% 17.0% + 2 Hold window 0 65 100 50.0% 12.0% + 3 Reset window -31 62 100 45.5% 19.0% + +Score of PVS vs No PVS: 27 - 13 - 10 + +Score of PVS & LMR vs PVS: 32 - 10 - 8 + +Extensions: + Rank Name Elo +/- Games Score Draw + 1 All extensions 21 50 150 53.0% 20.7% + 2 Promotion extension -5 50 150 49.3% 21.3% + 3 No extensions -7 50 150 49.0% 20.7% + 4 Check extension -9 49 150 48.7% 22.7% + + +Null Move Pruning: + Rank Name Elo +/- Games Score Draw + 1 NMP static eval 40 42 200 55.8% 25.5% + 2 NMP 26 41 200 53.8% 26.5% + 3 NMP capture, static eval 16 41 200 52.3% 26.5% + 4 NMP capture -9 41 200 48.8% 28.5% + 5 No NMP -74 43 200 39.5% 23.0% + +Score of Order-Hash move, MVV-LVA vs Order-Hash move: 41 - 3 - 6 + + +History Heuristic & Killer Moves + Rank Name Elo +/- Games Score Draw + 1 Killer moves, history 49 48 150 57.0% 26.0% + 2 History 44 48 150 56.3% 26.0% + 3 Killer moves 23 48 150 53.3% 25.3% + 4 Current -120 49 150 33.3% 28.0% + +History Reductions: + Idk there's something about this technique that feels off, I think I'm gonna comment it out for now + + (With a value of 800 is lost horribly, and with a value of 3000 it did worse than 1600) + Score of History reductions (1600) vs Current: 22 - 14 - 14 + Score of History reductions (2200) vs Current: 23 - 12 - 15 +*/ + + +use crate::piece_square_tables::BASE_WORTHS_OF_PIECE_TYPE; use crate::STARTING_FEN; use crate::piece_square_tables::{PAWN_WORTH, QUEEN_WORTH}; use crate::pieces::{PAWN, PROMOTABLE, NO_PIECE}; @@ -20,7 +69,7 @@ pub const RFP_THESHOLD_PER_PLY: i32 = 60; // TODO pub const MAX_DEPTH_LEFT_FOR_RAZORING: u8 = 3; // TODO pub const RAZORING_THRESHOLD_PER_PLY: i32 = 300; // TODO -pub const HISTORY_THRESHOLD: i32 = 1000; // TODO +pub const HISTORY_THRESHOLD: i32 = 2200; // TODO pub const PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES: f32 = 0.025; // 2.5% pub const PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES: f32 = 0.07; // 7% @@ -252,7 +301,6 @@ impl Bot { } if let Some(data) = self.transposition_table.lookup(board.zobrist.key, depth_left, depth, alpha, beta) { - // TODO: Can I produce a cutoff from here? self.positions_searched -= 1; self.transposition_hits += 1; @@ -267,6 +315,7 @@ impl Bot { let not_pv = alpha == beta - 1; + // TODO: && !evaluation_is_mate(alpha)? if not_pv && depth > 0 && depth_left > 0 { // && board.get_last_move().capture == NO_PIECE as u8 This made it play worse @@ -370,9 +419,10 @@ impl Bot { && !board.king_in_check(board.white_to_move) { reduction += 1; - // TODO + // History Reductions // if depth_left > 3 { // let history_value = self.move_sorter.history[m.piece as usize][m.to as usize]; + // // 1 + (HISTORY_THRESHOLD - history_value) / HISTORY_THRESHOLD_PER_PLY_REDUCTION for reduction? // reduction += if history_value < HISTORY_THRESHOLD { 1 } else { 0 }; // } } @@ -398,10 +448,10 @@ impl Bot { if evaluation >= beta { self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); - // if m.capture == NO_PIECE as u8 { - // self.move_sorter.push_killer_move(m, depth); - // self.move_sorter.history[m.piece as usize][m.to as usize] += (depth_left * depth_left) as i32; - // } + if m.capture == NO_PIECE as u8 { + self.move_sorter.push_killer_move(m, depth); + self.move_sorter.history[m.piece as usize][m.to as usize] += (depth_left * depth_left) as i32; + } return beta; } @@ -420,11 +470,9 @@ impl Bot { } } - // if !not_pv { // lol /* I don't need to check for a null move here, because the first move in the list will always be the best move so far */ self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); - // } alpha } @@ -456,10 +504,11 @@ impl Bot { for m in sorted_moves { // TODO // Delta Pruning - // if !board.king_in_check(board.white_to_move) { + // if board.total_material_without_pawns > 0 // ? + // && !board.king_in_check(board.white_to_move) { // let mut threshold = QUEEN_WORTH; // if PROMOTABLE.contains(&m.flag) { - // threshold += QUEEN_WORTH - PAWN_WORTH; + // threshold += BASE_WORTHS_OF_PIECE_TYPE[m.flag as usize] - PAWN_WORTH; // } // if evaluation < alpha - threshold { diff --git a/src/main.rs b/src/main.rs index 56edc36..108ebfa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,38 +1,4 @@ -/* RESULTS -aspiration window: - Try this again when I have more features added back, and also try the more popular approach: - To drop the entire window if it falls out of bounds, and also with a smaller window - Rank Name Elo +/- Games Score Draw - 1 No window 31 63 100 54.5% 17.0% - 2 Hold window 0 65 100 50.0% 12.0% - 3 Reset window -31 62 100 45.5% 19.0% - -Score of PVS vs No PVS: 27 - 13 - 10 - -Score of PVS & LMR vs PVS: 32 - 10 - 8 - -Extensions: - Rank Name Elo +/- Games Score Draw - 1 All extensions 21 50 150 53.0% 20.7% - 2 Promotion extension -5 50 150 49.3% 21.3% - 3 No extensions -7 50 150 49.0% 20.7% - 4 Check extension -9 49 150 48.7% 22.7% - - -Null Move Pruning: - Rank Name Elo +/- Games Score Draw - 1 NMP static eval 40 42 200 55.8% 25.5% - 2 NMP 26 41 200 53.8% 26.5% - 3 NMP capture, static eval 16 41 200 52.3% 26.5% - 4 NMP capture -9 41 200 48.8% 28.5% - 5 No NMP -74 43 200 39.5% 23.0% -*/ - - - /* TODO -add output per ply for perft search - big idea: remove all constant variables, and put them into the BotConfig struct, then write my own tuning / matchmaking program that will tweak the values @@ -48,11 +14,11 @@ https://www.chessprogramming.org/Futility_Pruning https://www.chessprogramming.org/Internal_Iterative_Deepening Random ideas to try (from other engines and chessprogramming.org) -History reduction +https://www.chessprogramming.org/Static_Exchange_Evaluation https://www.chessprogramming.org/History_Leaf_Pruning https://www.chessprogramming.org/Futility_Pruning#MoveCountBasedPruning https://www.chessprogramming.org/Triangular_PV-Table -https://www.chessprogramming.org/Static_Exchange_Evaluation +https://www.chessprogramming.org/Countermove_Heuristic Some random resources I found: (Not using them right now but they could be useful) https://analog-hors.github.io/site/magic-bitboards/ @@ -234,7 +200,7 @@ fn main() { } "perft" => { - let depth = command_split[1].parse::().expect("Invalid depth"); + let depth = command_split[1].parse::().expect("Invalid depth"); PerftResults::calculate(&mut board, depth); } diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 2711523..3e4fd74 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -1,4 +1,4 @@ -use crate::piece_square_tables::get_base_worth_of_piece; +use crate::move_data::EN_PASSANT_FLAG; use crate::killer_moves::KillerMoves; use crate::move_data::{MoveData, NULL_MOVE, SHORT_CASTLE_FLAG, LONG_CASTLE_FLAG}; use crate::pieces::*; @@ -19,6 +19,7 @@ pub const MVV_LVA: [i32; 36] = [ pub struct MoveSorter { pub killer_moves: [KillerMoves; MAX_KILLER_MOVE_PLY], pub history: [[i32; 64]; PIECE_COUNT], + // TODO: Countermoves? } impl MoveSorter { @@ -51,7 +52,6 @@ impl MoveSorter { // board.calculate_attacked_squares(); // board.calculate_attacked_squares_for_color((!board.white_to_move) as usize); - // TODO: test both of these // let squares_i_attack = board.attacked_squares_bitboards[board.white_to_move as usize]; // let squares_opponent_attacks = board.attacked_squares_bitboards[!board.white_to_move as usize]; @@ -64,33 +64,35 @@ impl MoveSorter { score = i32::MAX; } else { if m.capture != NO_PIECE as u8 { - // score += (5 * get_full_worth_of_piece(m.capture as usize, m.to as usize, endgame) - get_full_worth_of_piece(m.piece as usize, m.from as usize, endgame)) + 8000; score += MVV_LVA[get_piece_type(m.piece as usize) * 6 + get_piece_type(m.capture as usize)] + 8000; + + // TODO: static exchange evaluation } else { - // if depth < MAX_KILLER_MOVE_PLY as u8 - // && self.killer_moves[depth as usize].is_killer(m) { - // score += 5000; - // } + if depth < MAX_KILLER_MOVE_PLY as u8 + && self.killer_moves[depth as usize].is_killer(m) { + score += 5000; + } - // score += self.history[m.piece as usize][m.to as usize]; + score += self.history[m.piece as usize][m.to as usize]; } - if m.flag == SHORT_CASTLE_FLAG - || m.flag == LONG_CASTLE_FLAG { - score += 2000; - } + // TODO + // if m.flag == SHORT_CASTLE_FLAG + // || m.flag == LONG_CASTLE_FLAG { + // score += 2000; + // } else if PROMOTABLE.contains(&m.flag) { // TODO + // score += get_base_worth_of_piece(m.flag) + 12000; + // } + // TODO // if squares_i_attack & (1 << m.to) != 0 { - // score += get_full_worth_of_piece(m.piece as usize, m.to as usize, endgame); + // score += get_base_worth_of_piece(m.piece as usize); // } + // TODO // if squares_opponent_attacks & (1 << m.to) != 0 { // score -= 2 * get_base_worth_of_piece(m.piece as usize); // } - - if PROMOTABLE.contains(&m.flag) { - score += get_base_worth_of_piece(build_piece(is_piece_white(m.piece as usize), m.flag as usize)) + 12000; - } } scores[i] = (score, i); diff --git a/src/perft.rs b/src/perft.rs index 66b30f5..f1ddb09 100644 --- a/src/perft.rs +++ b/src/perft.rs @@ -6,7 +6,7 @@ use crate::pieces::PROMOTABLE; #[derive(Default, Debug)] pub struct PerftResults { - pub depth: usize, + pub depth: u8, pub positions: u128, pub captures: u128, @@ -18,32 +18,34 @@ pub struct PerftResults { } impl PerftResults { - pub fn new(depth: usize) -> Self { + pub fn new(depth: u8) -> Self { Self { depth, ..Default::default() } } - pub fn calculate(board: &mut Board, depth: usize) { + pub fn calculate(board: &mut Board, depth: u8) { let mut results = PerftResults::new(depth); let timer = Instant::now(); let depth = results.depth; - perft(board, &mut results, depth); + perft(board, &mut results, depth, 0); - println!("{} seconds", timer.elapsed().as_secs_f32()); + println!("\n{} seconds\n", timer.elapsed().as_secs_f32()); println!("{:#?}", results); } } -fn perft(board: &mut Board, results: &mut PerftResults, depth_left: usize) { +fn perft(board: &mut Board, results: &mut PerftResults, depth_left: u8, depth: u8) { if depth_left == 0 { results.positions += 1; return; } for data in board.get_legal_moves_for_color(board.white_to_move, false) { + let position_count_before_move = results.positions; + if data.capture != NO_PIECE as u8 { results.captures += 1; @@ -65,7 +67,12 @@ fn perft(board: &mut Board, results: &mut PerftResults, depth_left: usize) { results.checks += 1; } - perft(board, results, depth_left - 1); + perft(board, results, depth_left - 1, depth + 1); board.undo_last_move(); + + if depth == 0 { + let positions_this_move = results.positions - position_count_before_move; + println!("{}: {}", data.to_coordinates(), positions_this_move); + } } } \ No newline at end of file diff --git a/src/piece_square_tables.rs b/src/piece_square_tables.rs index 7b647e4..ef733ba 100644 --- a/src/piece_square_tables.rs +++ b/src/piece_square_tables.rs @@ -97,6 +97,15 @@ pub const ROOK_WORTH: i32 = 500; pub const QUEEN_WORTH: i32 = 900; pub const KING_WORTH: i32 = 0; // chessprogramming.org says this should be 20k but I don't think it matters /\o/\ +pub const BASE_WORTHS_OF_PIECE_TYPE: [i32; 6] = [ + PAWN_WORTH, + KNIGHT_WORTH, + BISHOP_WORTH, + ROOK_WORTH, + QUEEN_WORTH, + KING_WORTH, +]; + pub fn get_full_worth_of_piece(piece: usize, mut i: usize, endgame: f32) -> i32 { if !is_piece_white(piece) { i = flip_index(i); @@ -111,18 +120,6 @@ pub fn get_full_worth_of_piece(piece: usize, mut i: usize, endgame: f32) -> i32 KING => (MIDDLEGAME_KING_TABLE[i] as f32 * (1.0 - endgame) + ENDGAME_KING_TABLE[i] as f32 * endgame) as i32, - _ => 0, - } -} - -pub fn get_base_worth_of_piece(piece: usize) -> i32 { - match get_piece_type(piece) { - PAWN => PAWN_WORTH, - KNIGHT => KNIGHT_WORTH, - BISHOP => BISHOP_WORTH, - ROOK => ROOK_WORTH, - QUEEN => QUEEN_WORTH, - _ => 0, } } \ No newline at end of file From 5d1e55917274bc55da5f5119fa69cd2a306da56c Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Fri, 12 Jan 2024 18:10:51 -0600 Subject: [PATCH 07/47] 1/12/24 6:10 PM Updated README Tested alot of move ordering Tested and re-implemented razoring --- README.md | 30 +++++++++++------------- src/board.rs | 17 +++++++++++--- src/bot.rs | 47 ++++++++++++++++++++++++-------------- src/move_sorter.rs | 28 ++++++++--------------- src/piece_square_tables.rs | 2 +- src/pieces.rs | 9 ++++++++ 6 files changed, 76 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index 66ceb55..6b91232 100644 --- a/README.md +++ b/README.md @@ -30,30 +30,26 @@ #### Move Ordering - Hash move / best move from previous iteration - MVV-LVA - - 2 Killer moves - - History heuristic - - Castling - - Promotions + - 2 Killer Moves + - History Heuristic - Penalty for moving a piece to a square an opponent's piece attacks #### Search + - Principal Variation Search - Iterative deepening - - Aspiration windows - - Starts at 40 and multiplies by 4 if out of alpha beta bounds - - Time management - - If less than 7 moves have been played, it uses 2.5% of it's remaining time, otherwise 7% - - This value is then also clamped between 0.25 and 20.0 seconds - - Exits search if a mate is found within search depth - - Alpha beta pruning - - Quiescence search with Delta Pruning - - Transposition table + - Alpha-Beta Pruning + - Quiescence Search with Delta Pruning + - Transposition Table - No set max size, but entries get removed after 10 moves without hits - - Null move pruning + - Null Move Pruning - Razoring - - Reverse futility pruning - - Late move reduction - - Search extensions + - Reverse Futility Pruning + - Late Move Reductions + - Search Extensions - Promotions - Checks + - Time management + - If less than 7 moves have been played, it uses 2.5% of it's remaining time, otherwise 7% + - This value is then also clamped between 0.25 and 20.0 seconds ## Helpful Sources & References - [Sebastian Lague's Chess Programming series](https://www.youtube.com/playlist?list=PLFt_AvWsXl0cvHyu32ajwh2qU1i6hl77c) diff --git a/src/board.rs b/src/board.rs index 4381f7b..aa4cf7f 100644 --- a/src/board.rs +++ b/src/board.rs @@ -113,6 +113,11 @@ impl Board { self.calculate_attacked_squares_for_color(1); } + pub fn get_attacked_squares_for_color(&mut self, color: usize) -> u64 { + self.calculate_attacked_squares_for_color(color); + self.attacked_squares_bitboards[color] + } + // This is SLOOOOOOOOOOOOOWWWWWWW :[ pub fn calculate_attacked_squares_for_color(&mut self, color: usize) { if self.attacked_squares_calculated[color] { @@ -173,7 +178,7 @@ impl Board { println!("{}", output); } - pub fn print_bitboards(&self) { + pub fn print_bitboards(&mut self) { for piece in 0..BITBOARD_COUNT { let c = piece_to_char(piece); print_bitboard( @@ -189,6 +194,8 @@ impl Board { print_bitboard("Black pieces", "1".bold().italic().white().on_black(), self.color_bitboards[0]); print_bitboard("White pieces", "1".bold().italic().normal().on_white(), self.color_bitboards[1]); + + self.calculate_attacked_squares(); print_bitboard("Black attacked squares", "1".bold().italic().white().on_black(), self.attacked_squares_bitboards[0]); print_bitboard("White attacked squares", "1".bold().italic().normal().on_white(), self.attacked_squares_bitboards[1]); } @@ -436,8 +443,8 @@ impl Board { } pub fn king_in_check(&mut self, king_is_white: bool) -> bool { - self.calculate_attacked_squares_for_color((!king_is_white) as usize); - self.piece_bitboards[build_piece(king_is_white, KING)] & self.attacked_squares_bitboards[(!king_is_white) as usize] != 0 + let attacked_squares = self.get_attacked_squares_for_color((!king_is_white) as usize); + self.piece_bitboards[build_piece(king_is_white, KING)] & attacked_squares != 0 } pub fn get_legal_moves_for_color(&mut self, white_pieces: bool, only_captures: bool) -> Vec { @@ -968,6 +975,10 @@ impl Board { // let weak_lines_from_white_king = (self.calculate_queen_attack_bitboard(white_king_index).count_ones() as f32 * (1.0 - endgame)) as i32; // let weak_lines_from_black_king = (self.calculate_queen_attack_bitboard(black_king_index).count_ones() as f32 * (1.0 - endgame)) as i32; + /* TODO + a small boost for having the bishop pair? + */ + ((white_material + white_attacked_squares * 10 - weak_squares_around_white_king * 20 + white_pawn_evaluation) - (black_material + black_attacked_squares * 10 - weak_squares_around_black_king * 20 + black_pawn_evaluation)) * self.perspective() } diff --git a/src/bot.rs b/src/bot.rs index 157739c..bc8563d 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -18,21 +18,19 @@ Extensions: 3 No extensions -7 50 150 49.0% 20.7% 4 Check extension -9 49 150 48.7% 22.7% - Null Move Pruning: Rank Name Elo +/- Games Score Draw 1 NMP static eval 40 42 200 55.8% 25.5% 2 NMP 26 41 200 53.8% 26.5% - 3 NMP capture, static eval 16 41 200 52.3% 26.5% + 3 NMP capture, static eval 16 41 200 52.3% 26.5% 4 NMP capture -9 41 200 48.8% 28.5% 5 No NMP -74 43 200 39.5% 23.0% Score of Order-Hash move, MVV-LVA vs Order-Hash move: 41 - 3 - 6 - History Heuristic & Killer Moves Rank Name Elo +/- Games Score Draw - 1 Killer moves, history 49 48 150 57.0% 26.0% + 1 Killer moves, history 49 48 150 57.0% 26.0% 2 History 44 48 150 56.3% 26.0% 3 Killer moves 23 48 150 53.3% 25.3% 4 Current -120 49 150 33.3% 28.0% @@ -43,6 +41,18 @@ History Reductions: (With a value of 800 is lost horribly, and with a value of 3000 it did worse than 1600) Score of History reductions (1600) vs Current: 22 - 14 - 14 Score of History reductions (2200) vs Current: 23 - 12 - 15 + +Razoring: + 200 and 400 made it worse + TODO: try 280 and 320 + Score of Razoring (300) vs Current: 23 - 11 - 16 + +Move Ordering tests: + Rank Name Elo +/- Games Score Draw + 1 Order attacked square penalty 56 47 150 58.0% 30.7% + 2 Current 0 47 150 50.0% 29.3% + 3 Order protected square boost -23 47 150 46.7% 29.3% + 4 Order both -33 48 150 45.3% 28.0% */ @@ -61,15 +71,15 @@ use crate::Board; pub const MAX_SEARCH_EXTENSIONS: u8 = 16; // TODO pub const ASPIRATION_WINDOW: i32 = 25; // TODO -pub const MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE: u8 = 3; // TODO +pub const MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE: u8 = 3; pub const MAX_DEPTH_LEFT_FOR_RFP: u8 = 4; // TODO pub const RFP_THESHOLD_PER_PLY: i32 = 60; // TODO -pub const MAX_DEPTH_LEFT_FOR_RAZORING: u8 = 3; // TODO -pub const RAZORING_THRESHOLD_PER_PLY: i32 = 300; // TODO +pub const MAX_DEPTH_LEFT_FOR_RAZORING: u8 = 3; +pub const RAZORING_THRESHOLD_PER_PLY: i32 = 300; -pub const HISTORY_THRESHOLD: i32 = 2200; // TODO +pub const HISTORY_THRESHOLD: i32 = 2200; pub const PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES: f32 = 0.025; // 2.5% pub const PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES: f32 = 0.07; // 7% @@ -218,7 +228,11 @@ impl Bot { self.evaluation_this_iteration = 0; - // Aspiration Window + /* Aspiration Window + I think Aspiration Windows aren't working for me because when it searches with a smaller window, + the data will then get put into the transposition table, and then if it has to re-search, instead + of seeing the moves that it missed, it'll just grab the faulty values from the transposition table + */ // let mut evaluation = self.alpha_beta_search(board, 0, depth, alpha, beta); // if evaluation <= alpha || evaluation >= beta { @@ -280,7 +294,7 @@ impl Bot { &mut self, board: &mut Board, depth: u8, - depth_left: u8, + mut depth_left: u8, mut alpha: i32, beta: i32, total_extensions: u8, @@ -343,12 +357,11 @@ impl Bot { // return static_eval; // } - // TODO // Razoring - // if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING - // && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { - // depth_left -= 1; - // } + if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING + && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { + depth_left -= 1; + } // TODO /* Internal Iterative Reductions @@ -393,7 +406,7 @@ impl Bot { let mut extension = 0; if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { - if board.king_in_check(board.white_to_move) { + if board.king_in_check(board.white_to_move) { // TODO: or if the flag is a promotion? extension = 1; } else if m.piece == PAWN as u8 { let rank = m.to / 8; @@ -446,7 +459,7 @@ impl Bot { } if evaluation >= beta { - self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); + self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); // TODO: Store the aspiration window? if m.capture == NO_PIECE as u8 { self.move_sorter.push_killer_move(m, depth); diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 3e4fd74..91d72c3 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -1,8 +1,8 @@ use crate::move_data::EN_PASSANT_FLAG; use crate::killer_moves::KillerMoves; -use crate::move_data::{MoveData, NULL_MOVE, SHORT_CASTLE_FLAG, LONG_CASTLE_FLAG}; +use crate::move_data::{MoveData, NULL_MOVE}; use crate::pieces::*; -use crate::piece_square_tables::get_full_worth_of_piece; +use crate::piece_square_tables::BASE_WORTHS_OF_PIECE_TYPE; use crate::Board; pub const MAX_KILLER_MOVE_PLY: usize = 32; @@ -49,11 +49,7 @@ impl MoveSorter { let num_of_moves = moves.len(); let mut scores = vec![(0, 0); num_of_moves]; - // board.calculate_attacked_squares(); - // board.calculate_attacked_squares_for_color((!board.white_to_move) as usize); - - // let squares_i_attack = board.attacked_squares_bitboards[board.white_to_move as usize]; - // let squares_opponent_attacks = board.attacked_squares_bitboards[!board.white_to_move as usize]; + let squares_opponent_attacks = board.get_attacked_squares_for_color((!board.white_to_move) as usize); for i in 0..num_of_moves { let m = moves[i]; @@ -76,23 +72,17 @@ impl MoveSorter { score += self.history[m.piece as usize][m.to as usize]; } - // TODO + // This made it worse // if m.flag == SHORT_CASTLE_FLAG // || m.flag == LONG_CASTLE_FLAG { // score += 2000; - // } else if PROMOTABLE.contains(&m.flag) { // TODO - // score += get_base_worth_of_piece(m.flag) + 12000; - // } - - // TODO - // if squares_i_attack & (1 << m.to) != 0 { - // score += get_base_worth_of_piece(m.piece as usize); + // } else if PROMOTABLE.contains(&m.flag) { + // score += BASE_WORTHS_OF_PIECE_TYPE[m.flag as usize] + 12000; // } - // TODO - // if squares_opponent_attacks & (1 << m.to) != 0 { - // score -= 2 * get_base_worth_of_piece(m.piece as usize); - // } + if squares_opponent_attacks & (1 << m.to) != 0 { + score -= 2 * BASE_WORTHS_OF_PIECE_TYPE[get_piece_type(m.piece as usize)]; + } } scores[i] = (score, i); diff --git a/src/piece_square_tables.rs b/src/piece_square_tables.rs index ef733ba..0d55840 100644 --- a/src/piece_square_tables.rs +++ b/src/piece_square_tables.rs @@ -91,7 +91,7 @@ pub const ENDGAME_KING_TABLE: [i32; 64] = [ pub fn flip_index(i: usize) -> usize { i ^ 56 } pub const PAWN_WORTH: i32 = 100; -pub const KNIGHT_WORTH: i32 = 320; +pub const KNIGHT_WORTH: i32 = 320; // Maybe increase knight and bishop worth to discourage trading for rook + pawn? pub const BISHOP_WORTH: i32 = 330; pub const ROOK_WORTH: i32 = 500; pub const QUEEN_WORTH: i32 = 900; diff --git a/src/pieces.rs b/src/pieces.rs index 6eb2559..0a63cba 100644 --- a/src/pieces.rs +++ b/src/pieces.rs @@ -29,6 +29,15 @@ pub const PROMOTABLE: [u8; 4] = [ QUEEN as u8, ]; +// pub fn flag_is_promotion(flag: u8) -> bool { +// let flag = flag as usize; + +// flag == KNIGHT +// || flag == BISHOP +// || flag == ROOK +// || flag == QUEEN +// } + pub fn is_piece_white(piece: usize) -> bool { piece > BLACK_KING } From 78413253f0b97c234a41bd0aa3ddb8070db2610d Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sat, 13 Jan 2024 14:20:28 -0600 Subject: [PATCH 08/47] 1/13/24 2:20 PM Tested and scrapped Internal Iterative Reductions Tested and implemented Delta Pruning Lowered pawn endgame PST values (+16 wins XD) --- src/bot.rs | 63 +++++++++++++++++++++----------------- src/piece_square_tables.rs | 21 +++++++------ 2 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index bc8563d..ba10e75 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -2,6 +2,10 @@ Aspiration Window: Try this again when I have more features added back, and also try the more popular approach: To drop the entire window if it falls out of bounds, and also with a smaller window + + This goes deeper than I thought, I'm gonna have to get some help with this, because apparently + PVS + TT + AW = alot of problems + Rank Name Elo +/- Games Score Draw 1 No window 31 63 100 54.5% 17.0% 2 Hold window 0 65 100 50.0% 12.0% @@ -53,6 +57,21 @@ Move Ordering tests: 2 Current 0 47 150 50.0% 29.3% 3 Order protected square boost -23 47 150 46.7% 29.3% 4 Order both -33 48 150 45.3% 28.0% + +Reverse Futility Pruning: +Rank Name Elo +/- Games Score Draw + 1 RFP after razor 115 55 100 66.0% 38.0% + 2 RFP before razor 45 56 100 56.5% 35.0% + 3 Current -168 59 100 27.5% 33.0% + +Delta Pruning +Rank Name Elo +/- Games Score Draw + 1 Delta pruning 28 54 100 54.0% 38.0% + 2 Delta pruning with material check 7 57 100 51.0% 30.0% + 3 Current -35 58 100 45.0% 30.0% + +I lowered the values of the pawn endgame PST, and this was the result XD +Score of Lower pawn eval vs Test-Current: 25 - 9 - 16 */ @@ -350,27 +369,17 @@ impl Bot { } } - // TODO - // Reverse Futility Pruning - // if depth_left <= MAX_DEPTH_LEFT_FOR_RFP - // && static_eval - RFP_THESHOLD_PER_PLY * (depth_left as i32) >= beta { - // return static_eval; - // } - // Razoring if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { depth_left -= 1; } - // TODO - /* Internal Iterative Reductions - I don't need to test if there's a hash move here, because if there was - it would have already exited this search - */ - // if depth_left > 3 { - // depth_left -= 1; - // } + // Reverse Futility Pruning + if depth_left <= MAX_DEPTH_LEFT_FOR_RFP + && static_eval - RFP_THESHOLD_PER_PLY * (depth_left as i32) >= beta { + return static_eval; + } } if depth_left == 0 { @@ -459,7 +468,7 @@ impl Bot { } if evaluation >= beta { - self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); // TODO: Store the aspiration window? + self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); if m.capture == NO_PIECE as u8 { self.move_sorter.push_killer_move(m, depth); @@ -515,19 +524,17 @@ impl Bot { let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, u8::MAX); for m in sorted_moves { - // TODO // Delta Pruning - // if board.total_material_without_pawns > 0 // ? - // && !board.king_in_check(board.white_to_move) { - // let mut threshold = QUEEN_WORTH; - // if PROMOTABLE.contains(&m.flag) { - // threshold += BASE_WORTHS_OF_PIECE_TYPE[m.flag as usize] - PAWN_WORTH; - // } - - // if evaluation < alpha - threshold { - // continue; - // } - // } + if !board.king_in_check(board.white_to_move) { // && board.total_material_without_pawns > 0 made it worse + let mut threshold = QUEEN_WORTH; + if PROMOTABLE.contains(&m.flag) { + threshold += BASE_WORTHS_OF_PIECE_TYPE[m.flag as usize] - PAWN_WORTH; + } + + if evaluation < alpha - threshold { + continue; + } + } board.make_move(m); let evaluation = -self.quiescence_search(board, -beta, -alpha); diff --git a/src/piece_square_tables.rs b/src/piece_square_tables.rs index 0d55840..56f9331 100644 --- a/src/piece_square_tables.rs +++ b/src/piece_square_tables.rs @@ -11,15 +11,16 @@ pub const MIDDLEGAME_PAWN_TABLE: [i32; 64] = [ 0, 0, 0, 0, 0, 0, 0, 0, ]; -pub const ENDGAME_PAWN_TABLE: [i32; 64] = [ // TODO: tweak this (maybe lower the values?) - 0, 0, 0, 0, 0, 0, 0, 0, - 100,100,100,100,100,100,100,100, - 70, 70, 70, 70, 70, 70, 70, 70, - 50, 50, 50, 50, 50, 50, 50, 50, - 30, 30, 30, 30, 30, 30, 30, 30, - 15, 15, 15, 15, 15, 15, 15, 15, - 15, 15, 15, 15, 15, 15, 15, 15, - 0, 0, 0, 0, 0, 0, 0, 0, +// TODO: tweak these further +pub const ENDGAME_PAWN_TABLE: [i32; 64] = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 80, 80, 80, 80, 80, 80, 80, 80, + 55, 55, 55, 55, 55, 55, 55, 55, + 35, 35, 35, 35, 35, 35, 35, 35, + 20, 20, 20, 20, 20, 20, 20, 20, + 10, 10, 10, 10, 10, 10, 10, 10, + 10, 10, 10, 10, 10, 10, 10, 10, + 0, 0, 0, 0, 0, 0, 0, 0, ]; pub const KNIGHT_TABLE: [i32; 64] = [ @@ -91,7 +92,7 @@ pub const ENDGAME_KING_TABLE: [i32; 64] = [ pub fn flip_index(i: usize) -> usize { i ^ 56 } pub const PAWN_WORTH: i32 = 100; -pub const KNIGHT_WORTH: i32 = 320; // Maybe increase knight and bishop worth to discourage trading for rook + pawn? +pub const KNIGHT_WORTH: i32 = 320; // TODO: Maybe increase knight and bishop worth to discourage trading for rook + pawn? pub const BISHOP_WORTH: i32 = 330; pub const ROOK_WORTH: i32 = 500; pub const QUEEN_WORTH: i32 = 900; From 176e618bcea28eb1af5c44c3d4365de0b5f83aa5 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sun, 14 Jan 2024 11:31:24 -0600 Subject: [PATCH 09/47] 1/14/24 11:31 AM Added TODOs everywhere Tested Aspiration Windows again, still not really helping and causing some major instability Tested increasing Knight and Bishop worth (didn't help) --- README.md | 5 +++++ src/board.rs | 7 +++---- src/bot.rs | 42 +++++++++++++++++++++++--------------- src/piece_square_tables.rs | 2 +- 4 files changed, 35 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 6b91232..01614a5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ ![](/icon/Maxwell_316x316.png) # Maxwell Chess Engine + CCRL Blitz ratings
+ | Date | Version | Rating | Rank | + | --- | --- | + | 1/14/24 | v3.0.8-1 | 1830 | 538 / 715 | + A Chess engine written from scratch in Rust!
If you use this code verbatim, or as a reference, please credit me!

[Play against Maxwell on Lichess!](https://lichess.org/@/MaxwellOnLC) | [Some of Maxwell's Games](https://www.chess.com/library/collections/maxwells-games-my-chess-engine-2FFU82NM4) diff --git a/src/board.rs b/src/board.rs index aa4cf7f..adf4729 100644 --- a/src/board.rs +++ b/src/board.rs @@ -960,7 +960,8 @@ impl Board { let white_king_index = get_lsb(self.piece_bitboards[WHITE_KING]) as usize; let black_king_index = get_lsb(self.piece_bitboards[BLACK_KING]) as usize; - // TODO: weak squares, weak lines, or none? + // TODO: weak squares, weak lines, or none? + // TODO: Or count how many friendly pieces are around the king? let weak_squares_around_white_king = (( self.precalculated_move_data.king_attacks[white_king_index] @@ -975,9 +976,7 @@ impl Board { // let weak_lines_from_white_king = (self.calculate_queen_attack_bitboard(white_king_index).count_ones() as f32 * (1.0 - endgame)) as i32; // let weak_lines_from_black_king = (self.calculate_queen_attack_bitboard(black_king_index).count_ones() as f32 * (1.0 - endgame)) as i32; - /* TODO - a small boost for having the bishop pair? - */ + // TODO: a small boost for having the bishop pair? ((white_material + white_attacked_squares * 10 - weak_squares_around_white_king * 20 + white_pawn_evaluation) - (black_material + black_attacked_squares * 10 - weak_squares_around_black_king * 20 + black_pawn_evaluation)) * self.perspective() diff --git a/src/bot.rs b/src/bot.rs index ba10e75..5eba6c9 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,10 +1,7 @@ /* RESULTS Aspiration Window: - Try this again when I have more features added back, and also try the more popular approach: - To drop the entire window if it falls out of bounds, and also with a smaller window - - This goes deeper than I thought, I'm gonna have to get some help with this, because apparently - PVS + TT + AW = alot of problems + The problems go deeper than I thought, I'm gonna have to get some help with this, + because apparently PVS + TT + AW = alot of problems Rank Name Elo +/- Games Score Draw 1 No window 31 63 100 54.5% 17.0% @@ -72,6 +69,17 @@ Rank Name Elo +/- Games Score Draw I lowered the values of the pawn endgame PST, and this was the result XD Score of Lower pawn eval vs Test-Current: 25 - 9 - 16 + + +First match vs v3.0.8: +Score of Maxwell v3.Latest vs Maxwell v3.0.8: 14 - 25 - 11 +sadge + + +20 did no better +Score of Aspiration window (30) vs Current: 17 - 15 - 18 +Score of Aspiration window (30) vs Current: 19 - 18 - 13 +Score of Aspiration window (40) vs Current: 15 - 15 - 20 */ @@ -88,7 +96,7 @@ use crate::opening_book::OpeningBook; use crate::Board; pub const MAX_SEARCH_EXTENSIONS: u8 = 16; // TODO -pub const ASPIRATION_WINDOW: i32 = 25; // TODO +pub const ASPIRATION_WINDOW: i32 = 30; // TODO pub const MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE: u8 = 3; @@ -247,15 +255,11 @@ impl Bot { self.evaluation_this_iteration = 0; - /* Aspiration Window - I think Aspiration Windows aren't working for me because when it searches with a smaller window, - the data will then get put into the transposition table, and then if it has to re-search, instead - of seeing the moves that it missed, it'll just grab the faulty values from the transposition table - */ - // let mut evaluation = self.alpha_beta_search(board, 0, depth, alpha, beta); + // Aspiration Window: not really working for this engine, idk if it's worth the search instability + // let mut evaluation = self.alpha_beta_search(board, 0, depth, alpha, beta, 0); // if evaluation <= alpha || evaluation >= beta { - // evaluation = self.alpha_beta_search(board, 0, depth, -i32::MAX, i32::MAX); + // evaluation = self.alpha_beta_search(board, 0, depth, -i32::MAX, i32::MAX, 0); // } // alpha = evaluation - ASPIRATION_WINDOW; @@ -432,13 +436,14 @@ impl Bot { if i > 0 { let mut reduction = 0; + // Reductions // Late Move Reduction if i > 3 && depth_left > 2 && depth > 0 && extension == 0 - && m.capture == NO_PIECE as u8 - && !board.king_in_check(board.white_to_move) { + && m.capture == NO_PIECE as u8 { // don't need to check for checks here because that's already an extension + // reduction += 1 + (depth_left - 2) / 2; reduction += 1; // History Reductions @@ -447,10 +452,15 @@ impl Bot { // // 1 + (HISTORY_THRESHOLD - history_value) / HISTORY_THRESHOLD_PER_PLY_REDUCTION for reduction? // reduction += if history_value < HISTORY_THRESHOLD { 1 } else { 0 }; // } + + // evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 2, -alpha - 1, -alpha, total_extensions); + // needs_full_search = evaluation > alpha; } + // if needs_full_search { evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, total_extensions + extension); - needs_full_search = evaluation > alpha && evaluation < beta; + needs_full_search = evaluation > alpha && evaluation < beta; // && evaluation < beta? + // } } if needs_full_search { diff --git a/src/piece_square_tables.rs b/src/piece_square_tables.rs index 56f9331..968ef04 100644 --- a/src/piece_square_tables.rs +++ b/src/piece_square_tables.rs @@ -92,7 +92,7 @@ pub const ENDGAME_KING_TABLE: [i32; 64] = [ pub fn flip_index(i: usize) -> usize { i ^ 56 } pub const PAWN_WORTH: i32 = 100; -pub const KNIGHT_WORTH: i32 = 320; // TODO: Maybe increase knight and bishop worth to discourage trading for rook + pawn? +pub const KNIGHT_WORTH: i32 = 320; pub const BISHOP_WORTH: i32 = 330; pub const ROOK_WORTH: i32 = 500; pub const QUEEN_WORTH: i32 = 900; From 33a578c66e44ccf7cad91ad7b7f56339958b5c88 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sun, 14 Jan 2024 11:36:13 -0600 Subject: [PATCH 10/47] Updated README --- README.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 01614a5..4505d5e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,14 @@ ![](/icon/Maxwell_316x316.png) # Maxwell Chess Engine - CCRL Blitz ratings
+ A Chess engine written from scratch in Rust.
+ If you use this code verbatim, or as a reference, please credit me!

+ + CCRL Blitz ratings | Date | Version | Rating | Rank | - | --- | --- | + | --- | --- | --- | --- | | 1/14/24 | v3.0.8-1 | 1830 | 538 / 715 | - - A Chess engine written from scratch in Rust!
- If you use this code verbatim, or as a reference, please credit me!

+ +

[Play against Maxwell on Lichess!](https://lichess.org/@/MaxwellOnLC) | [Some of Maxwell's Games](https://www.chess.com/library/collections/maxwells-games-my-chess-engine-2FFU82NM4) ## Features @@ -66,4 +68,4 @@ - [Fruit 2.1](https://github.com/Warpten/Fruit-2.1/) - [Perfect 2021 Opening Book](https://sites.google.com/site/computerschess/perfect-2021-books) - [Cute Chess](https://cutechess.com/) - - [PVS Implementation](https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm) \ No newline at end of file + - [PVS Implementation](https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm) From 60e646f5ca989e317cf402f52066a09a7cfb0922 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Mon, 15 Jan 2024 23:03:50 -0600 Subject: [PATCH 11/47] 1/15/24 11:03 PM rewrote PVS + LMR conditions fixed hash_move --- README.md | 2 ++ src/bot.rs | 61 ++++++++++++++++++++++++++++++------------------------ 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4505d5e..9d8e37e 100644 --- a/README.md +++ b/README.md @@ -69,3 +69,5 @@ - [Perfect 2021 Opening Book](https://sites.google.com/site/computerschess/perfect-2021-books) - [Cute Chess](https://cutechess.com/) - [PVS Implementation](https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm) + - [LMR Implementation](https://web.archive.org/web/20150212051846/http://www.glaurungchess.com/lmr.html) + - [Mediocre Chess](https://mediocrechess.blogspot.com/) \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 5eba6c9..19d64d1 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -80,6 +80,10 @@ sadge Score of Aspiration window (30) vs Current: 17 - 15 - 18 Score of Aspiration window (30) vs Current: 19 - 18 - 13 Score of Aspiration window (40) vs Current: 15 - 15 - 20 + + + +Score of PVS changes vs Current: 19 - 15 - 16 */ @@ -98,8 +102,6 @@ use crate::Board; pub const MAX_SEARCH_EXTENSIONS: u8 = 16; // TODO pub const ASPIRATION_WINDOW: i32 = 30; // TODO -pub const MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE: u8 = 3; - pub const MAX_DEPTH_LEFT_FOR_RFP: u8 = 4; // TODO pub const RFP_THESHOLD_PER_PLY: i32 = 60; // TODO @@ -359,12 +361,13 @@ impl Bot { let static_eval = board.evaluate(); // Null Move Pruning - if depth_left >= MIN_DEPTH_LEFT_FOR_NULL_MOVE_PRUNE + if depth_left > 2 && static_eval >= beta && board.total_material_without_pawns > 0 // This doesn't work in king and pawn endgames because of zugzwang && board.try_null_move() { - // let reduction = 3 - (depth_left - 3) / 2; // This didn't work at all lol - let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, total_extensions); + // let reduction = 2 + (depth_left - 2) / 3; TODO + let reduction = 2; + let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -beta, -beta + 1, total_extensions); board.undo_null_move(); @@ -403,7 +406,8 @@ impl Bot { } let hash_move = - if depth == 0 { + if depth == 0 + && self.best_move != NULL_MOVE { self.best_move } else if let Some(data) = self.transposition_table.table.get(&board.zobrist.key) { data.best_move @@ -412,6 +416,9 @@ impl Bot { }; let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, hash_move, depth); + // if hash_move != NULL_MOVE { + // assert_eq!(hash_move.to_coordinates(), sorted_moves[0].to_coordinates()); + // } for i in 0..sorted_moves.len() { let m = sorted_moves[i]; @@ -433,34 +440,32 @@ impl Bot { let mut evaluation = 0; let mut needs_full_search = true; - if i > 0 { + if i > 0 // > 3? + && depth_left > 1 // > 2? + && depth > 0 { let mut reduction = 0; - // Reductions - // Late Move Reduction - if i > 3 + // Late Move Reductions + if i > 4 // then remove these && depth_left > 2 - && depth > 0 && extension == 0 - && m.capture == NO_PIECE as u8 { // don't need to check for checks here because that's already an extension - // reduction += 1 + (depth_left - 2) / 2; - reduction += 1; - + && m.capture == NO_PIECE as u8 { + reduction += 1 + (depth_left - 2) / 5; // History Reductions - // if depth_left > 3 { + // if depth_left - reduction > 3 { // let history_value = self.move_sorter.history[m.piece as usize][m.to as usize]; - // // 1 + (HISTORY_THRESHOLD - history_value) / HISTORY_THRESHOLD_PER_PLY_REDUCTION for reduction? + // // TODO: Figure out some sort of formula for this reduction // reduction += if history_value < HISTORY_THRESHOLD { 1 } else { 0 }; // } - - // evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 2, -alpha - 1, -alpha, total_extensions); - // needs_full_search = evaluation > alpha; } // if needs_full_search { evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, total_extensions + extension); - needs_full_search = evaluation > alpha && evaluation < beta; // && evaluation < beta? + needs_full_search = evaluation > alpha; // } + // needs_full_search = evaluation > alpha && evaluation < beta; // ? + // needs_full_search = evaluation > alpha || evaluation >= beta; // ? + // needs_full_search = evaluation > alpha; // ? } if needs_full_search { @@ -502,9 +507,9 @@ impl Bot { } } - /* I don't need to check for a null move here, because the first move in the list - will always be the best move so far */ + // if !not_pv { // ? self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); + // } alpha } @@ -536,10 +541,12 @@ impl Bot { for m in sorted_moves { // Delta Pruning if !board.king_in_check(board.white_to_move) { // && board.total_material_without_pawns > 0 made it worse - let mut threshold = QUEEN_WORTH; - if PROMOTABLE.contains(&m.flag) { - threshold += BASE_WORTHS_OF_PIECE_TYPE[m.flag as usize] - PAWN_WORTH; - } + let threshold = QUEEN_WORTH + + if PROMOTABLE.contains(&m.flag) { + BASE_WORTHS_OF_PIECE_TYPE[m.flag as usize] - PAWN_WORTH + } else { + 0 + }; if evaluation < alpha - threshold { continue; From 7b5a2d6b5fd9a471d32c3fb594ee516ed4097e07 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Tue, 16 Jan 2024 13:58:37 -0600 Subject: [PATCH 12/47] 1/16/24 1:58 PM Updated README Fixed white passed pawn boost Tweaked pruning, reductions and PVS LMR --- README.md | 16 ++++----- src/board.rs | 6 ++-- src/bot.rs | 94 ++++++++++++++++++++++++---------------------------- 3 files changed, 56 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 9d8e37e..786d5e6 100644 --- a/README.md +++ b/README.md @@ -35,24 +35,24 @@ - Passed, isolated and doubled pawns - Attacked squares around kings #### Move Ordering - - Hash move / best move from previous iteration + - Best move from the previous iteration, otherwise from the transposition table - MVV-LVA - 2 Killer Moves - History Heuristic - Penalty for moving a piece to a square an opponent's piece attacks #### Search - - Principal Variation Search - - Iterative deepening + - Iterative Deepening - Alpha-Beta Pruning - - Quiescence Search with Delta Pruning - - Transposition Table - - No set max size, but entries get removed after 10 moves without hits + - Principal Variation Search + - Late Move Reductions - Null Move Pruning - Razoring - Reverse Futility Pruning - - Late Move Reductions + - Quiescence Search with Delta Pruning + - Transposition Table + - No set max size, but entries get removed after 10 moves without hits - Search Extensions - - Promotions + - Pawn moves to the 2nd or 7th rank - Checks - Time management - If less than 7 moves have been played, it uses 2.5% of it's remaining time, otherwise 7% diff --git a/src/board.rs b/src/board.rs index adf4729..de81b7f 100644 --- a/src/board.rs +++ b/src/board.rs @@ -923,7 +923,7 @@ impl Board { if self.precalculated_move_data.squares_ahead_of_pawn[1][piece_index] & self.piece_bitboards[BLACK_PAWN] == 0 && self.precalculated_move_data.file_in_front_of_pawn[1][piece_index] & self.piece_bitboards[WHITE_PAWN] == 0 { // Passed pawn - white_pawn_evaluation += PASSED_PAWN_BOOST[8 - piece_index / 8]; + white_pawn_evaluation += PASSED_PAWN_BOOST[7 - piece_index / 8]; } } } else { @@ -947,7 +947,7 @@ impl Board { } } - let pawn_evaluation_multiplier = (endgame + 0.3).clamp(0.3, 1.0); // TODO + let pawn_evaluation_multiplier = (endgame + 0.3).clamp(0.3, 1.0); // TODO white_pawn_evaluation = (white_pawn_evaluation as f32 * pawn_evaluation_multiplier) as i32; black_pawn_evaluation = (black_pawn_evaluation as f32 * pawn_evaluation_multiplier) as i32; @@ -978,6 +978,8 @@ impl Board { // TODO: a small boost for having the bishop pair? + // TODO: rooks on open lines + ((white_material + white_attacked_squares * 10 - weak_squares_around_white_king * 20 + white_pawn_evaluation) - (black_material + black_attacked_squares * 10 - weak_squares_around_black_king * 20 + black_pawn_evaluation)) * self.perspective() } diff --git a/src/bot.rs b/src/bot.rs index 19d64d1..ebe429f 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -83,7 +83,8 @@ Score of Aspiration window (40) vs Current: 15 - 15 - 20 -Score of PVS changes vs Current: 19 - 15 - 16 +Score of PVS changes 1 vs Current: 19 - 15 - 16 +Score of PVS changes 2 vs PVS changes 1: 19 - 14 - 17 */ @@ -102,14 +103,6 @@ use crate::Board; pub const MAX_SEARCH_EXTENSIONS: u8 = 16; // TODO pub const ASPIRATION_WINDOW: i32 = 30; // TODO -pub const MAX_DEPTH_LEFT_FOR_RFP: u8 = 4; // TODO -pub const RFP_THESHOLD_PER_PLY: i32 = 60; // TODO - -pub const MAX_DEPTH_LEFT_FOR_RAZORING: u8 = 3; -pub const RAZORING_THRESHOLD_PER_PLY: i32 = 300; - -pub const HISTORY_THRESHOLD: i32 = 2200; - pub const PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES: f32 = 0.025; // 2.5% pub const PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES: f32 = 0.07; // 7% @@ -354,20 +347,26 @@ impl Bot { let not_pv = alpha == beta - 1; - // TODO: && !evaluation_is_mate(alpha)? + // TODO: and not a mate evaluation? if not_pv && depth > 0 - && depth_left > 0 { // && board.get_last_move().capture == NO_PIECE as u8 This made it play worse + && depth_left > 0 + && !board.king_in_check(board.white_to_move) { // Checking if the last move was a capture made it worse let static_eval = board.evaluate(); + // Reverse Futility Pruning + if depth_left < 5 // maybe this should be higher? + && static_eval - 60 * (depth_left as i32) >= beta { // tweak this threshold + return static_eval; + } + // Null Move Pruning if depth_left > 2 && static_eval >= beta && board.total_material_without_pawns > 0 // This doesn't work in king and pawn endgames because of zugzwang && board.try_null_move() { // let reduction = 2 + (depth_left - 2) / 3; TODO - let reduction = 2; - let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -beta, -beta + 1, total_extensions); + let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, total_extensions); board.undo_null_move(); @@ -377,16 +376,10 @@ impl Bot { } // Razoring - if depth_left <= MAX_DEPTH_LEFT_FOR_RAZORING - && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha { + if depth_left > 3 + && static_eval + 300 * (depth_left as i32) < alpha { depth_left -= 1; } - - // Reverse Futility Pruning - if depth_left <= MAX_DEPTH_LEFT_FOR_RFP - && static_eval - RFP_THESHOLD_PER_PLY * (depth_left as i32) >= beta { - return static_eval; - } } if depth_left == 0 { @@ -420,9 +413,8 @@ impl Bot { // assert_eq!(hash_move.to_coordinates(), sorted_moves[0].to_coordinates()); // } - for i in 0..sorted_moves.len() { - let m = sorted_moves[i]; - board.make_move(m); + for (i, m) in sorted_moves.iter().enumerate() { + board.make_move(*m); let mut extension = 0; if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { @@ -436,46 +428,48 @@ impl Bot { } } - // Principal Variation Search - let mut evaluation = 0; - let mut needs_full_search = true; + /* + Thanks to everybody who replied to my Reddit thread here: + https://www.reddit.com/r/chessprogramming/comments/197ctk2/question_about_pvs_lmr/ + */ - if i > 0 // > 3? - && depth_left > 1 // > 2? - && depth > 0 { - let mut reduction = 0; + let mut evaluation = 0; + let mut needs_fuller_search = true; + // Principal Variation Search + if i > 0 + // && depth > 0 // ? + && depth_left > 1 { // Late Move Reductions - if i > 4 // then remove these - && depth_left > 2 + if i > 3 && extension == 0 && m.capture == NO_PIECE as u8 { - reduction += 1 + (depth_left - 2) / 5; + let reduction = 1 + (depth_left - 2) / 5; + // History Reductions // if depth_left - reduction > 3 { // let history_value = self.move_sorter.history[m.piece as usize][m.to as usize]; // // TODO: Figure out some sort of formula for this reduction - // reduction += if history_value < HISTORY_THRESHOLD { 1 } else { 0 }; + // reduction += if history_value < 2200 { 1 } else { 0 }; // } + + // LMR Search + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, total_extensions); + needs_fuller_search = evaluation > alpha; } - // if needs_full_search { - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, total_extensions + extension); - needs_full_search = evaluation > alpha; - // } - // needs_full_search = evaluation > alpha && evaluation < beta; // ? - // needs_full_search = evaluation > alpha || evaluation >= beta; // ? - // needs_full_search = evaluation > alpha; // ? + if needs_fuller_search { + // PVS Search + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1, -alpha - 1, -alpha, total_extensions + extension); + needs_fuller_search = evaluation > alpha; + } } - if needs_full_search { + if needs_fuller_search { + // Full Window Search evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1, -beta, -alpha, total_extensions + extension); } - // let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1, -beta, -alpha); - - - board.undo_last_move(); if self.should_cancel_search() { @@ -483,10 +477,10 @@ impl Bot { } if evaluation >= beta { - self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); + self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, *m, NodeType::LowerBound); if m.capture == NO_PIECE as u8 { - self.move_sorter.push_killer_move(m, depth); + self.move_sorter.push_killer_move(*m, depth); self.move_sorter.history[m.piece as usize][m.to as usize] += (depth_left * depth_left) as i32; } @@ -494,7 +488,7 @@ impl Bot { } if evaluation > alpha { - best_move_this_search = m; + best_move_this_search = *m; node_type = NodeType::Exact; alpha = evaluation; From 58013148b0e0cbd41d02b10e178525ecb864899f Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:30:05 -0600 Subject: [PATCH 13/47] 1/17/24 9:30 AM Added triangular PV table Worked on LMR + PVS implementation (THESE CHANGES ARENT READY YET I just didn't want to lose progress) --- src/board.rs | 1 + src/bot.rs | 48 ++++++++++++++++++++++++---------------------- src/move_sorter.rs | 47 ++++++++++++++++++++++++++++++++++----------- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/src/board.rs b/src/board.rs index de81b7f..000fe54 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1006,6 +1006,7 @@ impl Board { } pub fn try_null_move(&mut self) -> bool { + // TOOD: || self.get_last_move() == NULL_MOVE? if self.king_in_check(self.white_to_move) || self.king_in_check(!self.white_to_move) { return false; diff --git a/src/bot.rs b/src/bot.rs index ebe429f..57b8d85 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -85,6 +85,9 @@ Score of Aspiration window (40) vs Current: 15 - 15 - 20 Score of PVS changes 1 vs Current: 19 - 15 - 16 Score of PVS changes 2 vs PVS changes 1: 19 - 14 - 17 + +Bruh +Score of Removed PVS vs PVS changes 2: 19 - 13 - 18 */ @@ -94,7 +97,7 @@ use crate::piece_square_tables::{PAWN_WORTH, QUEEN_WORTH}; use crate::pieces::{PAWN, PROMOTABLE, NO_PIECE}; use crate::utils::{CHECKMATE_EVAL, evaluation_is_mate, moves_ply_from_mate}; use std::time::Instant; -use crate::move_sorter::MoveSorter; +use crate::move_sorter::{MoveSorter, MAX_SORT_MOVE_PLY}; use crate::transposition_table::{TranspositionTable, NodeType}; use crate::move_data::{MoveData, NULL_MOVE}; use crate::opening_book::OpeningBook; @@ -301,6 +304,11 @@ impl Bot { if self.config.debug_output { self.transposition_table.print_size(); } + + // for i in 0..self.move_sorter.pv_length[0] { + // print!("{} ", self.move_sorter.pv_table[0][i].to_coordinates()); + // } + // println!(""); } fn should_cancel_search(&mut self) -> bool { @@ -321,6 +329,8 @@ impl Bot { return 0; } + self.move_sorter.set_pv_length(depth as usize); + self.positions_searched += 1; if depth > 0 @@ -398,20 +408,15 @@ impl Bot { return 0; } - let hash_move = - if depth == 0 - && self.best_move != NULL_MOVE { - self.best_move - } else if let Some(data) = self.transposition_table.table.get(&board.zobrist.key) { - data.best_move - } else { - NULL_MOVE - }; - - let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, hash_move, depth); - // if hash_move != NULL_MOVE { - // assert_eq!(hash_move.to_coordinates(), sorted_moves[0].to_coordinates()); - // } + let sorted_moves = self.move_sorter.sort_moves( + board, + legal_moves, + match self.transposition_table.table.get(&board.zobrist.key) { + Some(data) => data.best_move, + None => NULL_MOVE, + }, + depth as usize, + ); for (i, m) in sorted_moves.iter().enumerate() { board.make_move(*m); @@ -436,10 +441,8 @@ impl Bot { let mut evaluation = 0; let mut needs_fuller_search = true; - // Principal Variation Search if i > 0 - // && depth > 0 // ? - && depth_left > 1 { + && depth_left > 2 { // Late Move Reductions if i > 3 && extension == 0 @@ -453,13 +456,12 @@ impl Bot { // reduction += if history_value < 2200 { 1 } else { 0 }; // } - // LMR Search evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, total_extensions); needs_fuller_search = evaluation > alpha; } + // Principal Variation Search if needs_fuller_search { - // PVS Search evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1, -alpha - 1, -alpha, total_extensions + extension); needs_fuller_search = evaluation > alpha; } @@ -480,7 +482,7 @@ impl Bot { self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, *m, NodeType::LowerBound); if m.capture == NO_PIECE as u8 { - self.move_sorter.push_killer_move(*m, depth); + self.move_sorter.push_killer_move(*m, depth as usize); self.move_sorter.history[m.piece as usize][m.to as usize] += (depth_left * depth_left) as i32; } @@ -488,6 +490,7 @@ impl Bot { } if evaluation > alpha { + self.move_sorter.push_pv_move(*m, depth as usize); best_move_this_search = *m; node_type = NodeType::Exact; alpha = evaluation; @@ -529,8 +532,7 @@ impl Bot { return evaluation; } - // Depth is set to u8::MAX because it's only used for killer moves, and we don't need that here - let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, u8::MAX); + let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, MAX_SORT_MOVE_PLY); for m in sorted_moves { // Delta Pruning diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 91d72c3..8c22df8 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -5,7 +5,7 @@ use crate::pieces::*; use crate::piece_square_tables::BASE_WORTHS_OF_PIECE_TYPE; use crate::Board; -pub const MAX_KILLER_MOVE_PLY: usize = 32; +pub const MAX_SORT_MOVE_PLY: usize = 32; pub const MVV_LVA: [i32; 36] = [ 15, 25, 35, 45, 55, 65, // Pawn @@ -17,7 +17,9 @@ pub const MVV_LVA: [i32; 36] = [ ]; pub struct MoveSorter { - pub killer_moves: [KillerMoves; MAX_KILLER_MOVE_PLY], + pub pv_length: [usize; MAX_SORT_MOVE_PLY], + pub pv_table: [[MoveData; MAX_SORT_MOVE_PLY]; MAX_SORT_MOVE_PLY], + pub killer_moves: [KillerMoves; MAX_SORT_MOVE_PLY], pub history: [[i32; 64]; PIECE_COUNT], // TODO: Countermoves? } @@ -25,23 +27,43 @@ pub struct MoveSorter { impl MoveSorter { pub fn new() -> Self { Self { - killer_moves: [KillerMoves::new(); MAX_KILLER_MOVE_PLY], + pv_length: [0; MAX_SORT_MOVE_PLY], + pv_table: [[NULL_MOVE; MAX_SORT_MOVE_PLY]; MAX_SORT_MOVE_PLY], + killer_moves: [KillerMoves::new(); MAX_SORT_MOVE_PLY], history: [[0; 64]; PIECE_COUNT], } } pub fn clear(&mut self) { - self.killer_moves = [KillerMoves::new(); MAX_KILLER_MOVE_PLY]; + self.killer_moves = [KillerMoves::new(); MAX_SORT_MOVE_PLY]; self.history = [[0; 64]; PIECE_COUNT]; } - pub fn push_killer_move(&mut self, data: MoveData, depth: u8) { - if depth < MAX_KILLER_MOVE_PLY as u8 { - self.killer_moves[depth as usize].push(data); + pub fn set_pv_length(&mut self, depth: usize) { + if depth < MAX_SORT_MOVE_PLY { + self.pv_length[depth] = depth; } } - pub fn sort_moves(&mut self, board: &mut Board, moves: Vec, hash_move: MoveData, depth: u8) -> Vec { + pub fn push_pv_move(&mut self, data: MoveData, depth: usize) { + if depth + 1 < MAX_SORT_MOVE_PLY { + self.pv_table[depth][depth] = data; + + for next_depth in (depth + 1)..self.pv_length[depth + 1] { + self.pv_table[depth][next_depth] = self.pv_table[depth + 1][next_depth]; + } + + self.pv_length[depth] = self.pv_length[depth + 1]; + } + } + + pub fn push_killer_move(&mut self, data: MoveData, depth: usize) { + if depth < MAX_SORT_MOVE_PLY { + self.killer_moves[depth].push(data); + } + } + + pub fn sort_moves(&mut self, board: &mut Board, moves: Vec, hash_move: MoveData, depth: usize) -> Vec { if moves.is_empty() { return vec![]; } @@ -56,16 +78,19 @@ impl MoveSorter { let mut score = 0; - if m == hash_move { + if depth < MAX_SORT_MOVE_PLY + && m == self.pv_table[depth][depth] { score = i32::MAX; + } else if m == hash_move { + score = i32::MAX - 1; } else { if m.capture != NO_PIECE as u8 { score += MVV_LVA[get_piece_type(m.piece as usize) * 6 + get_piece_type(m.capture as usize)] + 8000; // TODO: static exchange evaluation } else { - if depth < MAX_KILLER_MOVE_PLY as u8 - && self.killer_moves[depth as usize].is_killer(m) { + if depth < MAX_SORT_MOVE_PLY + && self.killer_moves[depth].is_killer(m) { score += 5000; } From 9a606990404315e934d10353abdd1ec4318df3a9 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Wed, 17 Jan 2024 12:40:46 -0600 Subject: [PATCH 14/47] 1/17/24 12:40 PM Added check in try_null_move to make sure you can't do 2 null moves in the same line Tweaked PVS + LMR --- src/board.rs | 4 ++-- src/bot.rs | 19 +++++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/board.rs b/src/board.rs index 000fe54..9f7e455 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1006,8 +1006,8 @@ impl Board { } pub fn try_null_move(&mut self) -> bool { - // TOOD: || self.get_last_move() == NULL_MOVE? - if self.king_in_check(self.white_to_move) + if self.get_last_move() == NULL_MOVE + || self.king_in_check(self.white_to_move) || self.king_in_check(!self.white_to_move) { return false; } diff --git a/src/bot.rs b/src/bot.rs index 57b8d85..b6af792 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -88,6 +88,8 @@ Score of PVS changes 2 vs PVS changes 1: 19 - 14 - 17 Bruh Score of Removed PVS vs PVS changes 2: 19 - 13 - 18 + +Score of PVS changes 3 vs Removed PVS: 19 - 18 - 13 */ @@ -305,8 +307,11 @@ impl Bot { self.transposition_table.print_size(); } - // for i in 0..self.move_sorter.pv_length[0] { - // print!("{} ", self.move_sorter.pv_table[0][i].to_coordinates()); + // for i in 0..MAX_SORT_MOVE_PLY { + // for j in 0..self.move_sorter.pv_length[i] { + // print!("{} ", self.move_sorter.pv_table[i][j].to_coordinates()); + // } + // println!(""); // } // println!(""); } @@ -423,7 +428,7 @@ impl Bot { let mut extension = 0; if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { - if board.king_in_check(board.white_to_move) { // TODO: or if the flag is a promotion? + if board.king_in_check(board.white_to_move) { // || PROMOTABLE.contains(&m.flag)? extension = 1; } else if m.piece == PAWN as u8 { let rank = m.to / 8; @@ -442,11 +447,13 @@ impl Bot { let mut needs_fuller_search = true; if i > 0 - && depth_left > 2 { - // Late Move Reductions - if i > 3 + && depth_left > 1 { + if i > 2 && extension == 0 && m.capture == NO_PIECE as u8 { + // TODO: Late Move Pruning? + + // Late Move Reductions let reduction = 1 + (depth_left - 2) / 5; // History Reductions From f503a43819c06d0b41e11cb84395f1fe9648d919 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Wed, 17 Jan 2024 16:03:39 -0600 Subject: [PATCH 15/47] 1/17/24 4:03 PM Tweaked RFP threshold Moved PV Table to separate struct --- src/bot.rs | 20 +++++++--------- src/main.rs | 1 + src/move_sorter.rs | 27 ++++----------------- src/pv_table.rs | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 35 deletions(-) create mode 100644 src/pv_table.rs diff --git a/src/bot.rs b/src/bot.rs index b6af792..2d0a133 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -89,7 +89,7 @@ Score of PVS changes 2 vs PVS changes 1: 19 - 14 - 17 Bruh Score of Removed PVS vs PVS changes 2: 19 - 13 - 18 -Score of PVS changes 3 vs Removed PVS: 19 - 18 - 13 +Score of PVS changes 4 vs Removed PVS: 21 - 10 - 19 */ @@ -307,13 +307,9 @@ impl Bot { self.transposition_table.print_size(); } - // for i in 0..MAX_SORT_MOVE_PLY { - // for j in 0..self.move_sorter.pv_length[i] { - // print!("{} ", self.move_sorter.pv_table[i][j].to_coordinates()); - // } - // println!(""); - // } - // println!(""); + // self.move_sorter.pv_table.print(); + // self.move_sorter.pv_table.pop(); + // self.move_sorter.pv_table.print(); } fn should_cancel_search(&mut self) -> bool { @@ -334,7 +330,7 @@ impl Bot { return 0; } - self.move_sorter.set_pv_length(depth as usize); + self.move_sorter.pv_table.set_pv_length(depth as usize); self.positions_searched += 1; @@ -371,7 +367,7 @@ impl Bot { // Reverse Futility Pruning if depth_left < 5 // maybe this should be higher? - && static_eval - 60 * (depth_left as i32) >= beta { // tweak this threshold + && static_eval - 90 * (depth_left as i32) >= beta { // tweak this threshold return static_eval; } @@ -497,7 +493,7 @@ impl Bot { } if evaluation > alpha { - self.move_sorter.push_pv_move(*m, depth as usize); + self.move_sorter.pv_table.push_pv_move(*m, depth as usize); best_move_this_search = *m; node_type = NodeType::Exact; alpha = evaluation; @@ -511,7 +507,7 @@ impl Bot { } } - // if !not_pv { // ? + // if not_pv { // ? self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); // } diff --git a/src/main.rs b/src/main.rs index 108ebfa..f140c67 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,6 +45,7 @@ mod board; mod zobrist; mod perft; mod bot; +mod pv_table; mod move_sorter; use crate::castling_rights::print_castling_rights; diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 8c22df8..4b3ccb2 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -1,3 +1,4 @@ +use crate::pv_table::PVTable; use crate::move_data::EN_PASSANT_FLAG; use crate::killer_moves::KillerMoves; use crate::move_data::{MoveData, NULL_MOVE}; @@ -17,8 +18,7 @@ pub const MVV_LVA: [i32; 36] = [ ]; pub struct MoveSorter { - pub pv_length: [usize; MAX_SORT_MOVE_PLY], - pub pv_table: [[MoveData; MAX_SORT_MOVE_PLY]; MAX_SORT_MOVE_PLY], + pub pv_table: PVTable, pub killer_moves: [KillerMoves; MAX_SORT_MOVE_PLY], pub history: [[i32; 64]; PIECE_COUNT], // TODO: Countermoves? @@ -27,8 +27,7 @@ pub struct MoveSorter { impl MoveSorter { pub fn new() -> Self { Self { - pv_length: [0; MAX_SORT_MOVE_PLY], - pv_table: [[NULL_MOVE; MAX_SORT_MOVE_PLY]; MAX_SORT_MOVE_PLY], + pv_table: PVTable::new(), killer_moves: [KillerMoves::new(); MAX_SORT_MOVE_PLY], history: [[0; 64]; PIECE_COUNT], } @@ -39,24 +38,6 @@ impl MoveSorter { self.history = [[0; 64]; PIECE_COUNT]; } - pub fn set_pv_length(&mut self, depth: usize) { - if depth < MAX_SORT_MOVE_PLY { - self.pv_length[depth] = depth; - } - } - - pub fn push_pv_move(&mut self, data: MoveData, depth: usize) { - if depth + 1 < MAX_SORT_MOVE_PLY { - self.pv_table[depth][depth] = data; - - for next_depth in (depth + 1)..self.pv_length[depth + 1] { - self.pv_table[depth][next_depth] = self.pv_table[depth + 1][next_depth]; - } - - self.pv_length[depth] = self.pv_length[depth + 1]; - } - } - pub fn push_killer_move(&mut self, data: MoveData, depth: usize) { if depth < MAX_SORT_MOVE_PLY { self.killer_moves[depth].push(data); @@ -79,7 +60,7 @@ impl MoveSorter { let mut score = 0; if depth < MAX_SORT_MOVE_PLY - && m == self.pv_table[depth][depth] { + && m == self.pv_table.get_pv_move(depth) { score = i32::MAX; } else if m == hash_move { score = i32::MAX - 1; diff --git a/src/pv_table.rs b/src/pv_table.rs new file mode 100644 index 0000000..fe0ae41 --- /dev/null +++ b/src/pv_table.rs @@ -0,0 +1,58 @@ +use crate::move_data::{MoveData, NULL_MOVE}; +use crate::move_sorter::MAX_SORT_MOVE_PLY; + +pub struct PVTable { + pub length: [usize; MAX_SORT_MOVE_PLY], + pub table: [[MoveData; MAX_SORT_MOVE_PLY]; MAX_SORT_MOVE_PLY], +} + +impl PVTable { + pub fn new() -> Self { + Self { + length: [0; MAX_SORT_MOVE_PLY], + table: [[NULL_MOVE; MAX_SORT_MOVE_PLY]; MAX_SORT_MOVE_PLY], + } + } + + pub fn print(&self) { + for i in 0..MAX_SORT_MOVE_PLY { + if self.length[i] == 0 { + break; + } + + for j in 0..self.length[i] { + print!("{} ", self.table[i][j].to_coordinates()); + } + println!(""); + } + println!(""); + } + + pub fn set_pv_length(&mut self, depth: usize) { + if depth < MAX_SORT_MOVE_PLY { + self.length[depth] = depth; + } + } + + pub fn push_pv_move(&mut self, data: MoveData, depth: usize) { + if depth + 1 < MAX_SORT_MOVE_PLY { + self.table[depth][depth] = data; + + for next_depth in (depth + 1)..self.length[depth + 1] { + self.table[depth][next_depth] = self.table[depth + 1][next_depth]; + } + + self.length[depth] = self.length[depth + 1]; + } + } + + pub fn pop(&mut self) { + self.table[0][0] = NULL_MOVE; + self.table[0].rotate_left(1); + self.length[0] -= 1; + } + + pub fn get_pv_move(&self, depth: usize) -> MoveData { + self.table[depth][depth] + } +} \ No newline at end of file From 32c86e115bf4cb81644efd27e2d89da0b2525fae Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Wed, 17 Jan 2024 19:22:50 -0600 Subject: [PATCH 16/47] 1/17/24 7:23 PM Tweaked RFP, NMP and razoring parameters & thresholds --- src/bot.rs | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/bot.rs b/src/bot.rs index 2d0a133..9d26980 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -359,35 +359,38 @@ impl Bot { let not_pv = alpha == beta - 1; // TODO: and not a mate evaluation? - if not_pv - && depth > 0 + if depth > 0 && depth_left > 0 && !board.king_in_check(board.white_to_move) { // Checking if the last move was a capture made it worse let static_eval = board.evaluate(); - // Reverse Futility Pruning - if depth_left < 5 // maybe this should be higher? - && static_eval - 90 * (depth_left as i32) >= beta { // tweak this threshold - return static_eval; - } + if not_pv { + // TODO: all these need to be tweaked + if depth_left < 7 + && (static_eval + 300 * (depth_left as i32) < alpha // Futility Pruning + || static_eval - 90 * (depth_left as i32) >= beta) { // Reverse Futility Pruning + return static_eval; + } - // Null Move Pruning - if depth_left > 2 - && static_eval >= beta - && board.total_material_without_pawns > 0 // This doesn't work in king and pawn endgames because of zugzwang - && board.try_null_move() { - // let reduction = 2 + (depth_left - 2) / 3; TODO - let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, total_extensions); + // Null Move Pruning + if depth_left > 2 + && static_eval >= beta + && board.total_material_without_pawns > 0 // This doesn't work in king and pawn endgames because of zugzwang + && board.try_null_move() { + // let reduction = 2; + let reduction = 2 + (depth_left - 2) / 4; // TODO + let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -beta, -beta + 1, total_extensions); - board.undo_null_move(); + board.undo_null_move(); - if evaluation >= beta { - return evaluation; + if evaluation >= beta { + return evaluation; + } } } // Razoring - if depth_left > 3 + if depth_left < 3 && static_eval + 300 * (depth_left as i32) < alpha { depth_left -= 1; } From 0e40acb983a874ccc9351f60afbbb2c60dfa4adf Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Wed, 17 Jan 2024 22:22:01 -0600 Subject: [PATCH 17/47] 1/17/24 10:22 PM Added mate distance pruning Tweaked FP, RFP, and NMP thresholds --- src/board.rs | 6 ++++++ src/bot.rs | 40 ++++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/board.rs b/src/board.rs index 9f7e455..3c64da4 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1024,4 +1024,10 @@ impl Board { self.zobrist.pop(); self.moves.pop(); } + + pub fn is_draw(&self) -> bool { + self.fifty_move_draw.current >= 50 + || self.insufficient_checkmating_material() + || self.zobrist.is_threefold_repetition() + } } \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 9d26980..aef12c2 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -90,6 +90,9 @@ Bruh Score of Removed PVS vs PVS changes 2: 19 - 13 - 18 Score of PVS changes 4 vs Removed PVS: 21 - 10 - 19 + +Score of Pruning & reduction changes 1 vs Current: 21 - 19 - 10 +Score of Pruning & reduction changes 2 vs Current: 21 - 15 - 14 */ @@ -330,19 +333,24 @@ impl Bot { return 0; } - self.move_sorter.pv_table.set_pv_length(depth as usize); - self.positions_searched += 1; - if depth > 0 - && (board.fifty_move_draw.current >= 50 - || board.insufficient_checkmating_material() - || board.zobrist.is_threefold_repetition()) { - // Should I use this to discourage making a draw in a winning position? - // return -self.quiescence_search(board, alpha, beta); - return 0; + if depth > 0 { + if board.is_draw() { + // Should I use this to discourage making a draw in a winning position? + // return -self.quiescence_search(board, alpha, beta); + return 0; + } + + // Mate Distance Pruning + let alpha = i32::max(alpha, -CHECKMATE_EVAL + depth as i32); + if alpha >= i32::min(beta, CHECKMATE_EVAL - depth as i32) { + return alpha; + } } + self.move_sorter.pv_table.set_pv_length(depth as usize); + if let Some(data) = self.transposition_table.lookup(board.zobrist.key, depth_left, depth, alpha, beta) { self.positions_searched -= 1; self.transposition_hits += 1; @@ -358,27 +366,27 @@ impl Bot { let not_pv = alpha == beta - 1; - // TODO: and not a mate evaluation? if depth > 0 && depth_left > 0 && !board.king_in_check(board.white_to_move) { // Checking if the last move was a capture made it worse let static_eval = board.evaluate(); if not_pv { - // TODO: all these need to be tweaked + // TODO: all these values need to be tweaked if depth_left < 7 - && (static_eval + 300 * (depth_left as i32) < alpha // Futility Pruning - || static_eval - 90 * (depth_left as i32) >= beta) { // Reverse Futility Pruning + // && depth_left > 1 + && (static_eval + 500 * (depth_left as i32) < alpha // Futility Pruning + || static_eval - 100 * (depth_left as i32) >= beta) { // Reverse Futility Pruning return static_eval; } // Null Move Pruning - if depth_left > 2 + if depth_left > 1 && static_eval >= beta && board.total_material_without_pawns > 0 // This doesn't work in king and pawn endgames because of zugzwang && board.try_null_move() { // let reduction = 2; - let reduction = 2 + (depth_left - 2) / 4; // TODO + let reduction = 1 + (depth_left - 2) / 4; // TODO let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -beta, -beta + 1, total_extensions); board.undo_null_move(); @@ -446,7 +454,7 @@ impl Bot { let mut needs_fuller_search = true; if i > 0 - && depth_left > 1 { + && depth_left > 1 { // 1 or 2? if i > 2 && extension == 0 && m.capture == NO_PIECE as u8 { From b4da0f5002d0a01c91f8a36457b2cb59fe285c1a Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Thu, 18 Jan 2024 15:04:46 -0600 Subject: [PATCH 18/47] 1/18/24 3:04 PM v1 of the new (again) re-write Tweaked NMP parameters Fixed hash move --- README.md | 4 +- Test results.txt | 2 + src/bot.rs | 274 ++++++++++--------------------------- src/main.rs | 12 +- src/move_sorter.rs | 13 +- src/piece_square_tables.rs | 30 ++-- 6 files changed, 112 insertions(+), 223 deletions(-) create mode 100644 Test results.txt diff --git a/README.md b/README.md index 786d5e6..8c16612 100644 --- a/README.md +++ b/README.md @@ -70,4 +70,6 @@ - [Cute Chess](https://cutechess.com/) - [PVS Implementation](https://web.archive.org/web/20071030220825/http://www.brucemo.com/compchess/programming/pvs.htm) - [LMR Implementation](https://web.archive.org/web/20150212051846/http://www.glaurungchess.com/lmr.html) - - [Mediocre Chess](https://mediocrechess.blogspot.com/) \ No newline at end of file + - [Mediocre Chess](https://mediocrechess.blogspot.com/) + - [Tcheran](https://github.com/jgilchrist/tcheran/) + - [Rustic Chess Engine MVV-LVA](https://rustic-chess.org/search/ordering/mvv_lva.html) \ No newline at end of file diff --git a/Test results.txt b/Test results.txt new file mode 100644 index 0000000..8971b3e --- /dev/null +++ b/Test results.txt @@ -0,0 +1,2 @@ +REWRITE v1 vs v3.0.8 ++17, -14, =19 \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index aef12c2..d1e0c4a 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,115 +1,15 @@ -/* RESULTS -Aspiration Window: - The problems go deeper than I thought, I'm gonna have to get some help with this, - because apparently PVS + TT + AW = alot of problems - - Rank Name Elo +/- Games Score Draw - 1 No window 31 63 100 54.5% 17.0% - 2 Hold window 0 65 100 50.0% 12.0% - 3 Reset window -31 62 100 45.5% 19.0% - -Score of PVS vs No PVS: 27 - 13 - 10 - -Score of PVS & LMR vs PVS: 32 - 10 - 8 - -Extensions: - Rank Name Elo +/- Games Score Draw - 1 All extensions 21 50 150 53.0% 20.7% - 2 Promotion extension -5 50 150 49.3% 21.3% - 3 No extensions -7 50 150 49.0% 20.7% - 4 Check extension -9 49 150 48.7% 22.7% - -Null Move Pruning: - Rank Name Elo +/- Games Score Draw - 1 NMP static eval 40 42 200 55.8% 25.5% - 2 NMP 26 41 200 53.8% 26.5% - 3 NMP capture, static eval 16 41 200 52.3% 26.5% - 4 NMP capture -9 41 200 48.8% 28.5% - 5 No NMP -74 43 200 39.5% 23.0% - -Score of Order-Hash move, MVV-LVA vs Order-Hash move: 41 - 3 - 6 - -History Heuristic & Killer Moves - Rank Name Elo +/- Games Score Draw - 1 Killer moves, history 49 48 150 57.0% 26.0% - 2 History 44 48 150 56.3% 26.0% - 3 Killer moves 23 48 150 53.3% 25.3% - 4 Current -120 49 150 33.3% 28.0% - -History Reductions: - Idk there's something about this technique that feels off, I think I'm gonna comment it out for now - - (With a value of 800 is lost horribly, and with a value of 3000 it did worse than 1600) - Score of History reductions (1600) vs Current: 22 - 14 - 14 - Score of History reductions (2200) vs Current: 23 - 12 - 15 - -Razoring: - 200 and 400 made it worse - TODO: try 280 and 320 - Score of Razoring (300) vs Current: 23 - 11 - 16 - -Move Ordering tests: - Rank Name Elo +/- Games Score Draw - 1 Order attacked square penalty 56 47 150 58.0% 30.7% - 2 Current 0 47 150 50.0% 29.3% - 3 Order protected square boost -23 47 150 46.7% 29.3% - 4 Order both -33 48 150 45.3% 28.0% - -Reverse Futility Pruning: -Rank Name Elo +/- Games Score Draw - 1 RFP after razor 115 55 100 66.0% 38.0% - 2 RFP before razor 45 56 100 56.5% 35.0% - 3 Current -168 59 100 27.5% 33.0% - -Delta Pruning -Rank Name Elo +/- Games Score Draw - 1 Delta pruning 28 54 100 54.0% 38.0% - 2 Delta pruning with material check 7 57 100 51.0% 30.0% - 3 Current -35 58 100 45.0% 30.0% - -I lowered the values of the pawn endgame PST, and this was the result XD -Score of Lower pawn eval vs Test-Current: 25 - 9 - 16 - - -First match vs v3.0.8: -Score of Maxwell v3.Latest vs Maxwell v3.0.8: 14 - 25 - 11 -sadge - - -20 did no better -Score of Aspiration window (30) vs Current: 17 - 15 - 18 -Score of Aspiration window (30) vs Current: 19 - 18 - 13 -Score of Aspiration window (40) vs Current: 15 - 15 - 20 - - - -Score of PVS changes 1 vs Current: 19 - 15 - 16 -Score of PVS changes 2 vs PVS changes 1: 19 - 14 - 17 - -Bruh -Score of Removed PVS vs PVS changes 2: 19 - 13 - 18 - -Score of PVS changes 4 vs Removed PVS: 21 - 10 - 19 - -Score of Pruning & reduction changes 1 vs Current: 21 - 19 - 10 -Score of Pruning & reduction changes 2 vs Current: 21 - 15 - 14 -*/ - - -use crate::piece_square_tables::BASE_WORTHS_OF_PIECE_TYPE; use crate::STARTING_FEN; use crate::piece_square_tables::{PAWN_WORTH, QUEEN_WORTH}; use crate::pieces::{PAWN, PROMOTABLE, NO_PIECE}; use crate::utils::{CHECKMATE_EVAL, evaluation_is_mate, moves_ply_from_mate}; use std::time::Instant; -use crate::move_sorter::{MoveSorter, MAX_SORT_MOVE_PLY}; +use crate::move_sorter::MoveSorter; use crate::transposition_table::{TranspositionTable, NodeType}; use crate::move_data::{MoveData, NULL_MOVE}; use crate::opening_book::OpeningBook; use crate::Board; pub const MAX_SEARCH_EXTENSIONS: u8 = 16; // TODO -pub const ASPIRATION_WINDOW: i32 = 30; // TODO pub const PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES: f32 = 0.025; // 2.5% pub const PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES: f32 = 0.07; // 7% @@ -239,8 +139,7 @@ impl Bot { self.search_cancelled = false; - // let mut alpha = -i32::MAX; - // let mut beta = i32::MAX; + let last_evaluation = self.evaluation; self.best_move = NULL_MOVE; self.evaluation = 0; @@ -251,6 +150,8 @@ impl Bot { self.move_sorter.clear(); + let mut window = 40; + self.think_timer = Instant::now(); for depth in 1..=depth_to_search { self.searched_one_move = false; @@ -258,17 +159,17 @@ impl Bot { self.evaluation_this_iteration = 0; - // Aspiration Window: not really working for this engine, idk if it's worth the search instability - // let mut evaluation = self.alpha_beta_search(board, 0, depth, alpha, beta, 0); + loop { + let (alpha, beta) = (last_evaluation - window, last_evaluation + window); - // if evaluation <= alpha || evaluation >= beta { - // evaluation = self.alpha_beta_search(board, 0, depth, -i32::MAX, i32::MAX, 0); - // } + let evaluation = self.alpha_beta_search(board, 0, depth, alpha, beta, 0); - // alpha = evaluation - ASPIRATION_WINDOW; - // beta = evaluation + ASPIRATION_WINDOW; + if alpha < evaluation && evaluation < beta { + break; + } - let _ = self.alpha_beta_search(board, 0, depth, -i32::MAX, i32::MAX, 0); + window *= 4; + } if !self.search_cancelled @@ -277,10 +178,9 @@ impl Bot { self.evaluation = self.evaluation_this_iteration; } - // , Window: {} - self.println(format!("Depth: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}", + self.println(format!("Depth: {}, Window: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}", depth, - // window, + window, self.evaluation * board.perspective(), self.best_move.to_coordinates(), self.positions_searched, @@ -309,10 +209,6 @@ impl Bot { if self.config.debug_output { self.transposition_table.print_size(); } - - // self.move_sorter.pv_table.print(); - // self.move_sorter.pv_table.pop(); - // self.move_sorter.pv_table.print(); } fn should_cancel_search(&mut self) -> bool { @@ -349,10 +245,7 @@ impl Bot { } } - self.move_sorter.pv_table.set_pv_length(depth as usize); - if let Some(data) = self.transposition_table.lookup(board.zobrist.key, depth_left, depth, alpha, beta) { - self.positions_searched -= 1; self.transposition_hits += 1; if depth == 0 { @@ -364,41 +257,39 @@ impl Bot { return data.evaluation; } - let not_pv = alpha == beta - 1; + let is_pv = alpha != beta - 1; - if depth > 0 + if !is_pv + && depth > 0 && depth_left > 0 - && !board.king_in_check(board.white_to_move) { // Checking if the last move was a capture made it worse - let static_eval = board.evaluate(); + && board.get_last_move().capture == NO_PIECE as u8 // ? + && !board.king_in_check(board.white_to_move) { + // TODO: move these around - if not_pv { - // TODO: all these values need to be tweaked - if depth_left < 7 - // && depth_left > 1 - && (static_eval + 500 * (depth_left as i32) < alpha // Futility Pruning - || static_eval - 100 * (depth_left as i32) >= beta) { // Reverse Futility Pruning - return static_eval; - } + let static_eval = board.evaluate(); - // Null Move Pruning - if depth_left > 1 - && static_eval >= beta - && board.total_material_without_pawns > 0 // This doesn't work in king and pawn endgames because of zugzwang - && board.try_null_move() { - // let reduction = 2; - let reduction = 1 + (depth_left - 2) / 4; // TODO - let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -beta, -beta + 1, total_extensions); + // Null Move Pruning + if depth_left > 2 + && static_eval >= beta + // && board.total_material_without_pawns > 0 + && board.try_null_move() { + let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, total_extensions); - board.undo_null_move(); + board.undo_null_move(); - if evaluation >= beta { - return evaluation; - } + if evaluation >= beta { + return evaluation; } } + // Reverse Futility Pruning + if depth_left < 5 + && static_eval - 60 * (depth_left as i32) >= beta { // TODO: tweak this threshold + return static_eval; + } + // Razoring - if depth_left < 3 + if depth_left < 4 && static_eval + 300 * (depth_left as i32) < alpha { depth_left -= 1; } @@ -423,67 +314,51 @@ impl Bot { let sorted_moves = self.move_sorter.sort_moves( board, legal_moves, - match self.transposition_table.table.get(&board.zobrist.key) { - Some(data) => data.best_move, - None => NULL_MOVE, + if depth == 0 + && self.best_move != NULL_MOVE { + self.best_move + } else { + match self.transposition_table.table.get(&board.zobrist.key) { + Some(data) => data.best_move, + None => NULL_MOVE, + } }, depth as usize, ); - for (i, m) in sorted_moves.iter().enumerate() { - board.make_move(*m); + for i in 0..sorted_moves.len() { + let m = sorted_moves[i]; + board.make_move(m); let mut extension = 0; if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { - if board.king_in_check(board.white_to_move) { // || PROMOTABLE.contains(&m.flag)? + if board.king_in_check(board.white_to_move) { extension = 1; - } else if m.piece == PAWN as u8 { - let rank = m.to / 8; - if rank == 1 || rank == 6 { - extension = 1; + } else { + if m.piece == PAWN as u8 { + let rank = m.to / 8; + if rank == 1 || rank == 6 { + extension = 1; + } } } } - /* - Thanks to everybody who replied to my Reddit thread here: - https://www.reddit.com/r/chessprogramming/comments/197ctk2/question_about_pvs_lmr/ - */ - + // Late Move Reduction / (Kind of) Principal Variation Search let mut evaluation = 0; - let mut needs_fuller_search = true; - - if i > 0 - && depth_left > 1 { // 1 or 2? - if i > 2 - && extension == 0 - && m.capture == NO_PIECE as u8 { - // TODO: Late Move Pruning? - - // Late Move Reductions - let reduction = 1 + (depth_left - 2) / 5; - - // History Reductions - // if depth_left - reduction > 3 { - // let history_value = self.move_sorter.history[m.piece as usize][m.to as usize]; - // // TODO: Figure out some sort of formula for this reduction - // reduction += if history_value < 2200 { 1 } else { 0 }; - // } - - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - reduction - 1, -alpha - 1, -alpha, total_extensions); - needs_fuller_search = evaluation > alpha; - } - - // Principal Variation Search - if needs_fuller_search { - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1, -alpha - 1, -alpha, total_extensions + extension); - needs_fuller_search = evaluation > alpha; - } + let mut needs_full_search = true; + + if i > 2 + && depth_left > 2 + && extension == 0 + && m.capture == NO_PIECE as u8 { + // let reduction = 1; + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 2, -alpha - 1, -alpha, total_extensions); + needs_full_search = evaluation > alpha; } - if needs_fuller_search { - // Full Window Search - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1, -beta, -alpha, total_extensions + extension); + if needs_full_search { + evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1 + extension, -beta, -alpha, total_extensions + extension); } board.undo_last_move(); @@ -493,10 +368,10 @@ impl Bot { } if evaluation >= beta { - self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, *m, NodeType::LowerBound); + self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); if m.capture == NO_PIECE as u8 { - self.move_sorter.push_killer_move(*m, depth as usize); + self.move_sorter.push_killer_move(m, depth as usize); self.move_sorter.history[m.piece as usize][m.to as usize] += (depth_left * depth_left) as i32; } @@ -504,8 +379,7 @@ impl Bot { } if evaluation > alpha { - self.move_sorter.pv_table.push_pv_move(*m, depth as usize); - best_move_this_search = *m; + best_move_this_search = m; node_type = NodeType::Exact; alpha = evaluation; @@ -518,9 +392,7 @@ impl Bot { } } - // if not_pv { // ? self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); - // } alpha } @@ -546,14 +418,14 @@ impl Bot { return evaluation; } - let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, MAX_SORT_MOVE_PLY); + let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, usize::MAX); for m in sorted_moves { // Delta Pruning - if !board.king_in_check(board.white_to_move) { // && board.total_material_without_pawns > 0 made it worse + if !board.king_in_check(board.white_to_move) { let threshold = QUEEN_WORTH + if PROMOTABLE.contains(&m.flag) { - BASE_WORTHS_OF_PIECE_TYPE[m.flag as usize] - PAWN_WORTH + QUEEN_WORTH - PAWN_WORTH } else { 0 }; diff --git a/src/main.rs b/src/main.rs index f140c67..7507e5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,21 +4,21 @@ big idea: then write my own tuning / matchmaking program that will tweak the values and play matches until it finds better values... +thoughts on NNUE: + I've wanted to learn how to write neural net for a long time, so I want to implement NNUE eventually. + But what I'm not going to do is just find a SF NNUE library and stick it in there because that's lame + calculate my own magic numbers; currently "borrowing" Sebastian Lague's ^^ check out pin detection to speed up check detection? -try to write a neural network to evaluate positions? :o figure out how to implement "pondering" to think on opponent's time -Ideas I've tried, but they didn't help, or made it play worse (Or I implemented them wrong :P) -https://www.chessprogramming.org/Futility_Pruning +Random ideas to try https://www.chessprogramming.org/Internal_Iterative_Deepening - -Random ideas to try (from other engines and chessprogramming.org) https://www.chessprogramming.org/Static_Exchange_Evaluation https://www.chessprogramming.org/History_Leaf_Pruning https://www.chessprogramming.org/Futility_Pruning#MoveCountBasedPruning -https://www.chessprogramming.org/Triangular_PV-Table https://www.chessprogramming.org/Countermove_Heuristic +https://www.chessprogramming.org/ProbCut Some random resources I found: (Not using them right now but they could be useful) https://analog-hors.github.io/site/magic-bitboards/ diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 4b3ccb2..8920cbc 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -18,7 +18,7 @@ pub const MVV_LVA: [i32; 36] = [ ]; pub struct MoveSorter { - pub pv_table: PVTable, + // pub pv_table: PVTable, pub killer_moves: [KillerMoves; MAX_SORT_MOVE_PLY], pub history: [[i32; 64]; PIECE_COUNT], // TODO: Countermoves? @@ -27,7 +27,7 @@ pub struct MoveSorter { impl MoveSorter { pub fn new() -> Self { Self { - pv_table: PVTable::new(), + // pv_table: PVTable::new(), killer_moves: [KillerMoves::new(); MAX_SORT_MOVE_PLY], history: [[0; 64]; PIECE_COUNT], } @@ -59,11 +59,12 @@ impl MoveSorter { let mut score = 0; - if depth < MAX_SORT_MOVE_PLY - && m == self.pv_table.get_pv_move(depth) { + // if depth < MAX_SORT_MOVE_PLY + // && m == self.pv_table.get_pv_move(depth) { + // score = i32::MAX; + // } else + if m == hash_move { score = i32::MAX; - } else if m == hash_move { - score = i32::MAX - 1; } else { if m.capture != NO_PIECE as u8 { score += MVV_LVA[get_piece_type(m.piece as usize) * 6 + get_piece_type(m.capture as usize)] + 8000; diff --git a/src/piece_square_tables.rs b/src/piece_square_tables.rs index 968ef04..92e77f8 100644 --- a/src/piece_square_tables.rs +++ b/src/piece_square_tables.rs @@ -11,16 +11,28 @@ pub const MIDDLEGAME_PAWN_TABLE: [i32; 64] = [ 0, 0, 0, 0, 0, 0, 0, 0, ]; -// TODO: tweak these further +// I had changed these values to this and I'm *pretty* sure that it destroyed the previous +// version but now I'm doubting myself +// pub const ENDGAME_PAWN_TABLE: [i32; 64] = [ +// 0, 0, 0, 0, 0, 0, 0, 0, +// 80, 80, 80, 80, 80, 80, 80, 80, +// 55, 55, 55, 55, 55, 55, 55, 55, +// 35, 35, 35, 35, 35, 35, 35, 35, +// 20, 20, 20, 20, 20, 20, 20, 20, +// 10, 10, 10, 10, 10, 10, 10, 10, +// 10, 10, 10, 10, 10, 10, 10, 10, +// 0, 0, 0, 0, 0, 0, 0, 0, +// ]; + pub const ENDGAME_PAWN_TABLE: [i32; 64] = [ - 0, 0, 0, 0, 0, 0, 0, 0, - 80, 80, 80, 80, 80, 80, 80, 80, - 55, 55, 55, 55, 55, 55, 55, 55, - 35, 35, 35, 35, 35, 35, 35, 35, - 20, 20, 20, 20, 20, 20, 20, 20, - 10, 10, 10, 10, 10, 10, 10, 10, - 10, 10, 10, 10, 10, 10, 10, 10, - 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 100,100,100,100,100,100,100,100, + 70, 70, 70, 70, 70, 70, 70, 70, + 50, 50, 50, 50, 50, 50, 50, 50, + 30, 30, 30, 30, 30, 30, 30, 30, + 15, 15, 15, 15, 15, 15, 15, 15, + 15, 15, 15, 15, 15, 15, 15, 15, + 0, 0, 0, 0, 0, 0, 0, 0, ]; pub const KNIGHT_TABLE: [i32; 64] = [ From a1f439c23816a3ca52463fa4dbb273d219d7026d Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Fri, 19 Jan 2024 00:34:14 -0600 Subject: [PATCH 19/47] 1/19/24 12:34 AM Move generation now uses pseudo legal moves, and legality is now being checked during the move loop Rewrote move sorter --- Test results.txt | 7 ++++++- src/board.rs | 51 ++++++++++++++++++++++++--------------------- src/bot.rs | 42 ++++++++++++++++++++++--------------- src/main.rs | 7 +++++++ src/move_sorter.rs | 23 +++++--------------- src/perft.rs | 6 ++++-- src/value_holder.rs | 1 + 7 files changed, 75 insertions(+), 62 deletions(-) diff --git a/Test results.txt b/Test results.txt index 8971b3e..4c0cf20 100644 --- a/Test results.txt +++ b/Test results.txt @@ -1,2 +1,7 @@ REWRITE v1 vs v3.0.8 -+17, -14, =19 \ No newline at end of file ++17, -14, =19 + + Elo +/- Games Score Draw +1 Re-write v2 63 58 100 59.0% 30.0% +2 Re-write v1 -7 58 100 49.0% 28.0% +3 Maxwell v3.0.8 -56 56 100 42.0% 34.0% \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index 3c64da4..32130fd 100644 --- a/src/board.rs +++ b/src/board.rs @@ -230,12 +230,12 @@ impl Board { pub fn play_move(&mut self, data: MoveData) -> bool { let promoting = PROMOTABLE.contains(&data.flag); if self.white_to_move == is_piece_white(self.get_piece(data.from)) { - let legal_moves = self.get_legal_moves_for_piece(data.from, false); - for m in legal_moves { + let moves = self.get_moves_for_piece(data.from, false); + for m in moves { if (!promoting || data.flag == m.flag) && m.from == data.from - && m.to == data.to { - self.make_move(m); + && m.to == data.to + && self.make_move(m) { return true; } } @@ -245,7 +245,7 @@ impl Board { false } - pub fn make_move(&mut self, data: MoveData) { + pub fn make_move(&mut self, data: MoveData) -> bool { let piece_color = is_piece_white(data.piece as usize) as usize; let other_color = !is_piece_white(data.piece as usize) as usize; @@ -356,6 +356,13 @@ impl Board { self.moves.push(data); self.white_to_move = !self.white_to_move; + + if self.king_in_check(!self.white_to_move) { + self.undo_last_move(); + return false; + } + + true } pub fn undo_last_move(&mut self) -> bool { @@ -447,7 +454,7 @@ impl Board { self.piece_bitboards[build_piece(king_is_white, KING)] & attacked_squares != 0 } - pub fn get_legal_moves_for_color(&mut self, white_pieces: bool, only_captures: bool) -> Vec { + pub fn get_pseudo_legal_moves_for_color(&mut self, white_pieces: bool, only_captures: bool) -> Vec { let mut result = vec![]; let pieces = if white_pieces { @@ -461,14 +468,26 @@ impl Board { while bitboard != 0 { let piece_index = pop_lsb(&mut bitboard); - result.extend(self.get_legal_moves_for_piece(piece_index, only_captures)); + result.extend(self.get_moves_for_piece(piece_index, only_captures)); } } + // for i in (0..result.len()).rev() { + // self.make_move(result[i]); + + // if self.king_in_check(!self.white_to_move) { + // result.remove(i); + // } + + // self.undo_last_move(); + // } + + // result + result } - pub fn get_legal_moves_for_piece(&mut self, piece_index: u8, only_captures: bool) -> Vec { + pub fn get_moves_for_piece(&mut self, piece_index: u8, only_captures: bool) -> Vec { let mut result = vec![]; let piece = self.get_piece(piece_index); @@ -843,22 +862,6 @@ impl Board { _ => {} } - for i in (0..result.len()).rev() { - let data = result[i]; - - // if data.piece == NO_PIECE as u8 { - // println!("Illegal move found! {:#?} on piece: {}, and index: {}, captures only: {}", data, piece_type, piece_index, only_captures); - // } - - self.make_move(data); - - if self.king_in_check(!self.white_to_move) { - result.remove(i); - } - - self.undo_last_move(); - } - result } diff --git a/src/bot.rs b/src/bot.rs index d1e0c4a..778f475 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -257,9 +257,9 @@ impl Bot { return data.evaluation; } - let is_pv = alpha != beta - 1; + let not_pv = alpha == beta - 1; - if !is_pv + if not_pv && depth > 0 && depth_left > 0 && board.get_last_move().capture == NO_PIECE as u8 // ? @@ -302,18 +302,11 @@ impl Bot { let mut best_move_this_search = NULL_MOVE; let mut node_type = NodeType::UpperBound; - let legal_moves = board.get_legal_moves_for_color(board.white_to_move, false); - if legal_moves.is_empty() { - if board.king_in_check(board.white_to_move) { - let mate_score = CHECKMATE_EVAL - depth as i32; - return -mate_score; - } - return 0; - } + let moves = board.get_pseudo_legal_moves_for_color(board.white_to_move, false); let sorted_moves = self.move_sorter.sort_moves( board, - legal_moves, + moves, if depth == 0 && self.best_move != NULL_MOVE { self.best_move @@ -326,9 +319,11 @@ impl Bot { depth as usize, ); - for i in 0..sorted_moves.len() { - let m = sorted_moves[i]; - board.make_move(m); + let mut i = 0; + for (_, m) in sorted_moves { + if !board.make_move(m) { + continue; + } let mut extension = 0; if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { @@ -390,6 +385,16 @@ impl Bot { self.evaluation_this_iteration = evaluation; } } + + i += 1; + } + + if i == 0 { + if board.king_in_check(board.white_to_move) { + let mate_score = CHECKMATE_EVAL - depth as i32; + return -mate_score; + } + return 0; } self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); @@ -413,14 +418,14 @@ impl Bot { alpha = evaluation; } - let legal_moves = board.get_legal_moves_for_color(board.white_to_move, true); + let legal_moves = board.get_pseudo_legal_moves_for_color(board.white_to_move, true); if legal_moves.is_empty() { return evaluation; } let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, usize::MAX); - for m in sorted_moves { + for (_, m) in sorted_moves { // Delta Pruning if !board.king_in_check(board.white_to_move) { let threshold = QUEEN_WORTH + @@ -435,7 +440,10 @@ impl Bot { } } - board.make_move(m); + if !board.make_move(m) { + continue; + } + let evaluation = -self.quiescence_search(board, -beta, -alpha); board.undo_last_move(); diff --git a/src/main.rs b/src/main.rs index 7507e5f..101c050 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,11 @@ /* TODO +overhaul CLI / UCI interface: + add UCI option for TT size + error handling + "go" and "go infinite" commands + +rewrite TT to use % instead of aging + big idea: remove all constant variables, and put them into the BotConfig struct, then write my own tuning / matchmaking program that will tweak the values diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 8920cbc..166772f 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -44,25 +44,18 @@ impl MoveSorter { } } - pub fn sort_moves(&mut self, board: &mut Board, moves: Vec, hash_move: MoveData, depth: usize) -> Vec { + pub fn sort_moves(&mut self, board: &mut Board, moves: Vec, hash_move: MoveData, depth: usize) -> Vec<(i32, MoveData)> { if moves.is_empty() { return vec![]; } - let num_of_moves = moves.len(); - let mut scores = vec![(0, 0); num_of_moves]; + let mut scores = vec![]; let squares_opponent_attacks = board.get_attacked_squares_for_color((!board.white_to_move) as usize); - for i in 0..num_of_moves { - let m = moves[i]; - + for m in moves { let mut score = 0; - // if depth < MAX_SORT_MOVE_PLY - // && m == self.pv_table.get_pv_move(depth) { - // score = i32::MAX; - // } else if m == hash_move { score = i32::MAX; } else { @@ -92,16 +85,10 @@ impl MoveSorter { } } - scores[i] = (score, i); + scores.push((score, m)); } scores.sort_by(|a, b| b.0.cmp(&a.0)); - - let mut ordered = vec![NULL_MOVE; num_of_moves]; - for i in 0..num_of_moves { - ordered[i] = moves[scores[i].1]; - } - - ordered + scores } } \ No newline at end of file diff --git a/src/perft.rs b/src/perft.rs index f1ddb09..46e5f53 100644 --- a/src/perft.rs +++ b/src/perft.rs @@ -43,7 +43,7 @@ fn perft(board: &mut Board, results: &mut PerftResults, depth_left: u8, depth: u return; } - for data in board.get_legal_moves_for_color(board.white_to_move, false) { + for data in board.get_pseudo_legal_moves_for_color(board.white_to_move, false) { let position_count_before_move = results.positions; if data.capture != NO_PIECE as u8 { @@ -61,7 +61,9 @@ fn perft(board: &mut Board, results: &mut PerftResults, depth_left: u8, depth: u results.promotions += 1; } - board.make_move(data); + if !board.make_move(data) { + continue; + } if board.king_in_check(board.white_to_move) { results.checks += 1; diff --git a/src/value_holder.rs b/src/value_holder.rs index d769b4f..1c6b5a6 100644 --- a/src/value_holder.rs +++ b/src/value_holder.rs @@ -1,3 +1,4 @@ +#[derive(Clone)] pub struct ValueHolder { pub current: T, pub index: usize, From 8c07c5bc0425f0ec892b3195155f841558570824 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sat, 20 Jan 2024 20:23:01 -0600 Subject: [PATCH 20/47] 1/20/24 8:23 PM !!! DISCLAIMER !!! This commit is actually slightly worse than the last one (I think due to the new TT) Rewrote TT to use a fixed size array instead of a growing hashmap Added UCI option for hash size "go" and "go infinite" are now valid UCI commands Fixed move flags MoveData can now be (partially) converted to and from binary --- README.md | 2 +- src/bot.rs | 73 ++++++++++--------- src/main.rs | 54 +++++++++----- src/move_data.rs | 47 ++++++++++-- src/pv_table.rs | 2 + src/transposition_table.rs | 142 +++++++++++++++++++++---------------- src/utils.rs | 2 +- 7 files changed, 195 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 8c16612..3aa2d56 100644 --- a/README.md +++ b/README.md @@ -72,4 +72,4 @@ - [LMR Implementation](https://web.archive.org/web/20150212051846/http://www.glaurungchess.com/lmr.html) - [Mediocre Chess](https://mediocrechess.blogspot.com/) - [Tcheran](https://github.com/jgilchrist/tcheran/) - - [Rustic Chess Engine MVV-LVA](https://rustic-chess.org/search/ordering/mvv_lva.html) \ No newline at end of file + - [Rustic (Engine and Book)](https://github.com/mvanthoor/rustic) \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 778f475..fce6c83 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -1,10 +1,10 @@ use crate::STARTING_FEN; use crate::piece_square_tables::{PAWN_WORTH, QUEEN_WORTH}; use crate::pieces::{PAWN, PROMOTABLE, NO_PIECE}; -use crate::utils::{CHECKMATE_EVAL, evaluation_is_mate, moves_ply_from_mate}; +use crate::utils::{CHECKMATE_EVAL, evaluation_is_mate, ply_from_mate}; use std::time::Instant; use crate::move_sorter::MoveSorter; -use crate::transposition_table::{TranspositionTable, NodeType}; +use crate::transposition_table::{TranspositionTable, EvalBound}; use crate::move_data::{MoveData, NULL_MOVE}; use crate::opening_book::OpeningBook; use crate::Board; @@ -23,6 +23,7 @@ pub struct BotConfig { pub debug_output: bool, pub opening_book: bool, pub time_management: bool, + pub tt_size_in_mb: usize, } impl BotConfig { @@ -35,6 +36,7 @@ impl BotConfig { debug_output: Self::get_arg_value(&args, "debug_output").unwrap_or(_true.clone()) == _true, opening_book: Self::get_arg_value(&args, "opening_book").unwrap_or(_false.clone()) == _true, time_management: Self::get_arg_value(&args, "time_management").unwrap_or(_true.clone()) == _true, + tt_size_in_mb: (Self::get_arg_value(&args, "tt_size").unwrap_or("256".to_string())).parse::().unwrap_or(256), } } @@ -65,22 +67,18 @@ pub struct Bot { pub best_move: MoveData, best_move_this_iteration: MoveData, - best_move_depth_searched_at: u8, evaluation: i32, evaluation_this_iteration: i32, positions_searched: u128, quiescence_searched: u128, - transposition_hits: u128, } impl Bot { pub fn new(config: BotConfig) -> Self { - let in_opening_book = config.opening_book; - Self { - config, + config: config.clone(), time_to_think: 0.0, think_timer: Instant::now(), @@ -88,21 +86,19 @@ impl Bot { searched_one_move: false, opening_book: OpeningBook::create(), - in_opening_book, + in_opening_book: config.opening_book, move_sorter: MoveSorter::new(), - transposition_table: TranspositionTable::empty(), + transposition_table: TranspositionTable::empty(config.tt_size_in_mb), best_move: NULL_MOVE, best_move_this_iteration: NULL_MOVE, - best_move_depth_searched_at: 0, evaluation: 0, evaluation_this_iteration: 0, positions_searched: 0, quiescence_searched: 0, - transposition_hits: 0, } } @@ -146,7 +142,7 @@ impl Bot { self.positions_searched = 0; self.quiescence_searched = 0; - self.transposition_hits = 0; + self.transposition_table.hits = 0; self.move_sorter.clear(); @@ -158,7 +154,7 @@ impl Bot { self.best_move_this_iteration = NULL_MOVE; self.evaluation_this_iteration = 0; - + // TODO: work on aspiration windows loop { let (alpha, beta) = (last_evaluation - window, last_evaluation + window); @@ -171,7 +167,6 @@ impl Bot { window *= 4; } - if !self.search_cancelled || self.searched_one_move { self.best_move = self.best_move_this_iteration; @@ -186,11 +181,11 @@ impl Bot { self.positions_searched, self.quiescence_searched, self.positions_searched + self.quiescence_searched, - self.transposition_hits, + self.transposition_table.hits, )); if evaluation_is_mate(self.evaluation) { - let moves_until_mate = moves_ply_from_mate(self.evaluation); + let moves_until_mate = ply_from_mate(self.evaluation); if moves_until_mate <= depth { self.println(format!("Mate found in {}", (moves_until_mate as f32 * 0.5).ceil())); break; @@ -205,7 +200,6 @@ impl Bot { self.println(format!("{} seconds", self.think_timer.elapsed().as_secs_f32())); - self.transposition_table.update(); if self.config.debug_output { self.transposition_table.print_size(); } @@ -240,27 +234,30 @@ impl Bot { // Mate Distance Pruning let alpha = i32::max(alpha, -CHECKMATE_EVAL + depth as i32); - if alpha >= i32::min(beta, CHECKMATE_EVAL - depth as i32) { + let beta = i32::min(beta, CHECKMATE_EVAL - depth as i32); + if alpha >= beta { return alpha; } } - if let Some(data) = self.transposition_table.lookup(board.zobrist.key, depth_left, depth, alpha, beta) { - self.transposition_hits += 1; + let (tt_eval, hash_move) = self.transposition_table.lookup(board.zobrist.key, depth_left, depth, alpha, beta); - if depth == 0 { - self.best_move_this_iteration = data.best_move; - self.best_move_depth_searched_at = data.depth_left; - self.evaluation_this_iteration = data.evaluation; + // We can't return on depth 0, because if a hash collision occurs, it might return an illegal move + if depth > 0 { + if let Some(tt_eval) = tt_eval { + return tt_eval; } - - return data.evaluation; + // else if depth_left > 3 { + // // Internal Iterative Reductions + // depth_left -= 1; + // } } + // This detects a null / zero window search, which is used in non PV nodes + // This will also never be true if depth == 0 because the bounds will never be zero let not_pv = alpha == beta - 1; if not_pv - && depth > 0 && depth_left > 0 && board.get_last_move().capture == NO_PIECE as u8 // ? && !board.king_in_check(board.white_to_move) { @@ -300,21 +297,22 @@ impl Bot { } let mut best_move_this_search = NULL_MOVE; - let mut node_type = NodeType::UpperBound; + let mut eval_bound = EvalBound::UpperBound; let moves = board.get_pseudo_legal_moves_for_color(board.white_to_move, false); let sorted_moves = self.move_sorter.sort_moves( board, moves, + /* + The best move is _not_ the same as the hash move, because we could have + found a new best move right before exiting the search, before tt.store gets called + */ if depth == 0 && self.best_move != NULL_MOVE { self.best_move } else { - match self.transposition_table.table.get(&board.zobrist.key) { - Some(data) => data.best_move, - None => NULL_MOVE, - } + hash_move.unwrap_or(NULL_MOVE) }, depth as usize, ); @@ -339,7 +337,7 @@ impl Bot { } } - // Late Move Reduction / (Kind of) Principal Variation Search + // Late Move Reduction let mut evaluation = 0; let mut needs_full_search = true; @@ -363,7 +361,7 @@ impl Bot { } if evaluation >= beta { - self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, NodeType::LowerBound); + self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, EvalBound::LowerBound); if m.capture == NO_PIECE as u8 { self.move_sorter.push_killer_move(m, depth as usize); @@ -375,13 +373,12 @@ impl Bot { if evaluation > alpha { best_move_this_search = m; - node_type = NodeType::Exact; + eval_bound = EvalBound::Exact; alpha = evaluation; if depth == 0 { self.searched_one_move = true; self.best_move_this_iteration = best_move_this_search; - self.best_move_depth_searched_at = depth_left; self.evaluation_this_iteration = evaluation; } } @@ -397,7 +394,9 @@ impl Bot { return 0; } - self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type); + if best_move_this_search != NULL_MOVE { + self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, eval_bound); + } alpha } diff --git a/src/main.rs b/src/main.rs index 101c050..2a4dc53 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,10 +1,5 @@ /* TODO -overhaul CLI / UCI interface: - add UCI option for TT size - error handling - "go" and "go infinite" commands - -rewrite TT to use % instead of aging +overhaul CLI / UCI interface with proper error handling big idea: remove all constant variables, and put them into the BotConfig struct, @@ -76,7 +71,7 @@ pub const ENDGAME_POSITION: &str = "8/pk4p1/2prp3/3p1p2/3P2p1/R2BP3/2P2KPP/8 w pub const PAWN_EVAL_TESTING: &str = "4k3/p1pp4/8/4pp1P/2P4P/8/P5P1/4K3 w - - 0 1"; fn main() { - let bot_config = BotConfig::from_args(std::env::args().collect::>()); + let mut bot_config = BotConfig::from_args(std::env::args().collect::>()); // let mut log = Log::none(); @@ -105,10 +100,24 @@ fn main() { "uci" => { println!("id name Maxwell v3.1.0"); println!("id author eboatwright"); + println!("option name Hash type spin default 256 min 0 max 4000"); println!("uciok"); } + "setoption" => { + // setoption name Hash value 32 + // I KNOW this is horrible I'm gonna rewrite all this eventually + + match command_split[2] { + "Hash" => { + bot_config.tt_size_in_mb = command_split[4].parse::().unwrap_or(256); + } + + _ => {} + } + } + "isready" => println!("readyok"), "ucinewgame" => { @@ -117,6 +126,7 @@ fn main() { bot = Bot::new(bot_config.clone()); } + // TODO: allow "position fen" // Format: position startpos (moves e2e4 e7e5 ...) "position" => { for _ in 0..board.moves.len() { @@ -137,24 +147,30 @@ fn main() { } // Format: - // go (movetime, wtime) X (btime Y) + // go (infinite) // go depth X + // go (movetime, wtime) X (btime Y) "go" => { let my_time_label = if board.white_to_move { "wtime" } else { "btime" }; let mut my_time = 0.0; let mut depth_to_search = 255 - MAX_SEARCH_EXTENSIONS; - for i in [1, 3] { - if command_split[i] == my_time_label - || command_split[i] == "movetime" { - if let Ok(time_in_millis) = command_split[i + 1].parse::() { - my_time = time_in_millis as f32 / 1000.0; - break; - } - } else if command_split[i] == "depth" { - if let Ok(_depth_to_search) = command_split[i + 1].parse::() { - depth_to_search = _depth_to_search; - break; + if command_split.len() > 2 { + for i in [1, 3] { + if command_split[i] == my_time_label + || command_split[i] == "movetime" { + if let Ok(time_in_millis) = command_split[i + 1].parse::() { + // This is capped at 1 millisecond, because if my_time is 0 + // it will just ignore the time limit and calculate. + // (And also because sometimes Cutechess gives negative time for some reason) + my_time = i32::max(1, time_in_millis) as f32 / 1000.0; + break; + } + } else if command_split[i] == "depth" { + if let Ok(_depth_to_search) = command_split[i + 1].parse::() { + depth_to_search = _depth_to_search; + break; + } } } } diff --git a/src/move_data.rs b/src/move_data.rs index 2cede86..32d7252 100644 --- a/src/move_data.rs +++ b/src/move_data.rs @@ -2,11 +2,11 @@ use crate::pieces::{char_to_piece, get_piece_type}; use crate::pieces::{PROMOTABLE, piece_to_char, NO_PIECE}; use crate::utils::{coordinate_to_index, index_to_coordinate}; -// 1, 2, 3, 4, 7, 8, 9, 10 for promoting pieces -pub const DOUBLE_PAWN_PUSH_FLAG: u8 = 11; -pub const EN_PASSANT_FLAG: u8 = 12; -pub const SHORT_CASTLE_FLAG: u8 = 13; -pub const LONG_CASTLE_FLAG: u8 = 14; +// 1, 2, 3, 4 for promoting pieces +pub const DOUBLE_PAWN_PUSH_FLAG: u8 = 5; +pub const EN_PASSANT_FLAG: u8 = 6; +pub const SHORT_CASTLE_FLAG: u8 = 7; +pub const LONG_CASTLE_FLAG: u8 = 8; pub const NULL_MOVE: MoveData = MoveData { flag: 0, @@ -16,7 +16,7 @@ pub const NULL_MOVE: MoveData = MoveData { to: 0, }; -#[derive(Copy, Clone, PartialEq, Debug)] +#[derive(Copy, Clone, Debug)] pub struct MoveData { pub flag: u8, pub capture: u8, @@ -29,6 +29,14 @@ impl Default for MoveData { fn default() -> Self { NULL_MOVE } } +impl PartialEq for MoveData { + fn eq(&self, other: &MoveData) -> bool { + self.flag == other.flag + && self.from == other.from + && self.to == other.to + } +} + impl MoveData { pub fn from_coordinates(coordinates: String) -> Self { Self { @@ -37,7 +45,7 @@ impl MoveData { } else { 0 }, - capture: 0, + capture: NO_PIECE as u8, piece: NO_PIECE as u8, from: coordinate_to_index(&coordinates[0..2]), to: coordinate_to_index(&coordinates[2..4]), @@ -57,4 +65,29 @@ impl MoveData { promotion, ) } + + /* + flag from to + 0000 000000 000000 + */ + + pub fn from_binary(binary: u16) -> MoveData { + MoveData { + flag: ((binary & 0b_1111_000000_000000) >> 12) as u8, + capture: NO_PIECE as u8, + piece: NO_PIECE as u8, + from: ((binary & 0b_0000_111111_000000) >> 6) as u8, + to: (binary & 0b_0000_000000_111111) as u8, + } + } + + pub fn to_binary(&self) -> u16 { + let mut result: u16 = 0b_0000_000000_000000; + + result |= (self.flag as u16) << 12; + result |= (self.from as u16) << 6; + result |= self.to as u16; + + result + } } \ No newline at end of file diff --git a/src/pv_table.rs b/src/pv_table.rs index fe0ae41..8bdbc7e 100644 --- a/src/pv_table.rs +++ b/src/pv_table.rs @@ -1,3 +1,5 @@ +// CURRENTLY NOT BEING USED + use crate::move_data::{MoveData, NULL_MOVE}; use crate::move_sorter::MAX_SORT_MOVE_PLY; diff --git a/src/transposition_table.rs b/src/transposition_table.rs index 2f10bfc..e91dfef 100644 --- a/src/transposition_table.rs +++ b/src/transposition_table.rs @@ -1,11 +1,17 @@ +/* +This TT implementation is a mashup of Rustic, Coding Adventure Bot and Weiawaga +Thanks for the inspiration! +*/ + use crate::utils::evaluation_is_mate; -use crate::MoveData; -use std::collections::HashMap; +use crate::move_data::MoveData; +use std::mem::size_of; -pub const ENTRY_SIZE: usize = std::mem::size_of::() + std::mem::size_of::(); +pub const MEGABYTE: usize = 1024 * 1024; +pub const ENTRY_SIZE: usize = size_of::>(); #[derive(Copy, Clone, PartialEq, Debug)] -pub enum NodeType { +pub enum EvalBound { UpperBound, LowerBound, Exact, @@ -13,96 +19,108 @@ pub enum NodeType { #[derive(Copy, Clone)] pub struct TranspositionData { + pub key: u64, pub depth_left: u8, pub evaluation: i32, - pub best_move: MoveData, - pub age: u8, - pub node_type: NodeType, + pub best_move: u16, + pub eval_bound: EvalBound, } pub struct TranspositionTable { - pub table: HashMap, + size_in_mb: usize, + entry_count: usize, + length: usize, + pub table: Vec>, + + pub hits: u128, } impl TranspositionTable { - pub fn empty() -> Self { + pub fn empty(size_in_mb: usize) -> Self { + let length = (size_in_mb * MEGABYTE) / ENTRY_SIZE; + Self { - table: HashMap::new(), + size_in_mb, + entry_count: 0, + length, + table: vec![None; length], + + hits: 0, } } - pub fn store(&mut self, key: u64, depth_left: u8, depth: u8, evaluation: i32, best_move: MoveData, node_type: NodeType) { + pub fn get_index(&self, key: u64) -> usize { (key as usize) % self.length } + + pub fn store(&mut self, key: u64, depth_left: u8, depth: u8, evaluation: i32, best_move: MoveData, eval_bound: EvalBound) { + if self.length == 0 { + return; + } + let mut fixed_mate_evaluation = evaluation; if evaluation_is_mate(evaluation) { let sign = if evaluation > 0 { 1 } else { -1 }; fixed_mate_evaluation = (evaluation * sign + depth as i32) * sign; } - self.table.insert(key, - TranspositionData { - depth_left, - evaluation: fixed_mate_evaluation, - best_move, - age: 0, - node_type, - } - ); - } + let index = self.get_index(key); + + // if self.table[index].is_none() { + // self.entry_count += 1; + // } - pub fn lookup(&mut self, key: u64, depth_left: u8, depth: u8, alpha: i32, beta: i32) -> Option { - if let Some(data) = self.table.get_mut(&key) { - if data.depth_left >= depth_left { - let mut fixed_mate_evaluation = data.evaluation; - if evaluation_is_mate(data.evaluation) { - let sign = if data.evaluation > 0 { 1 } else { -1 }; - fixed_mate_evaluation = (data.evaluation * sign - depth as i32) * sign; + self.table[index] = + Some( + TranspositionData { + key, + depth_left, + evaluation: fixed_mate_evaluation, + best_move: best_move.to_binary(), + eval_bound, } + ); + } + + pub fn lookup(&mut self, key: u64, depth_left: u8, depth: u8, alpha: i32, beta: i32) -> (Option, Option) { + if self.length == 0 { + return (None, None); + } + if let Some(data) = self.table[self.get_index(key)] { + if data.key == key { + self.hits += 1; + let mut return_evaluation = None; - match data.node_type { - NodeType::UpperBound => { - if fixed_mate_evaluation <= alpha { - data.age = 0; - return Some(TranspositionData { - evaluation: fixed_mate_evaluation, - ..*data - }); - } - } + if data.depth_left >= depth_left { + let mut fixed_mate_evaluation = data.evaluation; - NodeType::LowerBound => { - if fixed_mate_evaluation >= beta { - data.age = 0; - return Some(TranspositionData { - evaluation: fixed_mate_evaluation, - ..*data - }); - } + if evaluation_is_mate(data.evaluation) { + let sign = if data.evaluation > 0 { 1 } else { -1 }; + fixed_mate_evaluation = (data.evaluation * sign - depth as i32) * sign; } - NodeType::Exact => { - data.age = 0; - return Some(TranspositionData { - evaluation: fixed_mate_evaluation, - ..*data - }); + match data.eval_bound { + EvalBound::LowerBound => + if fixed_mate_evaluation >= beta { + return_evaluation = Some(fixed_mate_evaluation); + }, + EvalBound::UpperBound => + if fixed_mate_evaluation <= alpha { + return_evaluation = Some(fixed_mate_evaluation); + }, + EvalBound::Exact => + return_evaluation = Some(fixed_mate_evaluation), } } + + return (return_evaluation, Some(MoveData::from_binary(data.best_move))); } } - None - } - pub fn update(&mut self) { - self.table.retain(|_, data| { - data.age += 1; - data.age <= 10 - }); + (None, None) } pub fn print_size(&self) { - let length = (self.table.len() * ENTRY_SIZE) as f32 / 1_000_000.0; - let capacity = (self.table.capacity() * ENTRY_SIZE) as f32 / 1_000_000.0; - println!("Transposition table size: {} MB / {} MB", length, capacity); + let size = (self.entry_count * ENTRY_SIZE) as f32 / MEGABYTE as f32; + println!("Transposition table size: {} MB / {} MB", size, self.size_in_mb); } } \ No newline at end of file diff --git a/src/utils.rs b/src/utils.rs index 43d475f..4403c2c 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -49,7 +49,7 @@ pub fn evaluation_is_mate(evaluation: i32) -> bool { evaluation.abs() > CHECKMATE_EVAL - 100 } -pub fn moves_ply_from_mate(evaluation: i32) -> u8 { +pub fn ply_from_mate(evaluation: i32) -> u8 { (CHECKMATE_EVAL - evaluation.abs()) as u8 } From 168f6cf3526a25b6d6e09d0b25596a1a7be1c862 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:16:35 -0600 Subject: [PATCH 21/47] 1/23/24 10:16 AM Updated README Added TODOs everywhere Renamed depth_left to depth, and depth to ply Rewrote TT to use a HashMap again Also added a replacement scheme to the TT: it prefers any entry with a higher depth than another, and if the depths are equal it prefers an exact eval bound --- README.md | 11 ++-- Test results.txt | 14 ++++- src/board.rs | 5 +- src/bot.rs | 104 +++++++++++++++++++------------------ src/main.rs | 4 +- src/move_sorter.rs | 12 ++--- src/perft.rs | 8 +-- src/pv_table.rs | 22 ++++---- src/transposition_table.rs | 89 ++++++++++++++++--------------- 9 files changed, 142 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 3aa2d56..8d7d994 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,10 @@ A Chess engine written from scratch in Rust.
If you use this code verbatim, or as a reference, please credit me!

- CCRL Blitz ratings - | Date | Version | Rating | Rank | - | --- | --- | --- | --- | - | 1/14/24 | v3.0.8-1 | 1830 | 538 / 715 | -

- [Play against Maxwell on Lichess!](https://lichess.org/@/MaxwellOnLC) | [Some of Maxwell's Games](https://www.chess.com/library/collections/maxwells-games-my-chess-engine-2FFU82NM4) + [Play against Maxwell on Lichess!](https://lichess.org/@/MaxwellOnLC) | [Some of Maxwell's Games](https://www.chess.com/library/collections/maxwells-games-my-chess-engine-2FFU82NM4) | [Maxwell's CCRL](https://computerchess.org.uk/ccrl/404/cgi/engine_details.cgi?print=Details&each_game=1&eng=Maxwell%203.0.8-1%2064-bit#Maxwell_3_0_8-1_64-bit) -## Features +## Features - NOT UP TO DATE WITH DEV BRANCH #### Parameters - fen=\: Sets up the board by a fen string (Doesn't work for UCI games) (default=STARTING_FEN) - debug=\: Toggle debug output that gets outputed per ply (default=true) @@ -56,7 +51,7 @@ - Checks - Time management - If less than 7 moves have been played, it uses 2.5% of it's remaining time, otherwise 7% - - This value is then also clamped between 0.25 and 20.0 seconds + - This value is then also clamped between 0.2 and 20.0 seconds ## Helpful Sources & References - [Sebastian Lague's Chess Programming series](https://www.youtube.com/playlist?list=PLFt_AvWsXl0cvHyu32ajwh2qU1i6hl77c) diff --git a/Test results.txt b/Test results.txt index 4c0cf20..4a0c405 100644 --- a/Test results.txt +++ b/Test results.txt @@ -1,7 +1,19 @@ REWRITE v1 vs v3.0.8 +17, -14, =19 + Elo +/- Games Score Draw 1 Re-write v2 63 58 100 59.0% 30.0% 2 Re-write v1 -7 58 100 49.0% 28.0% -3 Maxwell v3.0.8 -56 56 100 42.0% 34.0% \ No newline at end of file +3 Maxwell v3.0.8 -56 56 100 42.0% 34.0% + + +This is for when to return a TT evaluation +Score of Re-write v3 (Not root) vs Re-write v3 (Not PV): 131 - 60 - 109 +Score of Re-write v3 (Not root) vs Re-write v3 (All): 33 - 33 - 36 +Score of Re-write v3 (Depth replace) vs Re-write v3 (Always replace): 74 - 60 - 66 + + Elo +/- Games Points Draw +1 Re-write v3 (Higher depth, exact bound) 37 37 200 110.5 40.5% +2 Re-write v3 (Higher depth, equals key) -7 37 200 98.0 40.0% +3 Re-write v3 (Higher depth) -30 38 200 91.5 39.5% \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index 32130fd..e5b44d8 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1009,9 +1009,10 @@ impl Board { } pub fn try_null_move(&mut self) -> bool { + // TODO if self.get_last_move() == NULL_MOVE - || self.king_in_check(self.white_to_move) - || self.king_in_check(!self.white_to_move) { + || self.king_in_check(self.white_to_move) // Maybe only check this before calling this function instead of inside the function? + || self.king_in_check(!self.white_to_move) { // ? return false; } diff --git a/src/bot.rs b/src/bot.rs index fce6c83..7818a90 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -11,12 +11,6 @@ use crate::Board; pub const MAX_SEARCH_EXTENSIONS: u8 = 16; // TODO -pub const PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES: f32 = 0.025; // 2.5% -pub const PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES: f32 = 0.07; // 7% - -pub const MIN_TIME_PER_MOVE: f32 = 0.25; // seconds -pub const MAX_TIME_PER_MOVE: f32 = 20.0; - #[derive(Clone, Debug)] pub struct BotConfig { pub fen: String, @@ -108,7 +102,7 @@ impl Bot { } } - pub fn start(&mut self, board: &mut Board, moves: String, my_time: f32, depth_to_search: u8) { + pub fn start(&mut self, board: &mut Board, moves: String, my_time: f32, depth: u8) { if self.in_opening_book { let opening_move = self.opening_book.get_opening_move(moves); if opening_move == NULL_MOVE { @@ -123,12 +117,12 @@ impl Bot { if self.config.time_management && my_time > 0.0 { let time_percentage = if board.moves.len() / 2 <= 6 { - PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES + 0.025 } else { - PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES + 0.07 }; - (my_time * time_percentage).clamp(MIN_TIME_PER_MOVE, MAX_TIME_PER_MOVE) + (my_time * time_percentage).clamp(0.2, 20.0) } else { my_time }; @@ -149,7 +143,7 @@ impl Bot { let mut window = 40; self.think_timer = Instant::now(); - for depth in 1..=depth_to_search { + for current_depth in 1..=depth { self.searched_one_move = false; self.best_move_this_iteration = NULL_MOVE; self.evaluation_this_iteration = 0; @@ -158,7 +152,7 @@ impl Bot { loop { let (alpha, beta) = (last_evaluation - window, last_evaluation + window); - let evaluation = self.alpha_beta_search(board, 0, depth, alpha, beta, 0); + let evaluation = self.alpha_beta_search(board, current_depth, 0, alpha, beta, 0); if alpha < evaluation && evaluation < beta { break; @@ -174,7 +168,7 @@ impl Bot { } self.println(format!("Depth: {}, Window: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}", - depth, + current_depth, window, self.evaluation * board.perspective(), self.best_move.to_coordinates(), @@ -186,7 +180,7 @@ impl Bot { if evaluation_is_mate(self.evaluation) { let moves_until_mate = ply_from_mate(self.evaluation); - if moves_until_mate <= depth { + if moves_until_mate <= current_depth { self.println(format!("Mate found in {}", (moves_until_mate as f32 * 0.5).ceil())); break; } @@ -213,19 +207,20 @@ impl Bot { fn alpha_beta_search( &mut self, board: &mut Board, - depth: u8, - mut depth_left: u8, + mut depth: u8, + ply: u8, mut alpha: i32, beta: i32, total_extensions: u8, ) -> i32 { + // TODO: try moving this into the move loop and break instead of return 0? if self.should_cancel_search() { return 0; } self.positions_searched += 1; - if depth > 0 { + if ply > 0 { if board.is_draw() { // Should I use this to discourage making a draw in a winning position? // return -self.quiescence_search(board, alpha, beta); @@ -233,32 +228,33 @@ impl Bot { } // Mate Distance Pruning - let alpha = i32::max(alpha, -CHECKMATE_EVAL + depth as i32); - let beta = i32::min(beta, CHECKMATE_EVAL - depth as i32); + let mate_value = CHECKMATE_EVAL - ply as i32; + let alpha = i32::max(alpha, -CHECKMATE_EVAL + ply as i32); + let beta = i32::min(beta, CHECKMATE_EVAL - ply as i32); if alpha >= beta { return alpha; } } - let (tt_eval, hash_move) = self.transposition_table.lookup(board.zobrist.key, depth_left, depth, alpha, beta); + let (tt_eval, hash_move) = self.transposition_table.lookup(board.zobrist.key, ply, depth, alpha, beta); - // We can't return on depth 0, because if a hash collision occurs, it might return an illegal move - if depth > 0 { + // We don't really want to return from the root node, because if a hash collision occurs (although very rare) + // It will return an illegal move + if ply > 0 { if let Some(tt_eval) = tt_eval { return tt_eval; } - // else if depth_left > 3 { - // // Internal Iterative Reductions - // depth_left -= 1; - // } } // This detects a null / zero window search, which is used in non PV nodes - // This will also never be true if depth == 0 because the bounds will never be zero + // This will also never be true if ply == 0 because the bounds will never be zero at ply 0 let not_pv = alpha == beta - 1; + // TODO: Maybe allow these pruning techniques during PV nodes? + // Or maybe just make them more aggressive to allow for more search + // time on PV lines? if not_pv - && depth_left > 0 + && depth > 0 && board.get_last_move().capture == NO_PIECE as u8 // ? && !board.king_in_check(board.white_to_move) { // TODO: move these around @@ -266,11 +262,11 @@ impl Bot { let static_eval = board.evaluate(); // Null Move Pruning - if depth_left > 2 + if depth > 2 && static_eval >= beta - // && board.total_material_without_pawns > 0 + // && board.total_material_without_pawns > 0 // TODO && board.try_null_move() { - let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, total_extensions); + let evaluation = -self.alpha_beta_search(board, depth - 3, ply + 1, -beta, -beta + 1, total_extensions); board.undo_null_move(); @@ -280,19 +276,19 @@ impl Bot { } // Reverse Futility Pruning - if depth_left < 5 - && static_eval - 60 * (depth_left as i32) >= beta { // TODO: tweak this threshold + if depth < 5 + && static_eval - 60 * (depth as i32) >= beta { // TODO: tweak this threshold return static_eval; } // Razoring - if depth_left < 4 - && static_eval + 300 * (depth_left as i32) < alpha { - depth_left -= 1; + if depth < 4 + && static_eval + 300 * (depth as i32) < alpha { + depth -= 1; } } - if depth_left == 0 { + if depth == 0 { return self.quiescence_search(board, alpha, beta); } @@ -308,13 +304,13 @@ impl Bot { The best move is _not_ the same as the hash move, because we could have found a new best move right before exiting the search, before tt.store gets called */ - if depth == 0 + if ply == 0 && self.best_move != NULL_MOVE { self.best_move } else { hash_move.unwrap_or(NULL_MOVE) }, - depth as usize, + ply as usize, ); let mut i = 0; @@ -327,7 +323,7 @@ impl Bot { if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { if board.king_in_check(board.white_to_move) { extension = 1; - } else { + } else { // TODO: does this help at all? Or maybe try checking for a promotion flag if m.piece == PAWN as u8 { let rank = m.to / 8; if rank == 1 || rank == 6 { @@ -337,21 +333,27 @@ impl Bot { } } - // Late Move Reduction + // Late Move Reductions let mut evaluation = 0; let mut needs_full_search = true; if i > 2 - && depth_left > 2 + && depth > 2 && extension == 0 && m.capture == NO_PIECE as u8 { - // let reduction = 1; - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 2, -alpha - 1, -alpha, total_extensions); + // let mut reduction = 1; + + // if hash_move.is_none() { + // // Internal Iterative Reductions + // reduction += 1; + // } + + evaluation = -self.alpha_beta_search(board, depth - 2, ply + 1, -alpha - 1, -alpha, total_extensions); needs_full_search = evaluation > alpha; } if needs_full_search { - evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1 + extension, -beta, -alpha, total_extensions + extension); + evaluation = -self.alpha_beta_search(board, depth - 1 + extension, ply + 1, -beta, -alpha, total_extensions + extension); } board.undo_last_move(); @@ -361,11 +363,11 @@ impl Bot { } if evaluation >= beta { - self.transposition_table.store(board.zobrist.key, depth_left, depth, beta, m, EvalBound::LowerBound); + self.transposition_table.store(board.zobrist.key, depth, ply, beta, m, EvalBound::LowerBound); if m.capture == NO_PIECE as u8 { - self.move_sorter.push_killer_move(m, depth as usize); - self.move_sorter.history[m.piece as usize][m.to as usize] += (depth_left * depth_left) as i32; + self.move_sorter.push_killer_move(m, ply as usize); + self.move_sorter.history[m.piece as usize][m.to as usize] += (depth * depth) as i32; } return beta; @@ -376,7 +378,7 @@ impl Bot { eval_bound = EvalBound::Exact; alpha = evaluation; - if depth == 0 { + if ply == 0 { self.searched_one_move = true; self.best_move_this_iteration = best_move_this_search; self.evaluation_this_iteration = evaluation; @@ -388,14 +390,14 @@ impl Bot { if i == 0 { if board.king_in_check(board.white_to_move) { - let mate_score = CHECKMATE_EVAL - depth as i32; + let mate_score = CHECKMATE_EVAL - ply as i32; return -mate_score; } return 0; } if best_move_this_search != NULL_MOVE { - self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, eval_bound); + self.transposition_table.store(board.zobrist.key, depth, ply, alpha, best_move_this_search, eval_bound); } alpha diff --git a/src/main.rs b/src/main.rs index 2a4dc53..5377671 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ /* TODO overhaul CLI / UCI interface with proper error handling +look into using "buckets" for transposition table big idea: remove all constant variables, and put them into the BotConfig struct, @@ -15,6 +16,7 @@ check out pin detection to speed up check detection? figure out how to implement "pondering" to think on opponent's time Random ideas to try +History reductions https://www.chessprogramming.org/Internal_Iterative_Deepening https://www.chessprogramming.org/Static_Exchange_Evaluation https://www.chessprogramming.org/History_Leaf_Pruning @@ -86,7 +88,7 @@ fn main() { io::stdin() .read_line(&mut command) - .expect("Failed to read command"); + .expect("Failed to read terminal input"); // log.write(format!("Got command: {}\n", command)); diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 166772f..f01e36c 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -38,13 +38,13 @@ impl MoveSorter { self.history = [[0; 64]; PIECE_COUNT]; } - pub fn push_killer_move(&mut self, data: MoveData, depth: usize) { - if depth < MAX_SORT_MOVE_PLY { - self.killer_moves[depth].push(data); + pub fn push_killer_move(&mut self, data: MoveData, ply: usize) { + if ply < MAX_SORT_MOVE_PLY { + self.killer_moves[ply].push(data); } } - pub fn sort_moves(&mut self, board: &mut Board, moves: Vec, hash_move: MoveData, depth: usize) -> Vec<(i32, MoveData)> { + pub fn sort_moves(&mut self, board: &mut Board, moves: Vec, hash_move: MoveData, ply: usize) -> Vec<(i32, MoveData)> { if moves.is_empty() { return vec![]; } @@ -64,8 +64,8 @@ impl MoveSorter { // TODO: static exchange evaluation } else { - if depth < MAX_SORT_MOVE_PLY - && self.killer_moves[depth].is_killer(m) { + if ply < MAX_SORT_MOVE_PLY + && self.killer_moves[ply].is_killer(m) { score += 5000; } diff --git a/src/perft.rs b/src/perft.rs index 46e5f53..0d0d57b 100644 --- a/src/perft.rs +++ b/src/perft.rs @@ -37,8 +37,8 @@ impl PerftResults { } } -fn perft(board: &mut Board, results: &mut PerftResults, depth_left: u8, depth: u8) { - if depth_left == 0 { +fn perft(board: &mut Board, results: &mut PerftResults, depth: u8, ply: u8) { + if depth == 0 { results.positions += 1; return; } @@ -69,10 +69,10 @@ fn perft(board: &mut Board, results: &mut PerftResults, depth_left: u8, depth: u results.checks += 1; } - perft(board, results, depth_left - 1, depth + 1); + perft(board, results, depth - 1, ply + 1); board.undo_last_move(); - if depth == 0 { + if ply == 0 { let positions_this_move = results.positions - position_count_before_move; println!("{}: {}", data.to_coordinates(), positions_this_move); } diff --git a/src/pv_table.rs b/src/pv_table.rs index 8bdbc7e..68daec3 100644 --- a/src/pv_table.rs +++ b/src/pv_table.rs @@ -30,21 +30,21 @@ impl PVTable { println!(""); } - pub fn set_pv_length(&mut self, depth: usize) { - if depth < MAX_SORT_MOVE_PLY { - self.length[depth] = depth; + pub fn set_pv_length(&mut self, ply: usize) { + if ply < MAX_SORT_MOVE_PLY { + self.length[ply] = ply; } } - pub fn push_pv_move(&mut self, data: MoveData, depth: usize) { - if depth + 1 < MAX_SORT_MOVE_PLY { - self.table[depth][depth] = data; + pub fn push_pv_move(&mut self, data: MoveData, ply: usize) { + if ply + 1 < MAX_SORT_MOVE_PLY { + self.table[ply][ply] = data; - for next_depth in (depth + 1)..self.length[depth + 1] { - self.table[depth][next_depth] = self.table[depth + 1][next_depth]; + for next_depth in (ply + 1)..self.length[ply + 1] { + self.table[ply][next_depth] = self.table[ply + 1][next_depth]; } - self.length[depth] = self.length[depth + 1]; + self.length[ply] = self.length[ply + 1]; } } @@ -54,7 +54,7 @@ impl PVTable { self.length[0] -= 1; } - pub fn get_pv_move(&self, depth: usize) -> MoveData { - self.table[depth][depth] + pub fn get_pv_move(&self, ply: usize) -> MoveData { + self.table[ply][ply] } } \ No newline at end of file diff --git a/src/transposition_table.rs b/src/transposition_table.rs index e91dfef..ad08a27 100644 --- a/src/transposition_table.rs +++ b/src/transposition_table.rs @@ -3,12 +3,13 @@ This TT implementation is a mashup of Rustic, Coding Adventure Bot and Weiawaga Thanks for the inspiration! */ +use std::collections::HashMap; use crate::utils::evaluation_is_mate; use crate::move_data::MoveData; use std::mem::size_of; pub const MEGABYTE: usize = 1024 * 1024; -pub const ENTRY_SIZE: usize = size_of::>(); +pub const ENTRY_SIZE: usize = size_of::() + size_of::(); #[derive(Copy, Clone, PartialEq, Debug)] pub enum EvalBound { @@ -20,17 +21,17 @@ pub enum EvalBound { #[derive(Copy, Clone)] pub struct TranspositionData { pub key: u64, - pub depth_left: u8, + pub depth: u8, pub evaluation: i32, pub best_move: u16, pub eval_bound: EvalBound, + // pub age: u8, } pub struct TranspositionTable { size_in_mb: usize, - entry_count: usize, length: usize, - pub table: Vec>, + pub table: HashMap, pub hits: u128, } @@ -41,17 +42,16 @@ impl TranspositionTable { Self { size_in_mb, - entry_count: 0, length, - table: vec![None; length], + table: HashMap::with_capacity(length), hits: 0, } } - pub fn get_index(&self, key: u64) -> usize { (key as usize) % self.length } + pub fn get_index(&self, key: u64) -> u32 { (key % (self.length as u64)) as u32 } - pub fn store(&mut self, key: u64, depth_left: u8, depth: u8, evaluation: i32, best_move: MoveData, eval_bound: EvalBound) { + pub fn store(&mut self, key: u64, depth: u8, ply: u8, evaluation: i32, best_move: MoveData, eval_bound: EvalBound) { if self.length == 0 { return; } @@ -59,56 +59,59 @@ impl TranspositionTable { let mut fixed_mate_evaluation = evaluation; if evaluation_is_mate(evaluation) { let sign = if evaluation > 0 { 1 } else { -1 }; - fixed_mate_evaluation = (evaluation * sign + depth as i32) * sign; + fixed_mate_evaluation = (evaluation * sign + ply as i32) * sign; } let index = self.get_index(key); - // if self.table[index].is_none() { - // self.entry_count += 1; - // } - - self.table[index] = - Some( - TranspositionData { - key, - depth_left, - evaluation: fixed_mate_evaluation, - best_move: best_move.to_binary(), - eval_bound, - } - ); - } + if let Some(data) = self.table.get(&index) { + // If we already have a deeper depth, then we don't care about the new + // data so just return + if data.depth > depth { + return; + } - pub fn lookup(&mut self, key: u64, depth_left: u8, depth: u8, alpha: i32, beta: i32) -> (Option, Option) { - if self.length == 0 { - return (None, None); + // If the new data and the old data were at the exact same depth, prefer + // EvalBound::Exact over Lower or Upper bounds + if data.depth == depth + && (data.eval_bound == EvalBound::Exact + || eval_bound != EvalBound::Exact) { + return; + } } - if let Some(data) = self.table[self.get_index(key)] { + self.table.insert(index, + TranspositionData { + key, + depth, + evaluation: fixed_mate_evaluation, + best_move: best_move.to_binary(), + eval_bound, + // age: 0, + }); + } + + pub fn lookup(&mut self, key: u64, ply: u8, depth: u8, alpha: i32, beta: i32) -> (Option, Option) { + if let Some(data) = self.table.get(&self.get_index(key)) { if data.key == key { self.hits += 1; + // data.age = 0; + let mut return_evaluation = None; - if data.depth_left >= depth_left { + if data.depth >= depth { let mut fixed_mate_evaluation = data.evaluation; - if evaluation_is_mate(data.evaluation) { let sign = if data.evaluation > 0 { 1 } else { -1 }; - fixed_mate_evaluation = (data.evaluation * sign - depth as i32) * sign; + fixed_mate_evaluation = (data.evaluation * sign - ply as i32) * sign; } - match data.eval_bound { - EvalBound::LowerBound => - if fixed_mate_evaluation >= beta { - return_evaluation = Some(fixed_mate_evaluation); - }, - EvalBound::UpperBound => - if fixed_mate_evaluation <= alpha { - return_evaluation = Some(fixed_mate_evaluation); - }, - EvalBound::Exact => - return_evaluation = Some(fixed_mate_evaluation), + if match data.eval_bound { + EvalBound::LowerBound => fixed_mate_evaluation >= beta, + EvalBound::UpperBound => fixed_mate_evaluation <= alpha, + EvalBound::Exact => true, + } { + return_evaluation = Some(fixed_mate_evaluation); } } @@ -120,7 +123,7 @@ impl TranspositionTable { } pub fn print_size(&self) { - let size = (self.entry_count * ENTRY_SIZE) as f32 / MEGABYTE as f32; + let size = (self.table.len() * ENTRY_SIZE) as f32 / MEGABYTE as f32; println!("Transposition table size: {} MB / {} MB", size, self.size_in_mb); } } \ No newline at end of file From 77a7095eb858afc03841f21222d7f5fbad722936 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:26:56 -0600 Subject: [PATCH 22/47] Updated README (v3) --- README.md | 4 ++-- src/move_sorter.rs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8d7d994..ed7bfec 100644 --- a/README.md +++ b/README.md @@ -38,14 +38,14 @@ #### Search - Iterative Deepening - Alpha-Beta Pruning - - Principal Variation Search - Late Move Reductions - Null Move Pruning - Razoring - Reverse Futility Pruning - Quiescence Search with Delta Pruning - Transposition Table - - No set max size, but entries get removed after 10 moves without hits + - UCI Hash option to change max size, default is 256 MB + - Replacement scheme prefers higher depth and exact evaluation bound - Search Extensions - Pawn moves to the 2nd or 7th rank - Checks diff --git a/src/move_sorter.rs b/src/move_sorter.rs index f01e36c..641f938 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -80,6 +80,7 @@ impl MoveSorter { // score += BASE_WORTHS_OF_PIECE_TYPE[m.flag as usize] + 12000; // } + // TODO: should this be on quiet moves only? if squares_opponent_attacks & (1 << m.to) != 0 { score -= 2 * BASE_WORTHS_OF_PIECE_TYPE[get_piece_type(m.piece as usize)]; } From 0eaf5aeceb48f738ccaf1081b3371d3a04b2ad2f Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Tue, 23 Jan 2024 14:56:26 -0600 Subject: [PATCH 23/47] Fixed up NMP --- Test results.txt | 11 ++++++++++- src/board.rs | 6 ++---- src/bot.rs | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Test results.txt b/Test results.txt index 4a0c405..7436be1 100644 --- a/Test results.txt +++ b/Test results.txt @@ -13,7 +13,16 @@ Score of Re-write v3 (Not root) vs Re-write v3 (Not PV): 131 - 60 - 109 Score of Re-write v3 (Not root) vs Re-write v3 (All): 33 - 33 - 36 Score of Re-write v3 (Depth replace) vs Re-write v3 (Always replace): 74 - 60 - 66 + +TT replacement schemes Elo +/- Games Points Draw 1 Re-write v3 (Higher depth, exact bound) 37 37 200 110.5 40.5% 2 Re-write v3 (Higher depth, equals key) -7 37 200 98.0 40.0% -3 Re-write v3 (Higher depth) -30 38 200 91.5 39.5% \ No newline at end of file +3 Re-write v3 (Higher depth) -30 38 200 91.5 39.5% + + +v4 contains fixes for Null Move Pruning + Elo +/- Games Points Draw +1 Re-write v4 33 45 150 82.0 34.7% +2 Re-write v3 12 46 150 77.5 32.7% +3 Re-write v2 -44 43 150 65.5 40.7% \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index e5b44d8..f1f5ec4 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1008,11 +1008,9 @@ impl Board { && self.piece_bitboards[BLACK_PAWN] == 0 } + // Must be called when not in check! pub fn try_null_move(&mut self) -> bool { - // TODO - if self.get_last_move() == NULL_MOVE - || self.king_in_check(self.white_to_move) // Maybe only check this before calling this function instead of inside the function? - || self.king_in_check(!self.white_to_move) { // ? + if self.get_last_move() == NULL_MOVE { return false; } diff --git a/src/bot.rs b/src/bot.rs index 7818a90..62621e5 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -264,7 +264,7 @@ impl Bot { // Null Move Pruning if depth > 2 && static_eval >= beta - // && board.total_material_without_pawns > 0 // TODO + && board.total_material_without_pawns > 0 && board.try_null_move() { let evaluation = -self.alpha_beta_search(board, depth - 3, ply + 1, -beta, -beta + 1, total_extensions); From e98b45aa4ccf2c25e449857d38cbde688aaa73d3 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:10:03 -0600 Subject: [PATCH 24/47] 1/23/24 7:10 PM Updated README Tested promotion extensions Lowered minimum time_to_think --- README.md | 4 ++-- Test results.txt | 13 ++++++++++++- src/board.rs | 1 - src/bot.rs | 12 +++++------- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ed7bfec..b79adc4 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,11 @@ - UCI Hash option to change max size, default is 256 MB - Replacement scheme prefers higher depth and exact evaluation bound - Search Extensions - - Pawn moves to the 2nd or 7th rank - Checks + - Pawn moves to the 2nd or 7th rank - Time management - If less than 7 moves have been played, it uses 2.5% of it's remaining time, otherwise 7% - - This value is then also clamped between 0.2 and 20.0 seconds + - This value is then also clamped between 0.05 and 20.0 seconds ## Helpful Sources & References - [Sebastian Lague's Chess Programming series](https://www.youtube.com/playlist?list=PLFt_AvWsXl0cvHyu32ajwh2qU1i6hl77c) diff --git a/Test results.txt b/Test results.txt index 7436be1..c399fae 100644 --- a/Test results.txt +++ b/Test results.txt @@ -25,4 +25,15 @@ v4 contains fixes for Null Move Pruning Elo +/- Games Points Draw 1 Re-write v4 33 45 150 82.0 34.7% 2 Re-write v3 12 46 150 77.5 32.7% -3 Re-write v2 -44 43 150 65.5 40.7% \ No newline at end of file +3 Re-write v2 -44 43 150 65.5 40.7% + + +Promotion extension tests + Elo +/- Games Points Draw +Pawn one step from promotion 26 45 150 80.5 35.3% +Promotion flag -7 45 150 73.5 35.3% +Neither -19 45 150 71.0 34.7% + + +Tests TODO: + TT enabled vs TT disabled \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index f1f5ec4..69006ee 100644 --- a/src/board.rs +++ b/src/board.rs @@ -334,7 +334,6 @@ impl Board { self.castling_rights.current &= !WHITE_CASTLE_SHORT; } - if data.capture == NO_PIECE as u8 && get_piece_type(data.piece as usize) != PAWN { self.fifty_move_draw.current += 1; diff --git a/src/bot.rs b/src/bot.rs index 62621e5..0608643 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -122,7 +122,7 @@ impl Bot { 0.07 }; - (my_time * time_percentage).clamp(0.2, 20.0) + (my_time * time_percentage).clamp(0.05, 20.0) } else { my_time }; @@ -323,12 +323,10 @@ impl Bot { if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { if board.king_in_check(board.white_to_move) { extension = 1; - } else { // TODO: does this help at all? Or maybe try checking for a promotion flag - if m.piece == PAWN as u8 { - let rank = m.to / 8; - if rank == 1 || rank == 6 { - extension = 1; - } + } else if m.piece == PAWN as u8 { + let rank = m.to / 8; + if rank == 1 || rank == 6 { + extension = 1; } } } From a4b56769b75623da7bd4f53140d22625a25b46b5 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Wed, 24 Jan 2024 13:37:15 -0600 Subject: [PATCH 25/47] 1/24/24 1:37 PM (Re-write v5) Aspiration windows now use the evaluation from the last iteration instead of the last search Tried looking up position in TT from Q-search, but it somehow increased both node counts and search time --- Test results.txt | 20 ++++++++++++++------ src/bot.rs | 12 +++++++----- src/main.rs | 3 +++ src/piece_square_tables.rs | 2 ++ src/transposition_table.rs | 2 +- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/Test results.txt b/Test results.txt index c399fae..eff5527 100644 --- a/Test results.txt +++ b/Test results.txt @@ -1,5 +1,7 @@ -REWRITE v1 vs v3.0.8 -+17, -14, =19 +!!! For X - Y - Z the order is Wins, Losses, Draws + + +Re-write v1 vs v3.0.8: 17 - 14 - 19 Elo +/- Games Score Draw @@ -15,10 +17,10 @@ Score of Re-write v3 (Depth replace) vs Re-write v3 (Always replace): 74 - 60 - TT replacement schemes - Elo +/- Games Points Draw -1 Re-write v3 (Higher depth, exact bound) 37 37 200 110.5 40.5% -2 Re-write v3 (Higher depth, equals key) -7 37 200 98.0 40.0% -3 Re-write v3 (Higher depth) -30 38 200 91.5 39.5% + Elo +/- Games Points Draw +1 Re-write v3 (Higher depth, exact bound) 37 37 200 110.5 40.5% +2 Re-write v3 (Higher depth, equals key) -7 37 200 98.0 40.0% +3 Re-write v3 (Higher depth) -30 38 200 91.5 39.5% v4 contains fixes for Null Move Pruning @@ -35,5 +37,11 @@ Promotion flag -7 45 150 73.5 35.3% Neither -19 45 150 71.0 34.7% +Re-write v4 vs Aspiration window drops after out of bounds: 275 - 263 - 262 + + +Re-write v5 (Uses evaluation from the last iteration instead from the last search) vs Re-write v4: 106 - 93 - 101 + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 0608643..1009d70 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -132,7 +132,6 @@ impl Bot { let last_evaluation = self.evaluation; self.best_move = NULL_MOVE; - self.evaluation = 0; self.positions_searched = 0; self.quiescence_searched = 0; @@ -140,6 +139,7 @@ impl Bot { self.move_sorter.clear(); + // TODO: tweak this let mut window = 40; self.think_timer = Instant::now(); @@ -148,13 +148,13 @@ impl Bot { self.best_move_this_iteration = NULL_MOVE; self.evaluation_this_iteration = 0; - // TODO: work on aspiration windows loop { - let (alpha, beta) = (last_evaluation - window, last_evaluation + window); + let (alpha, beta) = (self.evaluation - window, self.evaluation + window); let evaluation = self.alpha_beta_search(board, current_depth, 0, alpha, beta, 0); - if alpha < evaluation && evaluation < beta { + if evaluation > alpha + && evaluation < beta { break; } @@ -283,7 +283,7 @@ impl Bot { // Razoring if depth < 4 - && static_eval + 300 * (depth as i32) < alpha { + && static_eval + 300 * (depth as i32) < alpha { // TODO: tweak this threshold depth -= 1; } } @@ -408,6 +408,8 @@ impl Bot { self.quiescence_searched += 1; + // I tried looking up the TT here, but somehow it increased node counts and search time + let evaluation = board.evaluate(); if evaluation >= beta { return beta; diff --git a/src/main.rs b/src/main.rs index 5377671..595ab6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,8 @@ calculate my own magic numbers; currently "borrowing" Sebastian Lague's ^^ check out pin detection to speed up check detection? figure out how to implement "pondering" to think on opponent's time +https://www.chessprogramming.org/Texel's_Tuning_Method + Random ideas to try History reductions https://www.chessprogramming.org/Internal_Iterative_Deepening @@ -23,6 +25,7 @@ https://www.chessprogramming.org/History_Leaf_Pruning https://www.chessprogramming.org/Futility_Pruning#MoveCountBasedPruning https://www.chessprogramming.org/Countermove_Heuristic https://www.chessprogramming.org/ProbCut +https://www.chessprogramming.org/Razoring#Strelka Some random resources I found: (Not using them right now but they could be useful) https://analog-hors.github.io/site/magic-bitboards/ diff --git a/src/piece_square_tables.rs b/src/piece_square_tables.rs index 92e77f8..10a08f1 100644 --- a/src/piece_square_tables.rs +++ b/src/piece_square_tables.rs @@ -1,5 +1,7 @@ use crate::pieces::*; +// TODO: maybe combine base piece values and piece square tables? + pub const MIDDLEGAME_PAWN_TABLE: [i32; 64] = [ 0, 0, 0, 0, 0, 0, 0, 0, 50, 50, 50, 50, 50, 50, 50, 50, diff --git a/src/transposition_table.rs b/src/transposition_table.rs index ad08a27..d892243 100644 --- a/src/transposition_table.rs +++ b/src/transposition_table.rs @@ -1,5 +1,5 @@ /* -This TT implementation is a mashup of Rustic, Coding Adventure Bot and Weiawaga +Rustic, Coding Adventure Bot and Weiawaga were very helpful resources while developing this TT Thanks for the inspiration! */ From 34860da8ff5c0a690de1ecd3a7bdf4eaf40e925a Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Thu, 25 Jan 2024 09:27:18 -0600 Subject: [PATCH 26/47] 1/25/24 9:27 AM Moved condition that checks if the last move wasn't a capture to only NMP Fixed node counting (This bug was discovered in Tcheran, and I realized I had the exact same problem) --- Test results.txt | 17 +++++++++++++++-- src/bot.rs | 11 +++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/Test results.txt b/Test results.txt index eff5527..c5a0bfd 100644 --- a/Test results.txt +++ b/Test results.txt @@ -1,5 +1,6 @@ -!!! For X - Y - Z the order is Wins, Losses, Draws - +For X - Y - Z the order is: Wins, Losses, Draws +Time control: 30+0.5 or 10+0.2 +================================================================ Re-write v1 vs v3.0.8: 17 - 14 - 19 @@ -43,5 +44,17 @@ Re-write v4 vs Aspiration window drops after out of bounds: 275 - 263 - 262 Re-write v5 (Uses evaluation from the last iteration instead from the last search) vs Re-write v4: 106 - 93 - 101 +Removed capture check before doing pruning techniques vs Re-write v5: 126 - 98 - 76 + + +Capture check only before NMP vs Re-write v5 (Capture check before any pruning) 54 - 37 - 35 + + + Elo +/- Games Points Draw +Capture check before NMP 17 20 800 419.0 31.3% +Removed capture check 7 20 800 408.0 31.5% +Re-write v5 (Capture check before pruning) -23 20 800 373.0 29.8% + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 1009d70..30e118c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -218,9 +218,9 @@ impl Bot { return 0; } - self.positions_searched += 1; - if ply > 0 { + self.positions_searched += 1; + if board.is_draw() { // Should I use this to discourage making a draw in a winning position? // return -self.quiescence_search(board, alpha, beta); @@ -228,7 +228,6 @@ impl Bot { } // Mate Distance Pruning - let mate_value = CHECKMATE_EVAL - ply as i32; let alpha = i32::max(alpha, -CHECKMATE_EVAL + ply as i32); let beta = i32::min(beta, CHECKMATE_EVAL - ply as i32); if alpha >= beta { @@ -255,7 +254,6 @@ impl Bot { // time on PV lines? if not_pv && depth > 0 - && board.get_last_move().capture == NO_PIECE as u8 // ? && !board.king_in_check(board.white_to_move) { // TODO: move these around @@ -265,6 +263,7 @@ impl Bot { if depth > 2 && static_eval >= beta && board.total_material_without_pawns > 0 + && board.get_last_move().capture == NO_PIECE as u8 // Moving the check from the above check to only NMP was a decent improvement && board.try_null_move() { let evaluation = -self.alpha_beta_search(board, depth - 3, ply + 1, -beta, -beta + 1, total_extensions); @@ -289,6 +288,10 @@ impl Bot { } if depth == 0 { + // The current position will now be searched as a quiescence position, so we + // put positions searched back to where it was to avoid double counting nodes + // (This was in Tcheran's changelog, and I realized I have the exact same problem) + self.positions_searched -= 1; return self.quiescence_search(board, alpha, beta); } From 4557978b94172c34fe2445042c2ba9924e359d59 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Thu, 25 Jan 2024 14:50:39 -0600 Subject: [PATCH 27/47] 1/25/24 2:50 PM Moved RFP from after NMP to before Added lots of TODOs --- Test results.txt | 5 ++++- src/bot.rs | 50 ++++++++++++++++++++++++++++++++---------------- src/main.rs | 1 + 3 files changed, 38 insertions(+), 18 deletions(-) diff --git a/Test results.txt b/Test results.txt index c5a0bfd..e3bc4b5 100644 --- a/Test results.txt +++ b/Test results.txt @@ -51,10 +51,13 @@ Capture check only before NMP vs Re-write v5 (Capture check before any pruning) Elo +/- Games Points Draw -Capture check before NMP 17 20 800 419.0 31.3% +Re-write v6 (Capture check before NMP) 17 20 800 419.0 31.3% Removed capture check 7 20 800 408.0 31.5% Re-write v5 (Capture check before pruning) -23 20 800 373.0 29.8% +Score of Re-write v7 (RFP before NMP) vs Re-write v6: 136 - 120 - 144 + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 30e118c..9f14ca2 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -252,17 +252,25 @@ impl Bot { // TODO: Maybe allow these pruning techniques during PV nodes? // Or maybe just make them more aggressive to allow for more search // time on PV lines? + + // TODO: maybe disable these pruning techniques when the opponent has no material, because it can't + // checkmate with N+B during games so I think it's pruning too much in those positions if not_pv && depth > 0 && !board.king_in_check(board.white_to_move) { - // TODO: move these around - let static_eval = board.evaluate(); + // if not_pv { // ? + // Reverse Futility Pruning + if depth < 8 // TODO: 6? + && static_eval - 60 * (depth as i32) >= beta { // TODO: tweak this threshold + return static_eval; + } + // Null Move Pruning if depth > 2 && static_eval >= beta - && board.total_material_without_pawns > 0 + && board.total_material_without_pawns > 0 // TODO: maybe change this to endgame_multiplier? && board.get_last_move().capture == NO_PIECE as u8 // Moving the check from the above check to only NMP was a decent improvement && board.try_null_move() { let evaluation = -self.alpha_beta_search(board, depth - 3, ply + 1, -beta, -beta + 1, total_extensions); @@ -273,12 +281,7 @@ impl Bot { return evaluation; } } - - // Reverse Futility Pruning - if depth < 5 - && static_eval - 60 * (depth as i32) >= beta { // TODO: tweak this threshold - return static_eval; - } + // } // Razoring if depth < 4 @@ -316,6 +319,8 @@ impl Bot { ply as usize, ); + // let mut found_pv = false; + let mut i = 0; for (_, m) in sorted_moves { if !board.make_move(m) { @@ -334,25 +339,34 @@ impl Bot { } } - // Late Move Reductions let mut evaluation = 0; let mut needs_full_search = true; + // Late Move Reductions if i > 2 && depth > 2 && extension == 0 && m.capture == NO_PIECE as u8 { - // let mut reduction = 1; - - // if hash_move.is_none() { - // // Internal Iterative Reductions - // reduction += 1; - // } - evaluation = -self.alpha_beta_search(board, depth - 2, ply + 1, -alpha - 1, -alpha, total_extensions); needs_full_search = evaluation > alpha; } + // Principal Variation Search + // if found_pv || not_pv { + // if i > 2 + // && extension == 0 + // && m.capture == NO_PIECE as u8 { + // // Late Move Reductions + // evaluation = -self.alpha_beta_search(board, depth.saturating_sub(3), ply + 1, -alpha - 1, -alpha, total_extensions); + // needs_fuller_search = evaluation > alpha; + // } + + // if needs_fuller_search { + // evaluation = -self.alpha_beta_search(board, depth - 1, ply + 1, -alpha - 1, -alpha, total_extensions + extension); + // needs_fuller_search = evaluation > alpha; + // } + // } + if needs_full_search { evaluation = -self.alpha_beta_search(board, depth - 1 + extension, ply + 1, -beta, -alpha, total_extensions + extension); } @@ -375,6 +389,8 @@ impl Bot { } if evaluation > alpha { + // found_pv = true; + best_move_this_search = m; eval_bound = EvalBound::Exact; alpha = evaluation; diff --git a/src/main.rs b/src/main.rs index 595ab6e..dbbce49 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,7 @@ /* TODO overhaul CLI / UCI interface with proper error handling look into using "buckets" for transposition table +count white and black material separately big idea: remove all constant variables, and put them into the BotConfig struct, From 24f2490fdc96dd50db7f0b62bd72738cda1ba36b Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Thu, 25 Jan 2024 19:24:45 -0600 Subject: [PATCH 28/47] 1/25/24 7:25 PM Added back PVS --- Test results.txt | 3 +++ src/bot.rs | 43 +++++++++++++++++-------------------------- src/main.rs | 3 +++ 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/Test results.txt b/Test results.txt index e3bc4b5..80a9ed1 100644 --- a/Test results.txt +++ b/Test results.txt @@ -59,5 +59,8 @@ Re-write v5 (Capture check before pruning) -23 20 800 373.0 Score of Re-write v7 (RFP before NMP) vs Re-write v6: 136 - 120 - 144 +Score of Re-write v8 (New PVS) vs Re-write v7: 161 - 117 - 122 + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 9f14ca2..1dad5dd 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -252,9 +252,6 @@ impl Bot { // TODO: Maybe allow these pruning techniques during PV nodes? // Or maybe just make them more aggressive to allow for more search // time on PV lines? - - // TODO: maybe disable these pruning techniques when the opponent has no material, because it can't - // checkmate with N+B during games so I think it's pruning too much in those positions if not_pv && depth > 0 && !board.king_in_check(board.white_to_move) { @@ -270,7 +267,8 @@ impl Bot { // Null Move Pruning if depth > 2 && static_eval >= beta - && board.total_material_without_pawns > 0 // TODO: maybe change this to endgame_multiplier? + && board.total_material_without_pawns > 0 + // && board.endgame_multiplier() < 1.0 // TODO && board.get_last_move().capture == NO_PIECE as u8 // Moving the check from the above check to only NMP was a decent improvement && board.try_null_move() { let evaluation = -self.alpha_beta_search(board, depth - 3, ply + 1, -beta, -beta + 1, total_extensions); @@ -319,7 +317,7 @@ impl Bot { ply as usize, ); - // let mut found_pv = false; + let mut found_pv = false; let mut i = 0; for (_, m) in sorted_moves { @@ -340,34 +338,27 @@ impl Bot { } let mut evaluation = 0; - let mut needs_full_search = true; + let mut needs_fuller_search = true; // Late Move Reductions - if i > 2 - && depth > 2 + if i > 3 // 2? + && depth > 2 // 1? && extension == 0 && m.capture == NO_PIECE as u8 { - evaluation = -self.alpha_beta_search(board, depth - 2, ply + 1, -alpha - 1, -alpha, total_extensions); - needs_full_search = evaluation > alpha; + let reduction = 2 + (depth - 2) / 5; // TODO: tweak this + + evaluation = -self.alpha_beta_search(board, depth.saturating_sub(reduction), ply + 1, -alpha - 1, -alpha, total_extensions); + needs_fuller_search = evaluation > alpha && evaluation < beta; // && evaluation < beta? } // Principal Variation Search - // if found_pv || not_pv { - // if i > 2 - // && extension == 0 - // && m.capture == NO_PIECE as u8 { - // // Late Move Reductions - // evaluation = -self.alpha_beta_search(board, depth.saturating_sub(3), ply + 1, -alpha - 1, -alpha, total_extensions); - // needs_fuller_search = evaluation > alpha; - // } - - // if needs_fuller_search { - // evaluation = -self.alpha_beta_search(board, depth - 1, ply + 1, -alpha - 1, -alpha, total_extensions + extension); - // needs_fuller_search = evaluation > alpha; - // } - // } + if needs_fuller_search + && found_pv { + evaluation = -self.alpha_beta_search(board, depth - 1, ply + 1, -alpha - 1, -alpha, total_extensions + extension); + needs_fuller_search = evaluation > alpha; // && evaluation < beta? + } - if needs_full_search { + if needs_fuller_search { evaluation = -self.alpha_beta_search(board, depth - 1 + extension, ply + 1, -beta, -alpha, total_extensions + extension); } @@ -389,7 +380,7 @@ impl Bot { } if evaluation > alpha { - // found_pv = true; + found_pv = true; best_move_this_search = m; eval_bound = EvalBound::Exact; diff --git a/src/main.rs b/src/main.rs index dbbce49..043ff57 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,7 @@ /* TODO +the engine makes horrible endgame blunders for some reason: + making a 3-fold repetition when it's K+Q vs K, K+N+B vs K and K+P vs K + overhaul CLI / UCI interface with proper error handling look into using "buckets" for transposition table count white and black material separately From 86b06dce757a900048bbd13d27d927e2f3a27199 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Fri, 26 Jan 2024 09:59:10 -0600 Subject: [PATCH 29/47] 1/26/24 9:59 AM Removed depth check before LMR Rewrote how much LMR reduces by Removed "&& evaluation < beta" when checking if an evaluation needs a fuller search TT now returns bounds from EvalBound::LowerBound and EvalBound::HigherBound. I don't think this changes much, but it seems like the "standard practice" thing to do --- Test results.txt | 3 +++ src/bot.rs | 17 +++++++++++++---- src/transposition_table.rs | 10 ++++------ 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Test results.txt b/Test results.txt index 80a9ed1..dd74235 100644 --- a/Test results.txt +++ b/Test results.txt @@ -62,5 +62,8 @@ Score of Re-write v7 (RFP before NMP) vs Re-write v6: 136 - 120 - 144 Score of Re-write v8 (New PVS) vs Re-write v7: 161 - 117 - 122 +Score of Re-write v9 (LMR & PVS changes) vs Re-write v8 (New PVS): 418 - 241 - 341 + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 1dad5dd..52aee1c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -341,14 +341,23 @@ impl Bot { let mut needs_fuller_search = true; // Late Move Reductions - if i > 3 // 2? - && depth > 2 // 1? + if i > 3 + // && depth > 1 + // && depth > 2 && extension == 0 && m.capture == NO_PIECE as u8 { - let reduction = 2 + (depth - 2) / 5; // TODO: tweak this + let mut reduction = 2; + + if found_pv { + reduction += 1; + } + + if not_pv { + reduction += 1; + } evaluation = -self.alpha_beta_search(board, depth.saturating_sub(reduction), ply + 1, -alpha - 1, -alpha, total_extensions); - needs_fuller_search = evaluation > alpha && evaluation < beta; // && evaluation < beta? + needs_fuller_search = evaluation > alpha; // && evaluation < beta? } // Principal Variation Search diff --git a/src/transposition_table.rs b/src/transposition_table.rs index d892243..e929cd8 100644 --- a/src/transposition_table.rs +++ b/src/transposition_table.rs @@ -106,12 +106,10 @@ impl TranspositionTable { fixed_mate_evaluation = (data.evaluation * sign - ply as i32) * sign; } - if match data.eval_bound { - EvalBound::LowerBound => fixed_mate_evaluation >= beta, - EvalBound::UpperBound => fixed_mate_evaluation <= alpha, - EvalBound::Exact => true, - } { - return_evaluation = Some(fixed_mate_evaluation); + match data.eval_bound { + EvalBound::LowerBound => if fixed_mate_evaluation >= beta { return_evaluation = Some(beta); }, + EvalBound::UpperBound => if fixed_mate_evaluation <= alpha { return_evaluation = Some(alpha); }, + EvalBound::Exact => return_evaluation = Some(fixed_mate_evaluation), } } From 60febbe514f6ea123f34740eed2cd898b75a5939 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sat, 27 Jan 2024 12:22:01 -0600 Subject: [PATCH 30/47] 1/27/24 12:22 PM Ran a test between this version and v3.0.8-1, and it went 754 wins, 171 losses, and 331 draws in this versions favor! Fixed mate distance pruning Tried alternative history heuristic setup (it didn't work) Fixed perft test Zobrist now uses the ValueHolder struct --- Test results.txt | 8 +++- src/board.rs | 14 ++----- src/bot.rs | 12 +++--- src/main.rs | 25 +++++++----- src/move_sorter.rs | 1 + src/perft.rs | 8 ++-- src/value_holder.rs | 6 ++- src/zobrist.rs | 98 ++++++++++++++++++--------------------------- 8 files changed, 81 insertions(+), 91 deletions(-) diff --git a/Test results.txt b/Test results.txt index dd74235..9eb0689 100644 --- a/Test results.txt +++ b/Test results.txt @@ -1,5 +1,5 @@ For X - Y - Z the order is: Wins, Losses, Draws -Time control: 30+0.5 or 10+0.2 +Time controls I usually use: 30+0.5 or 10+0.2 ================================================================ Re-write v1 vs v3.0.8: 17 - 14 - 19 @@ -65,5 +65,11 @@ Score of Re-write v8 (New PVS) vs Re-write v7: 161 - 117 - 122 Score of Re-write v9 (LMR & PVS changes) vs Re-write v8 (New PVS): 418 - 241 - 341 +Score of Re-write v10 vs Re-write v9: 138 - 135 - 127 + + +Score of Re-write v10 vs Maxwell v3.0.8: 754 - 171 - 331 (1256 games!) + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index 69006ee..9e04585 100644 --- a/src/board.rs +++ b/src/board.rs @@ -98,11 +98,7 @@ impl Board { } } - // This has to be done after the board is setup (Duh) - let mut zobrist = Zobrist::generate(); - zobrist.generate_initial_key(&mut board); - board.zobrist = zobrist; - + board.zobrist = Zobrist::generate(&mut board); board.calculate_attacked_squares(); board @@ -439,7 +435,7 @@ impl Board { self.fifty_move_draw.pop(); self.castling_rights.pop(); - self.zobrist.pop(); + self.zobrist.key.pop(); self.attacked_squares_calculated = [false; 2]; @@ -481,8 +477,6 @@ impl Board { // self.undo_last_move(); // } - // result - result } @@ -1022,13 +1016,13 @@ impl Board { pub fn undo_null_move(&mut self) { self.white_to_move = !self.white_to_move; - self.zobrist.pop(); + self.zobrist.key.pop(); self.moves.pop(); } pub fn is_draw(&self) -> bool { self.fifty_move_draw.current >= 50 || self.insufficient_checkmating_material() - || self.zobrist.is_threefold_repetition() + || self.zobrist.is_repetition() } } \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 52aee1c..51621d3 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -228,14 +228,15 @@ impl Bot { } // Mate Distance Pruning - let alpha = i32::max(alpha, -CHECKMATE_EVAL + ply as i32); - let beta = i32::min(beta, CHECKMATE_EVAL - ply as i32); + let mate_value = CHECKMATE_EVAL - ply as i32; + let alpha = i32::max(alpha, -mate_value); + let beta = i32::min(beta, mate_value - 1); if alpha >= beta { return alpha; } } - let (tt_eval, hash_move) = self.transposition_table.lookup(board.zobrist.key, ply, depth, alpha, beta); + let (tt_eval, hash_move) = self.transposition_table.lookup(board.zobrist.key.current, ply, depth, alpha, beta); // We don't really want to return from the root node, because if a hash collision occurs (although very rare) // It will return an illegal move @@ -363,6 +364,7 @@ impl Bot { // Principal Variation Search if needs_fuller_search && found_pv { + // Oops I forgot depth - 1 + extension evaluation = -self.alpha_beta_search(board, depth - 1, ply + 1, -alpha - 1, -alpha, total_extensions + extension); needs_fuller_search = evaluation > alpha; // && evaluation < beta? } @@ -378,7 +380,7 @@ impl Bot { } if evaluation >= beta { - self.transposition_table.store(board.zobrist.key, depth, ply, beta, m, EvalBound::LowerBound); + self.transposition_table.store(board.zobrist.key.current, depth, ply, beta, m, EvalBound::LowerBound); if m.capture == NO_PIECE as u8 { self.move_sorter.push_killer_move(m, ply as usize); @@ -414,7 +416,7 @@ impl Bot { } if best_move_this_search != NULL_MOVE { - self.transposition_table.store(board.zobrist.key, depth, ply, alpha, best_move_this_search, eval_bound); + self.transposition_table.store(board.zobrist.key.current, depth, ply, alpha, best_move_this_search, eval_bound); } alpha diff --git a/src/main.rs b/src/main.rs index 043ff57..4b2d25f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,8 @@ /* TODO -the engine makes horrible endgame blunders for some reason: - making a 3-fold repetition when it's K+Q vs K, K+N+B vs K and K+P vs K +It still makes 3-fold repetitions when it's completely winning and low time + +Every few thousand games or so, it'll play an illegal move: always "e1g1" so I gotta go +look thorugh the castling logic overhaul CLI / UCI interface with proper error handling look into using "buckets" for transposition table @@ -70,14 +72,15 @@ use std::io; use colored::Colorize; use std::time::Instant; -pub const STARTING_FEN: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; -pub const KIWIPETE_FEN: &str = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; -pub const TEST_POSITION_4: &str = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"; -pub const DRAWN_ENDGAME_FEN: &str = "8/8/8/3k4/R5p1/P5r1/4K3/8 w - - 0 1"; -pub const MATE_IN_5_FEN: &str = "4r3/7q/nb2prRp/pk1p3P/3P4/P7/1P2N1P1/1K1B1N2 w - - 0 1"; -pub const PAWN_ENDGAME_FEN: &str = "8/k7/3p4/p2P1p2/P2P1P2/8/8/K7 w - - 0 1"; -pub const ENDGAME_POSITION: &str = "8/pk4p1/2prp3/3p1p2/3P2p1/R2BP3/2P2KPP/8 w - - 8 35"; -pub const PAWN_EVAL_TESTING: &str = "4k3/p1pp4/8/4pp1P/2P4P/8/P5P1/4K3 w - - 0 1"; +pub const STARTING_FEN: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; +pub const KIWIPETE_FEN: &str = "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 0 1"; +pub const TEST_POSITION_4: &str = "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1"; +pub const DRAWN_ENDGAME_FEN: &str = "8/8/8/3k4/R5p1/P5r1/4K3/8 w - - 0 1"; +pub const MATE_IN_5_FEN: &str = "4r3/7q/nb2prRp/pk1p3P/3P4/P7/1P2N1P1/1K1B1N2 w - - 0 1"; +pub const PAWN_ENDGAME_FEN: &str = "8/k7/3p4/p2P1p2/P2P1P2/8/8/K7 w - - 0 1"; +pub const ONE_PAWN_ENDGAME_FEN: &str = "8/8/1k6/8/8/1K6/1P6/8 w - - 0 1"; +pub const ENDGAME_POSITION: &str = "8/pk4p1/2prp3/3p1p2/3P2p1/R2BP3/2P2KPP/8 w - - 8 35"; +pub const PAWN_EVAL_TESTING: &str = "4k3/p1pp4/8/4pp1P/2P4P/8/P5P1/4K3 w - - 0 1"; fn main() { let mut bot_config = BotConfig::from_args(std::env::args().collect::>()); @@ -223,7 +226,7 @@ fn main() { "print" => board.print(), "bitboards" => board.print_bitboards(), "castlingrights" => print_castling_rights(board.castling_rights.current), - "zobrist" => println!("{}", board.zobrist.key), + "zobrist" => println!("{}", board.zobrist.key.current), "eval" => println!("{}", board.evaluate() * board.perspective()), "ttsize" => bot.transposition_table.print_size(), diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 641f938..60d9039 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -20,6 +20,7 @@ pub const MVV_LVA: [i32; 36] = [ pub struct MoveSorter { // pub pv_table: PVTable, pub killer_moves: [KillerMoves; MAX_SORT_MOVE_PLY], + // pub history: [[[i32; 64]; 64]; 2], pub history: [[i32; 64]; PIECE_COUNT], // TODO: Countermoves? } diff --git a/src/perft.rs b/src/perft.rs index 0d0d57b..240c74e 100644 --- a/src/perft.rs +++ b/src/perft.rs @@ -44,6 +44,10 @@ fn perft(board: &mut Board, results: &mut PerftResults, depth: u8, ply: u8) { } for data in board.get_pseudo_legal_moves_for_color(board.white_to_move, false) { + if !board.make_move(data) { + continue; + } + let position_count_before_move = results.positions; if data.capture != NO_PIECE as u8 { @@ -61,10 +65,6 @@ fn perft(board: &mut Board, results: &mut PerftResults, depth: u8, ply: u8) { results.promotions += 1; } - if !board.make_move(data) { - continue; - } - if board.king_in_check(board.white_to_move) { results.checks += 1; } diff --git a/src/value_holder.rs b/src/value_holder.rs index 1c6b5a6..2c9c72d 100644 --- a/src/value_holder.rs +++ b/src/value_holder.rs @@ -1,4 +1,4 @@ -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct ValueHolder { pub current: T, pub index: usize, @@ -28,6 +28,10 @@ impl ValueHolder where T: Copy { self.current = self.history[self.index]; } + pub fn last(&mut self) -> Option { + self.history.last().copied() + } + pub fn clear(&mut self) { self.index = 0; self.current = self.history[0]; diff --git a/src/zobrist.rs b/src/zobrist.rs index 3b16e14..d60a077 100644 --- a/src/zobrist.rs +++ b/src/zobrist.rs @@ -1,3 +1,4 @@ +use crate::value_holder::ValueHolder; use crate::pieces::*; use crate::Board; use crate::move_data::*; @@ -8,9 +9,7 @@ use rand_pcg::Pcg64; const SEED: u64 = 19274892; // old seed: 3141592653589793238 pub struct Zobrist { - pub key: u64, - pub history: Vec, - key_index: usize, + pub key: ValueHolder, pieces: [[u64; 64]; PIECE_COUNT], castling_rights: [u64; 16], @@ -22,9 +21,7 @@ pub struct Zobrist { impl Default for Zobrist { fn default() -> Self { Self { - history: vec![], - key: 0, - key_index: 0, + key: ValueHolder::new(0), pieces: [[0; 64]; PIECE_COUNT], castling_rights: [0; 16], @@ -36,7 +33,7 @@ impl Default for Zobrist { } impl Zobrist { - pub fn generate() -> Self { + pub fn generate(board: &mut Board) -> Self { let mut zobrist = Self::default(); let mut rng = Pcg64::seed_from_u64(SEED); @@ -56,57 +53,40 @@ impl Zobrist { zobrist.side_to_move = rng.gen::(); - zobrist - } - pub fn pop(&mut self) { - self.key_index -= 1; - self.key = self.history[self.key_index - 1]; - } + let mut initial_key = 0; - pub fn generate_initial_key(&mut self, board: &mut Board) { for i in 0..64 { let piece = board.get_piece(i); if piece != NO_PIECE { - self.key ^= self.pieces[piece][i as usize]; + initial_key ^= zobrist.pieces[piece][i as usize]; } } - self.key ^= self.castling_rights[board.castling_rights.current as usize]; + initial_key ^= zobrist.castling_rights[board.castling_rights.current as usize]; - self.key ^= self.en_passant[board.en_passant_file]; + initial_key ^= zobrist.en_passant[board.en_passant_file]; if !board.white_to_move { - self.key ^= self.side_to_move; + initial_key ^= zobrist.side_to_move; } - self.push(); - } + zobrist.key = ValueHolder::new(initial_key); - pub fn push(&mut self) { - if self.key_index >= self.history.len() { - self.history.push(self.key); - } else { - self.history[self.key_index] = self.key; - } - self.key_index += 1; - } - pub fn clear(&mut self) { - self.key_index = 0; - self.key = self.history[0]; - self.history.clear(); - self.push(); + zobrist } - pub fn is_threefold_repetition(&self) -> bool { - let mut count = 0; - for i in 0..self.key_index { - if self.history[i] == self.key { - count += 1; - if count >= 2 { - return true; - } + // TODO: take in the current fifty move counter, and only count positions from there? + pub fn is_repetition(&self) -> bool { + // let mut count = 0; + for i in 0..self.key.index { + if self.key.history[i] == self.key.current { + // count += 1; + // if count >= 2 { + // return true; + // } + return true; } } false @@ -122,22 +102,22 @@ impl Zobrist { ) { let to = data.to as usize; - self.key ^= self.pieces[data.piece as usize][data.from as usize]; + self.key.current ^= self.pieces[data.piece as usize][data.from as usize]; if !PROMOTABLE.contains(&data.flag) { - self.key ^= self.pieces[data.piece as usize][to]; + self.key.current ^= self.pieces[data.piece as usize][to]; } else { - self.key ^= self.pieces[build_piece(is_piece_white(data.piece as usize), data.flag as usize)][to]; + self.key.current ^= self.pieces[build_piece(is_piece_white(data.piece as usize), data.flag as usize)][to]; } if data.flag == SHORT_CASTLE_FLAG { let rook = build_piece(is_piece_white(data.piece as usize), ROOK); - self.key ^= self.pieces[rook][to + 1]; - self.key ^= self.pieces[rook][to - 1]; + self.key.current ^= self.pieces[rook][to + 1]; + self.key.current ^= self.pieces[rook][to - 1]; } else if data.flag == LONG_CASTLE_FLAG { let rook = build_piece(is_piece_white(data.piece as usize), ROOK); - self.key ^= self.pieces[rook][to - 2]; - self.key ^= self.pieces[rook][to + 1]; + self.key.current ^= self.pieces[rook][to - 2]; + self.key.current ^= self.pieces[rook][to + 1]; } else if data.capture != NO_PIECE as u8 { if data.flag == EN_PASSANT_FLAG { let pawn_to_en_passant = if is_piece_white(data.piece as usize) { @@ -146,35 +126,35 @@ impl Zobrist { to - 8 }; - self.key ^= self.pieces[data.capture as usize][pawn_to_en_passant]; + self.key.current ^= self.pieces[data.capture as usize][pawn_to_en_passant]; } else { - self.key ^= self.pieces[data.capture as usize][to]; + self.key.current ^= self.pieces[data.capture as usize][to]; } } - self.key ^= self.castling_rights[last_castling_rights as usize]; - self.key ^= self.castling_rights[castling_rights as usize]; + self.key.current ^= self.castling_rights[last_castling_rights as usize]; + self.key.current ^= self.castling_rights[castling_rights as usize]; if last_move.flag == DOUBLE_PAWN_PUSH_FLAG { let file = (last_move.to as usize % 8) + 1; - self.key ^= self.en_passant[file]; + self.key.current ^= self.en_passant[file]; } if data.flag == DOUBLE_PAWN_PUSH_FLAG { let file = (to % 8) + 1; - self.key ^= self.en_passant[file]; + self.key.current ^= self.en_passant[file]; self.en_passant_file = file; } - self.key ^= self.side_to_move; + self.key.current ^= self.side_to_move; - self.push(); + self.key.push(); } pub fn make_null_move(&mut self) { - self.key ^= self.side_to_move; - self.key ^= self.en_passant[self.en_passant_file]; + self.key.current ^= self.side_to_move; + self.key.current ^= self.en_passant[self.en_passant_file]; - self.push(); + self.key.push(); } } \ No newline at end of file From e3e13f5f23c481e07125e9a76c01600d7a16ab8e Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sat, 27 Jan 2024 16:06:07 -0600 Subject: [PATCH 31/47] 1/27/24 4:06 PM --- Test results.txt | 3 +++ src/bot.rs | 2 +- src/castling_rights.rs | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Test results.txt b/Test results.txt index 9eb0689..76a69b8 100644 --- a/Test results.txt +++ b/Test results.txt @@ -71,5 +71,8 @@ Score of Re-write v10 vs Re-write v9: 138 - 135 - 127 Score of Re-write v10 vs Maxwell v3.0.8: 754 - 171 - 331 (1256 games!) +Score of Re-write v10 vs Added extension to PVS searches: 99 - 85 - 116 (?) + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 51621d3..2e2f167 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -364,7 +364,7 @@ impl Bot { // Principal Variation Search if needs_fuller_search && found_pv { - // Oops I forgot depth - 1 + extension + // Adding the extension to this search actually made it worse? evaluation = -self.alpha_beta_search(board, depth - 1, ply + 1, -alpha - 1, -alpha, total_extensions + extension); needs_fuller_search = evaluation > alpha; // && evaluation < beta? } diff --git a/src/castling_rights.rs b/src/castling_rights.rs index f08820a..be92ced 100644 --- a/src/castling_rights.rs +++ b/src/castling_rights.rs @@ -1,7 +1,7 @@ pub const WHITE_CASTLE_LONG: u8 = 0b1000; pub const WHITE_CASTLE_SHORT: u8 = 0b0100; -pub const BLACK_CASTLE_SHORT: u8 = 0b0010; -pub const BLACK_CASTLE_LONG: u8 = 0b0001; +pub const BLACK_CASTLE_LONG: u8 = 0b0010; +pub const BLACK_CASTLE_SHORT: u8 = 0b0001; pub const SHORT_CASTLING_RIGHTS: [u8; 2] = [BLACK_CASTLE_SHORT, WHITE_CASTLE_SHORT]; pub const LONG_CASTLING_RIGHTS: [u8; 2] = [BLACK_CASTLE_LONG, WHITE_CASTLE_LONG]; From fbad783af73fdd39732c6f5387eb941fb89bf4db Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Sun, 28 Jan 2024 10:18:27 -0600 Subject: [PATCH 32/47] 1/28/24 10:18 AM Rewrote draw detection code Fixed castling bug Added "&& depth > 1" to LMR --- Test results.txt | 3 +++ src/board.rs | 42 +++++++++++++++++++++++++----------------- src/bot.rs | 4 ++-- src/main.rs | 5 ++--- src/zobrist.rs | 9 ++------- 5 files changed, 34 insertions(+), 29 deletions(-) diff --git a/Test results.txt b/Test results.txt index 76a69b8..efdbde5 100644 --- a/Test results.txt +++ b/Test results.txt @@ -74,5 +74,8 @@ Score of Re-write v10 vs Maxwell v3.0.8: 754 - 171 - 331 (1256 games!) Score of Re-write v10 vs Added extension to PVS searches: 99 - 85 - 116 (?) +Score of Re-write v11 vs Re-write v10: 376 - 343 - 481 + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index 9e04585..4a2b064 100644 --- a/src/board.rs +++ b/src/board.rs @@ -23,7 +23,7 @@ pub struct Board { pub attacked_squares_bitboards: [u64; 2], pub castling_rights: ValueHolder, - pub fifty_move_draw: ValueHolder, + pub fifty_move_counter: ValueHolder, pub en_passant_file: usize, pub white_to_move: bool, @@ -48,7 +48,7 @@ impl Board { if fen[2].contains('q') { castling_rights ^= BLACK_CASTLE_LONG; } if fen[2].contains('k') { castling_rights ^= BLACK_CASTLE_SHORT; } - let fifty_move_draw = fen[4].parse::().unwrap_or(0); + let fifty_move_counter = fen[4].parse::().unwrap_or(0); let mut board = Self { precalculated_move_data: PrecalculatedMoveData::calculate(), @@ -58,7 +58,7 @@ impl Board { attacked_squares_bitboards: [0; 2], castling_rights: ValueHolder::new(castling_rights), - fifty_move_draw: ValueHolder::new(fifty_move_draw), + fifty_move_counter: ValueHolder::new(fifty_move_counter), en_passant_file: 0, // This isn't implemented at all // en_passant_file: if fen[3] == "-" { 0 } else { (coordinate_to_index(fen[3]) % 8) + 1 }, @@ -316,28 +316,36 @@ impl Board { self.color_bitboards[0] ^= 1; self.color_bitboards[0] ^= 1 << 3; } - } else if data.from == 0 - || data.to == 0 { + } + + if data.from == 0 { + self.castling_rights.current &= !BLACK_CASTLE_LONG; + } else if data.from == 7 { + self.castling_rights.current &= !BLACK_CASTLE_SHORT; + } else if data.from == 56 { + self.castling_rights.current &= !WHITE_CASTLE_LONG; + } else if data.from == 63 { + self.castling_rights.current &= !WHITE_CASTLE_SHORT; + } + + if data.to == 0 { self.castling_rights.current &= !BLACK_CASTLE_LONG; - } else if data.from == 7 - || data.to == 7 { + } else if data.to == 7 { self.castling_rights.current &= !BLACK_CASTLE_SHORT; - } else if data.from == 56 - || data.to == 56 { + } else if data.to == 56 { self.castling_rights.current &= !WHITE_CASTLE_LONG; - } else if data.from == 63 - || data.to == 63 { + } else if data.to == 63 { self.castling_rights.current &= !WHITE_CASTLE_SHORT; } if data.capture == NO_PIECE as u8 && get_piece_type(data.piece as usize) != PAWN { - self.fifty_move_draw.current += 1; + self.fifty_move_counter.current += 1; } else { - self.fifty_move_draw.current = 0; + self.fifty_move_counter.current = 0; } - self.fifty_move_draw.push(); + self.fifty_move_counter.push(); self.castling_rights.push(); self.zobrist.make_move( @@ -433,7 +441,7 @@ impl Board { } } - self.fifty_move_draw.pop(); + self.fifty_move_counter.pop(); self.castling_rights.pop(); self.zobrist.key.pop(); @@ -1021,8 +1029,8 @@ impl Board { } pub fn is_draw(&self) -> bool { - self.fifty_move_draw.current >= 50 + self.fifty_move_counter.current >= 100 || self.insufficient_checkmating_material() - || self.zobrist.is_repetition() + || self.zobrist.is_repetition(self.fifty_move_counter.current as usize) } } \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index 2e2f167..cbb2725 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -343,7 +343,7 @@ impl Bot { // Late Move Reductions if i > 3 - // && depth > 1 + && depth > 1 // && depth > 2 && extension == 0 && m.capture == NO_PIECE as u8 { @@ -365,7 +365,7 @@ impl Bot { if needs_fuller_search && found_pv { // Adding the extension to this search actually made it worse? - evaluation = -self.alpha_beta_search(board, depth - 1, ply + 1, -alpha - 1, -alpha, total_extensions + extension); + evaluation = -self.alpha_beta_search(board, depth - 1, ply + 1, -alpha - 1, -alpha, total_extensions); needs_fuller_search = evaluation > alpha; // && evaluation < beta? } diff --git a/src/main.rs b/src/main.rs index 4b2d25f..8010896 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,8 @@ /* TODO It still makes 3-fold repetitions when it's completely winning and low time + (I think this might have been fixed with the new draw detection changes?) -Every few thousand games or so, it'll play an illegal move: always "e1g1" so I gotta go -look thorugh the castling logic - +maybe remove the whole search extension logic and just add to depth? overhaul CLI / UCI interface with proper error handling look into using "buckets" for transposition table count white and black material separately diff --git a/src/zobrist.rs b/src/zobrist.rs index d60a077..8fda1c6 100644 --- a/src/zobrist.rs +++ b/src/zobrist.rs @@ -77,15 +77,10 @@ impl Zobrist { zobrist } - // TODO: take in the current fifty move counter, and only count positions from there? - pub fn is_repetition(&self) -> bool { + pub fn is_repetition(&self, fifty_move_counter: usize) -> bool { // let mut count = 0; - for i in 0..self.key.index { + for i in ((self.key.index - fifty_move_counter)..self.key.index).step_by(2) { if self.key.history[i] == self.key.current { - // count += 1; - // if count >= 2 { - // return true; - // } return true; } } From ff0b473a4cc032563ca21149fc996321f33d91f6 Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Mon, 29 Jan 2024 22:38:09 -0600 Subject: [PATCH 33/47] 1/29/24 10:38 PM Updated README Moved To-do list to separate file Added tons of TODO markers Started writing out a "MoveList" struct, although it's not being used yet Added null move check at the end of the search function --- README.md | 15 +++++++++---- Test results.txt | 7 ++++++ To-do list.txt | 45 +++++++++++++++++++++++++++++++++++++ src/board.rs | 2 +- src/bot.rs | 55 ++++++++++++++++++++++++++++++++++++++-------- src/main.rs | 39 +------------------------------- src/move_list.rs | 47 +++++++++++++++++++++++++++++++++++++++ src/move_sorter.rs | 9 ++++---- 8 files changed, 162 insertions(+), 57 deletions(-) create mode 100644 To-do list.txt create mode 100644 src/move_list.rs diff --git a/README.md b/README.md index b79adc4..7a589ea 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,11 @@ - debug=\: Toggle debug output that gets outputed per ply (default=true) - opening_book=\: Toggle built-in opening book (default=false) - time_management=\: Toggle time management, if false the bot will use all the remaining time (default=true) + - tt_size=\: Sets the hash size in Megabytes, there's also a UCI option for this under the name "Hash" (default=256) #### UCI Interface - uci, isready, ucinewgame, position, go, stop, and quit commands - "position" is only implemented for "position startpos", "position fen" is not yet implemented + - "Hash" UCI option, which sets the hash / transposition table size in Megabytes #### Board Representation - Purely bitboards - Supports loading from FEN strings @@ -23,6 +25,7 @@ - Magic bitboards for sliding pieces - Hardcoded pawn movement - Bitboard masks for other pieces calculated at startup + - Calculates pseudo-legal moves, then skips illegal moves in move loop #### Evaluation - Material count - Piece square tables @@ -30,19 +33,21 @@ - Passed, isolated and doubled pawns - Attacked squares around kings #### Move Ordering - - Best move from the previous iteration, otherwise from the transposition table + - Best move from the previous iteration, otherwise whatever move from the transposition table - MVV-LVA - 2 Killer Moves - History Heuristic - Penalty for moving a piece to a square an opponent's piece attacks #### Search - Iterative Deepening + - Negamax + - Principal Variation Search - Alpha-Beta Pruning - Late Move Reductions - Null Move Pruning - Razoring - Reverse Futility Pruning - - Quiescence Search with Delta Pruning + - Quiescence Search with Delta Pruning (No TT hits) - Transposition Table - UCI Hash option to change max size, default is 256 MB - Replacement scheme prefers higher depth and exact evaluation bound @@ -53,7 +58,7 @@ - If less than 7 moves have been played, it uses 2.5% of it's remaining time, otherwise 7% - This value is then also clamped between 0.05 and 20.0 seconds -## Helpful Sources & References +## Helpful Sources & References (In no particular order) - [Sebastian Lague's Chess Programming series](https://www.youtube.com/playlist?list=PLFt_AvWsXl0cvHyu32ajwh2qU1i6hl77c) - [The Chess Programming Wiki](https://www.chessprogramming.org/Main_Page) - [BBC Engine Development](https://www.youtube.com/playlist?list=PLmN0neTso3Jxh8ZIylk74JpwfiWNI76Cs) @@ -67,4 +72,6 @@ - [LMR Implementation](https://web.archive.org/web/20150212051846/http://www.glaurungchess.com/lmr.html) - [Mediocre Chess](https://mediocrechess.blogspot.com/) - [Tcheran](https://github.com/jgilchrist/tcheran/) - - [Rustic (Engine and Book)](https://github.com/mvanthoor/rustic) \ No newline at end of file + - [Rustic (Engine and Book)](https://github.com/mvanthoor/rustic) + - [Chess Programming Reddit](https://www.reddit.com/r/chessprogramming/) + - [TalkChess Forum](https://talkchess.com/forum3/index.php) \ No newline at end of file diff --git a/Test results.txt b/Test results.txt index efdbde5..f9c2697 100644 --- a/Test results.txt +++ b/Test results.txt @@ -77,5 +77,12 @@ Score of Re-write v10 vs Added extension to PVS searches: 99 - 85 - 116 (?) Score of Re-write v11 vs Re-write v10: 376 - 343 - 481 +Score of With alpha TT stores vs Without alpha TT stores (Re-write v11): 239 - 256 - 355 + + +Don't allow more than one null move in the search tree at once vs Re-write v11: 175 - 167 - 268 + Techincally a win, but the scores were fluctuating a good bit, and I'm not so sure about it + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/To-do list.txt b/To-do list.txt new file mode 100644 index 0000000..22ff8eb --- /dev/null +++ b/To-do list.txt @@ -0,0 +1,45 @@ +It still sometimes makes 3-fold repetitions when it's completely winning and low time + +big idea: + remove all constant variables, and put them into the BotConfig struct, + then write my own tuning / matchmaking program that will tweak the values + and play matches until it finds better values... + +thoughts on NNUE: + I've wanted to learn how to write neural net for a long time, so I want to implement NNUE eventually. + But what I'm not going to do is just find a SF NNUE library and stick it in there because that's lame + +try changing history to use [from][to] +countermoves + on > alpha, or beta cutoff? + index by [piece][to] + +figure out some sort of multithreading: + to implement pondering I think I'll have to add multithreading + maybe on thread that waits for UCI commands like "stop" + and a bunch of other threads to search + +try giving a small boost in evaluation for the current side to move +try lower pawn evaluation values +experiment with more than 2 killer moves per ply +maybe remove the whole max search extension logic and just add to depth +overhaul CLI / UCI interface with proper error handling +look into using "buckets" for transposition table +count white and black material separately +calculate my own magic numbers; currently "borrowing" Sebastian Lague's ^^ +check out pin detection to speed up check detection +try writing a struct that sorts moves incrementally + +History reductions / pruning +https://www.chessprogramming.org/Internal_Iterative_Deepening +https://www.chessprogramming.org/Static_Exchange_Evaluation +https://www.chessprogramming.org/History_Leaf_Pruning +https://www.chessprogramming.org/Futility_Pruning#MoveCountBasedPruning +https://www.chessprogramming.org/Countermove_Heuristic +https://www.chessprogramming.org/ProbCut +https://www.chessprogramming.org/Razoring#Strelka +https://www.chessprogramming.org/Texel's_Tuning_Method +https://www.chessprogramming.org/Enhanced_Transposition_Cutoff + +Some random resources I found: (Not using them right now but they could be useful) +https://analog-hors.github.io/site/magic-bitboards/ \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index 4a2b064..a7661ba 100644 --- a/src/board.rs +++ b/src/board.rs @@ -1031,6 +1031,6 @@ impl Board { pub fn is_draw(&self) -> bool { self.fifty_move_counter.current >= 100 || self.insufficient_checkmating_material() - || self.zobrist.is_repetition(self.fifty_move_counter.current as usize) + || self.zobrist.is_repetition(self.fifty_move_counter.current as usize) // TODO: should I take into account null moves? } } \ No newline at end of file diff --git a/src/bot.rs b/src/bot.rs index cbb2725..224c88c 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -129,8 +129,6 @@ impl Bot { self.search_cancelled = false; - let last_evaluation = self.evaluation; - self.best_move = NULL_MOVE; self.positions_searched = 0; @@ -192,6 +190,17 @@ impl Bot { } } + if self.best_move == NULL_MOVE { + let legal_moves = board.get_pseudo_legal_moves_for_color(board.white_to_move, false); + for m in legal_moves { + if board.make_move(m) { + board.undo_last_move(); // Technically not necessary, but makes me happy :> + self.best_move = m; + break; + } + } + } + self.println(format!("{} seconds", self.think_timer.elapsed().as_secs_f32())); if self.config.debug_output { @@ -244,6 +253,12 @@ impl Bot { if let Some(tt_eval) = tt_eval { return tt_eval; } + + // Internal Iterative Reductions + // if depth > 3 + // && hash_move.is_none() { + // depth -= 1; + // } } // This detects a null / zero window search, which is used in non PV nodes @@ -255,6 +270,8 @@ impl Bot { // time on PV lines? if not_pv && depth > 0 + // && !evaluation_is_mate(alpha) // TODO + // && !evaluation_is_mate(beta) // TODO && !board.king_in_check(board.white_to_move) { let static_eval = board.evaluate(); @@ -280,6 +297,24 @@ impl Bot { return evaluation; } } + + // Random NMP testing + // if depth > 1 + // && (static_eval >= beta || depth < 5) + // && board.total_material_without_pawns > 0 + // && !evaluation_is_mate(beta) + // // && board.endgame_multiplier() < 1.0 // TODO + // && board.get_last_move().capture == NO_PIECE as u8 // Moving the check from the above check to only NMP was a decent improvement + // && board.try_null_move() { + // let reduction = 3 + depth / 10; + // let evaluation = -self.alpha_beta_search(board, depth.saturating_sub(reduction), ply + 1, -beta, -beta + 1, total_extensions); + + // board.undo_null_move(); + + // if evaluation >= beta { + // return evaluation; + // } + // } // } // Razoring @@ -320,7 +355,7 @@ impl Bot { let mut found_pv = false; - let mut i = 0; + let mut legal_moves_found = 0; for (_, m) in sorted_moves { if !board.make_move(m) { continue; @@ -342,7 +377,7 @@ impl Bot { let mut needs_fuller_search = true; // Late Move Reductions - if i > 3 + if legal_moves_found > 3 && depth > 1 // && depth > 2 && extension == 0 @@ -404,10 +439,10 @@ impl Bot { } } - i += 1; + legal_moves_found += 1; } - if i == 0 { + if legal_moves_found == 0 { if board.king_in_check(board.white_to_move) { let mate_score = CHECKMATE_EVAL - ply as i32; return -mate_score; @@ -415,6 +450,8 @@ impl Bot { return 0; } + // I've seen a small improvement if I don't store EvalBound::UpperBound + // but idk I'll come back to this later if best_move_this_search != NULL_MOVE { self.transposition_table.store(board.zobrist.key.current, depth, ply, alpha, best_move_this_search, eval_bound); } @@ -440,12 +477,12 @@ impl Bot { alpha = evaluation; } - let legal_moves = board.get_pseudo_legal_moves_for_color(board.white_to_move, true); - if legal_moves.is_empty() { + let moves = board.get_pseudo_legal_moves_for_color(board.white_to_move, true); + if moves.is_empty() { return evaluation; } - let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, usize::MAX); + let sorted_moves = self.move_sorter.sort_moves(board, moves, NULL_MOVE, usize::MAX); for (_, m) in sorted_moves { // Delta Pruning diff --git a/src/main.rs b/src/main.rs index 8010896..d6e2105 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,41 +1,3 @@ -/* TODO -It still makes 3-fold repetitions when it's completely winning and low time - (I think this might have been fixed with the new draw detection changes?) - -maybe remove the whole search extension logic and just add to depth? -overhaul CLI / UCI interface with proper error handling -look into using "buckets" for transposition table -count white and black material separately - -big idea: - remove all constant variables, and put them into the BotConfig struct, - then write my own tuning / matchmaking program that will tweak the values - and play matches until it finds better values... - -thoughts on NNUE: - I've wanted to learn how to write neural net for a long time, so I want to implement NNUE eventually. - But what I'm not going to do is just find a SF NNUE library and stick it in there because that's lame - -calculate my own magic numbers; currently "borrowing" Sebastian Lague's ^^ -check out pin detection to speed up check detection? -figure out how to implement "pondering" to think on opponent's time - -https://www.chessprogramming.org/Texel's_Tuning_Method - -Random ideas to try -History reductions -https://www.chessprogramming.org/Internal_Iterative_Deepening -https://www.chessprogramming.org/Static_Exchange_Evaluation -https://www.chessprogramming.org/History_Leaf_Pruning -https://www.chessprogramming.org/Futility_Pruning#MoveCountBasedPruning -https://www.chessprogramming.org/Countermove_Heuristic -https://www.chessprogramming.org/ProbCut -https://www.chessprogramming.org/Razoring#Strelka - -Some random resources I found: (Not using them right now but they could be useful) -https://analog-hors.github.io/site/magic-bitboards/ -*/ - #![allow(dead_code)] #![allow(unused_variables)] #![allow(unused_imports)] @@ -59,6 +21,7 @@ mod perft; mod bot; mod pv_table; mod move_sorter; +mod move_list; use crate::castling_rights::print_castling_rights; use crate::bot::{Bot, BotConfig, MAX_SEARCH_EXTENSIONS}; diff --git a/src/move_list.rs b/src/move_list.rs new file mode 100644 index 0000000..bfa7ff0 --- /dev/null +++ b/src/move_list.rs @@ -0,0 +1,47 @@ +// NOT BEING USED + +use crate::MoveData; + +pub struct MoveList { + pub moves: Vec, + pub scores: Vec, + pub moves_tried: Vec, +} + +impl MoveList { + pub fn new() -> Self { + Self { + scores: vec![], + moves: vec![], + moves_tried: vec![], + } + } + + pub fn len(&self) -> usize { self.moves.len() } + + pub fn push(&mut self, move_data: MoveData, score: i32) { + self.moves.push(move_data); + self.scores.push(score); + self.moves_tried.push(false); + } + + pub fn get_next_move(&mut self) -> MoveData { + let mut best_move_i = 0; + let mut best_score = self.scores[0]; + + for i in 0..self.moves.len() { + if self.moves_tried[i] { + continue; + } + + let this_move_score = self.scores[i]; + if this_move_score > best_score { + best_move_i = i; + best_score = this_move_score; + } + } + + self.moves_tried[best_move_i] = true; + self.moves[best_move_i] + } +} \ No newline at end of file diff --git a/src/move_sorter.rs b/src/move_sorter.rs index 60d9039..5f133c4 100644 --- a/src/move_sorter.rs +++ b/src/move_sorter.rs @@ -1,4 +1,4 @@ -use crate::pv_table::PVTable; +// use crate::pv_table::PVTable; use crate::move_data::EN_PASSANT_FLAG; use crate::killer_moves::KillerMoves; use crate::move_data::{MoveData, NULL_MOVE}; @@ -20,15 +20,15 @@ pub const MVV_LVA: [i32; 36] = [ pub struct MoveSorter { // pub pv_table: PVTable, pub killer_moves: [KillerMoves; MAX_SORT_MOVE_PLY], - // pub history: [[[i32; 64]; 64]; 2], pub history: [[i32; 64]; PIECE_COUNT], - // TODO: Countermoves? + // TODO: + // pub history: [[i32; 64]; 64], + // pub countermoves: [[MoveData; 64]; PIECE_COUNT], } impl MoveSorter { pub fn new() -> Self { Self { - // pv_table: PVTable::new(), killer_moves: [KillerMoves::new(); MAX_SORT_MOVE_PLY], history: [[0; 64]; PIECE_COUNT], } @@ -51,7 +51,6 @@ impl MoveSorter { } let mut scores = vec![]; - let squares_opponent_attacks = board.get_attacked_squares_for_color((!board.white_to_move) as usize); for m in moves { From 01e2577b3636928afe3d0cbf461ac4d6d32ff29a Mon Sep 17 00:00:00 2001 From: eboatwright <64672325+eboatwright@users.noreply.github.com> Date: Thu, 1 Feb 2024 14:35:30 -0600 Subject: [PATCH 34/47] 2/1/24 2:35 PM Updated README Removed coordinate_to_index, and index_to_coordinate functions in favor of a 64 string long array with all the coordinates Rewrote most of the UCI interface, all that's left is the "position" command Rewrote the board's attacked squares cache, and updated total_material_without_pawns to keep track of both colors independently NMP. RFP and Razoring now longer get called when beta is mate Updated NMP reduction and threshold --- README.md | 3 +- Test results.txt | 12 ++++ To-do list.txt | 3 +- src/board.rs | 122 ++++++++++++++++++------------------- src/bot.rs | 66 ++++++++++---------- src/main.rs | 90 +++++++++++++++++---------- src/move_data.rs | 6 +- src/piece_square_tables.rs | 2 - src/pv_table.rs | 4 +- src/utils.rs | 82 ++++++++++++++----------- 10 files changed, 219 insertions(+), 171 deletions(-) diff --git a/README.md b/README.md index 7a589ea..163af73 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ If you use this code verbatim, or as a reference, please credit me!



- [Play against Maxwell on Lichess!](https://lichess.org/@/MaxwellOnLC) | [Some of Maxwell's Games](https://www.chess.com/library/collections/maxwells-games-my-chess-engine-2FFU82NM4) | [Maxwell's CCRL](https://computerchess.org.uk/ccrl/404/cgi/engine_details.cgi?print=Details&each_game=1&eng=Maxwell%203.0.8-1%2064-bit#Maxwell_3_0_8-1_64-bit) + Rating Lists featuring Maxwell: [CCRL Blitz](https://computerchess.org.uk/ccrl/404/) | [MCERL](https://www.chessengeria.eu/mcerl)
+ [Play against Maxwell on Lichess!](https://lichess.org/@/MaxwellOnLC) | [Some of Maxwell's Games](https://www.chess.com/library/collections/maxwells-games-my-chess-engine-2FFU82NM4) ## Features - NOT UP TO DATE WITH DEV BRANCH #### Parameters diff --git a/Test results.txt b/Test results.txt index f9c2697..507a0f8 100644 --- a/Test results.txt +++ b/Test results.txt @@ -84,5 +84,17 @@ Don't allow more than one null move in the search tree at once vs Re-write v11: Techincally a win, but the scores were fluctuating a good bit, and I'm not so sure about it +v12 vs v11 was equal, because v12 was just a bit of code cleanup + + +Strelka razoring vs Re-write v12: 234 - 304 - 368 + + +NMP reduction (> 1, and 3 + (depth - 2) / 5) vs Re-write v12: 255 - 254 - 391 + + +Re-write v13 (NMP changes: > 1, and 3 + (depth - 2) / 3) vs Re-write v12: 171 - 122 - 207 + + Tests TODO: TT enabled vs TT disabled \ No newline at end of file diff --git a/To-do list.txt b/To-do list.txt index 22ff8eb..38f1cae 100644 --- a/To-do list.txt +++ b/To-do list.txt @@ -42,4 +42,5 @@ https://www.chessprogramming.org/Texel's_Tuning_Method https://www.chessprogramming.org/Enhanced_Transposition_Cutoff Some random resources I found: (Not using them right now but they could be useful) -https://analog-hors.github.io/site/magic-bitboards/ \ No newline at end of file +https://analog-hors.github.io/site/magic-bitboards/ +https://mediocrechess.blogspot.com/2006/12/guide-attacked-squares.html \ No newline at end of file diff --git a/src/board.rs b/src/board.rs index a7661ba..a4737e4 100644 --- a/src/board.rs +++ b/src/board.rs @@ -20,7 +20,6 @@ pub struct Board { pub piece_bitboards: [u64; BITBOARD_COUNT], pub color_bitboards: [u64; 2], - pub attacked_squares_bitboards: [u64; 2], pub castling_rights: ValueHolder, pub fifty_move_counter: ValueHolder, @@ -28,13 +27,14 @@ pub struct Board { pub en_passant_file: usize, pub white_to_move: bool, - pub total_material_without_pawns: i32, + pub total_material_without_pawns: [i32; 2], pub zobrist: Zobrist, pub moves: Vec, - attacked_squares_calculated: [bool; 2], + pub attacked_squares_cache: [Option; 2], + // TODO: smaller version of the transposition table for evaluation cache? } impl Board { @@ -55,7 +55,6 @@ impl Board { piece_bitboards: [0; BITBOARD_COUNT], color_bitboards: [0; 2], - attacked_squares_bitboards: [0; 2], castling_rights: ValueHolder::new(castling_rights), fifty_move_counter: ValueHolder::new(fifty_move_counter), @@ -64,13 +63,13 @@ impl Board { // en_passant_file: if fen[3] == "-" { 0 } else { (coordinate_to_index(fen[3]) % 8) + 1 }, white_to_move: fen[1] == "w", - total_material_without_pawns: 0, + total_material_without_pawns: [0, 0], zobrist: Zobrist::default(), moves: vec![], - attacked_squares_calculated: [false; 2], + attacked_squares_cache: [None; 2], }; let piece_rows = fen[0].split('/').collect::>(); @@ -87,12 +86,13 @@ impl Board { board.color_bitboards[is_piece_white(piece) as usize] |= 1 << i; i += 1; + let piece_is_white = is_piece_white(piece); let piece_type = get_piece_type(piece); if piece_type != PAWN && piece_type != KING { let piece_worth = BASE_WORTHS_OF_PIECE_TYPE[piece_type]; - board.total_material_without_pawns += piece_worth; + board.total_material_without_pawns[piece_is_white as usize] += piece_worth; } } } @@ -111,12 +111,12 @@ impl Board { pub fn get_attacked_squares_for_color(&mut self, color: usize) -> u64 { self.calculate_attacked_squares_for_color(color); - self.attacked_squares_bitboards[color] + self.attacked_squares_cache[color].unwrap() } // This is SLOOOOOOOOOOOOOWWWWWWW :[ pub fn calculate_attacked_squares_for_color(&mut self, color: usize) { - if self.attacked_squares_calculated[color] { + if self.attacked_squares_cache[color].is_some() { return; } @@ -141,8 +141,7 @@ impl Board { } } - self.attacked_squares_bitboards[color] = attacked_squares; - self.attacked_squares_calculated[color] = true; + self.attacked_squares_cache[color] = Some(attacked_squares); } pub fn print(&self) { @@ -191,9 +190,10 @@ impl Board { print_bitboard("Black pieces", "1".bold().italic().white().on_black(), self.color_bitboards[0]); print_bitboard("White pieces", "1".bold().italic().normal().on_white(), self.color_bitboards[1]); - self.calculate_attacked_squares(); - print_bitboard("Black attacked squares", "1".bold().italic().white().on_black(), self.attacked_squares_bitboards[0]); - print_bitboard("White attacked squares", "1".bold().italic().normal().on_white(), self.attacked_squares_bitboards[1]); + let black_attacked_squares = self.get_attacked_squares_for_color(0); + let white_attacked_squares = self.get_attacked_squares_for_color(1); + print_bitboard("Black attacked squares", "1".bold().italic().white().on_black(), black_attacked_squares); + print_bitboard("White attacked squares", "1".bold().italic().normal().on_white(), white_attacked_squares); } pub fn get_last_move(&self) -> MoveData { @@ -256,7 +256,7 @@ impl Board { self.piece_bitboards[data.piece as usize] ^= 1 << data.to; } else { self.piece_bitboards[build_piece(piece_color == 1, data.flag as usize)] ^= 1 << data.to; - self.total_material_without_pawns += BASE_WORTHS_OF_PIECE_TYPE[data.flag as usize]; + self.total_material_without_pawns[piece_color] += BASE_WORTHS_OF_PIECE_TYPE[data.flag as usize]; } self.color_bitboards[piece_color] ^= 1 << data.from; @@ -265,7 +265,7 @@ impl Board { if data.capture != NO_PIECE as u8 { let capture_type = get_piece_type(data.capture as usize); if capture_type != PAWN { - self.total_material_without_pawns -= BASE_WORTHS_OF_PIECE_TYPE[capture_type]; + self.total_material_without_pawns[other_color] -= BASE_WORTHS_OF_PIECE_TYPE[capture_type]; } if data.flag == EN_PASSANT_FLAG { @@ -355,7 +355,7 @@ impl Board { self.castling_rights.history[self.castling_rights.index - 1], ); - self.attacked_squares_calculated = [false; 2]; + self.attacked_squares_cache = [None; 2]; self.moves.push(data); self.white_to_move = !self.white_to_move; @@ -383,7 +383,7 @@ impl Board { self.piece_bitboards[last_move.piece as usize] ^= 1 << last_move.to; } else { self.piece_bitboards[build_piece(piece_color == 1, last_move.flag as usize)] ^= 1 << last_move.to; - self.total_material_without_pawns -= BASE_WORTHS_OF_PIECE_TYPE[last_move.flag as usize]; + self.total_material_without_pawns[piece_color] -= BASE_WORTHS_OF_PIECE_TYPE[last_move.flag as usize]; } self.color_bitboards[piece_color] ^= 1 << last_move.from; @@ -392,7 +392,7 @@ impl Board { if last_move.capture != NO_PIECE as u8 { let capture_type = get_piece_type(last_move.capture as usize); if capture_type != PAWN { - self.total_material_without_pawns += BASE_WORTHS_OF_PIECE_TYPE[capture_type]; + self.total_material_without_pawns[other_color] += BASE_WORTHS_OF_PIECE_TYPE[capture_type]; } if last_move.flag == EN_PASSANT_FLAG { @@ -409,35 +409,33 @@ impl Board { self.piece_bitboards[last_move.capture as usize] ^= 1 << last_move.to; self.color_bitboards[other_color] ^= 1 << last_move.to; } - } else { - if last_move.flag == SHORT_CASTLE_FLAG { - if piece_color == 1 { - self.piece_bitboards[WHITE_ROOK] ^= 1 << 63; - self.piece_bitboards[WHITE_ROOK] ^= 1 << 61; + } else if last_move.flag == SHORT_CASTLE_FLAG { + if piece_color == 1 { + self.piece_bitboards[WHITE_ROOK] ^= 1 << 63; + self.piece_bitboards[WHITE_ROOK] ^= 1 << 61; - self.color_bitboards[1] ^= 1 << 63; - self.color_bitboards[1] ^= 1 << 61; - } else { - self.piece_bitboards[BLACK_ROOK] ^= 1 << 7; - self.piece_bitboards[BLACK_ROOK] ^= 1 << 5; + self.color_bitboards[1] ^= 1 << 63; + self.color_bitboards[1] ^= 1 << 61; + } else { + self.piece_bitboards[BLACK_ROOK] ^= 1 << 7; + self.piece_bitboards[BLACK_ROOK] ^= 1 << 5; - self.color_bitboards[0] ^= 1 << 7; - self.color_bitboards[0] ^= 1 << 5; - } - } else if last_move.flag == LONG_CASTLE_FLAG { - if piece_color == 1 { - self.piece_bitboards[WHITE_ROOK] ^= 1 << 56; - self.piece_bitboards[WHITE_ROOK] ^= 1 << 59; + self.color_bitboards[0] ^= 1 << 7; + self.color_bitboards[0] ^= 1 << 5; + } + } else if last_move.flag == LONG_CASTLE_FLAG { + if piece_color == 1 { + self.piece_bitboards[WHITE_ROOK] ^= 1 << 56; + self.piece_bitboards[WHITE_ROOK] ^= 1 << 59; - self.color_bitboards[1] ^= 1 << 56; - self.color_bitboards[1] ^= 1 << 59; - } else { - self.piece_bitboards[BLACK_ROOK] ^= 1; // << 0 - self.piece_bitboards[BLACK_ROOK] ^= 1 << 3; + self.color_bitboards[1] ^= 1 << 56; + self.color_bitboards[1] ^= 1 << 59; + } else { + self.piece_bitboards[BLACK_ROOK] ^= 1; // << 0 + self.piece_bitboards[BLACK_ROOK] ^= 1 << 3; - self.color_bitboards[0] ^= 1; - self.color_bitboards[0] ^= 1 << 3; - } + self.color_bitboards[0] ^= 1; + self.color_bitboards[0] ^= 1 << 3; } } @@ -445,7 +443,7 @@ impl Board { self.castling_rights.pop(); self.zobrist.key.pop(); - self.attacked_squares_calculated = [false; 2]; + self.attacked_squares_cache = [None; 2]; self.white_to_move = !self.white_to_move; @@ -890,7 +888,7 @@ impl Board { // Returns a value between 0.0 and 1.0 to reflect whether you're in an endgame or not // the closer to 1.0, the more of an endgame it is pub fn endgame_multiplier(&self) -> f32 { - (1.5 - self.total_material_without_pawns as f32 * (0.9 / MAX_ENDGAME_MATERIAL as f32)).clamp(0.0, 1.0) + (1.5 - self.total_material_without_pawns.iter().sum::() as f32 * (0.9 / MAX_ENDGAME_MATERIAL)).clamp(0.0, 1.0) // (1.0 - self.total_material_without_pawns as f32 * (1.0 / MAX_ENDGAME_MATERIAL)).clamp(0.0, 1.0) } @@ -955,27 +953,27 @@ impl Board { white_pawn_evaluation = (white_pawn_evaluation as f32 * pawn_evaluation_multiplier) as i32; black_pawn_evaluation = (black_pawn_evaluation as f32 * pawn_evaluation_multiplier) as i32; - self.calculate_attacked_squares(); + let white_attacks_bitboard = self.get_attacked_squares_for_color(1); + let black_attacks_bitboard = self.get_attacked_squares_for_color(0); - // TODO: try taking the square root of this? - let white_attacked_squares = self.attacked_squares_bitboards[1].count_ones() as i32; - let black_attacked_squares = self.attacked_squares_bitboards[0].count_ones() as i32; + // Taking the sqrt of this made it worse + let white_attacks_score = white_attacks_bitboard.count_ones() as i32 * 10; + let black_attacks_score = black_attacks_bitboard.count_ones() as i32 * 10; let white_king_index = get_lsb(self.piece_bitboards[WHITE_KING]) as usize; let black_king_index = get_lsb(self.piece_bitboards[BLACK_KING]) as usize; // TODO: weak squares, weak lines, or none? // TODO: Or count how many friendly pieces are around the king? - - let weak_squares_around_white_king = (( + let white_king_weakness_penalty = (( self.precalculated_move_data.king_attacks[white_king_index] - & self.attacked_squares_bitboards[0] - ).count_ones() as f32 * (1.0 - endgame)) as i32; + & black_attacks_bitboard + ).count_ones() as f32 * (1.0 - endgame)) as i32 * 20; - let weak_squares_around_black_king = (( + let black_king_weakness_penalty = (( self.precalculated_move_data.king_attacks[black_king_index] - & self.attacked_squares_bitboards[1] - ).count_ones() as f32 * (1.0 - endgame)) as i32; + & white_attacks_bitboard + ).count_ones() as f32 * (1.0 - endgame)) as i32 * 20; // let weak_lines_from_white_king = (self.calculate_queen_attack_bitboard(white_king_index).count_ones() as f32 * (1.0 - endgame)) as i32; // let weak_lines_from_black_king = (self.calculate_queen_attack_bitboard(black_king_index).count_ones() as f32 * (1.0 - endgame)) as i32; @@ -984,15 +982,15 @@ impl Board { // TODO: rooks on open lines - ((white_material + white_attacked_squares * 10 - weak_squares_around_white_king * 20 + white_pawn_evaluation) - - (black_material + black_attacked_squares * 10 - weak_squares_around_black_king * 20 + black_pawn_evaluation)) * self.perspective() + ((white_material + white_attacks_score - white_king_weakness_penalty + white_pawn_evaluation) + - (black_material + black_attacks_score - black_king_weakness_penalty + black_pawn_evaluation)) * self.perspective() } pub fn can_short_castle(&mut self, white: bool) -> bool { // self.king_in_check calculates attacked squares !self.king_in_check(white) && self.castling_rights.current & SHORT_CASTLING_RIGHTS[white as usize] != 0 - && (self.occupied_bitboard() | self.attacked_squares_bitboards[(!white) as usize]) & SHORT_CASTLE_MASK[white as usize] == 0 + && (self.occupied_bitboard() | self.attacked_squares_cache[(!white) as usize].unwrap()) & SHORT_CASTLE_MASK[white as usize] == 0 } pub fn can_long_castle(&mut self, white: bool) -> bool { @@ -1000,11 +998,11 @@ impl Board { !self.king_in_check(white) && self.castling_rights.current & LONG_CASTLING_RIGHTS[white as usize] != 0 && EXTRA_LONG_CASTLE_SQUARE_CHECK[white as usize] & occupied == 0 - && (occupied | self.attacked_squares_bitboards[(!white) as usize]) & LONG_CASTLE_MASK[white as usize] == 0 + && (occupied | self.attacked_squares_cache[(!white) as usize].unwrap()) & LONG_CASTLE_MASK[white as usize] == 0 } pub fn insufficient_checkmating_material(&self) -> bool { - self.total_material_without_pawns < ROOK_WORTH + self.total_material_without_pawns.iter().sum::() < ROOK_WORTH && self.piece_bitboards[WHITE_PAWN] == 0 && self.piece_bitboards[BLACK_PAWN] == 0 } diff --git a/src/bot.rs b/src/bot.rs index 224c88c..23991ed 100644 --- a/src/bot.rs +++ b/src/bot.rs @@ -255,7 +255,8 @@ impl Bot { } // Internal Iterative Reductions - // if depth > 3 + // if not_pv + // && depth > 2 // && hash_move.is_none() { // depth -= 1; // } @@ -270,26 +271,42 @@ impl Bot { // time on PV lines? if not_pv && depth > 0 - // && !evaluation_is_mate(alpha) // TODO - // && !evaluation_is_mate(beta) // TODO + && !evaluation_is_mate(beta) && !board.king_in_check(board.white_to_move) { let static_eval = board.evaluate(); - // if not_pv { // ? + // TODO: work on this next // Reverse Futility Pruning - if depth < 8 // TODO: 6? - && static_eval - 60 * (depth as i32) >= beta { // TODO: tweak this threshold + if depth < 8 + && static_eval - 60 * (depth as i32) >= beta { return static_eval; } + // Strelka Razoring (Slightly modified) + // if depth < 4 { + // let razoring_threshold = static_eval + + // if depth == 1 { + // 150 + // } else { + // 350 + // }; + + // if razoring_threshold < beta { + // let evaluation = self.quiescence_search(board, alpha, beta); + // if evaluation < beta { + // return i32::max(evaluation, razoring_threshold); + // } + // } + // } + // Null Move Pruning - if depth > 2 - && static_eval >= beta - && board.total_material_without_pawns > 0 - // && board.endgame_multiplier() < 1.0 // TODO + if depth > 1 + && static_eval >= beta // Fruit used || depth > X here, but I haven't found great results with that + && board.total_material_without_pawns[board.white_to_move as usize] > 0 && board.get_last_move().capture == NO_PIECE as u8 // Moving the check from the above check to only NMP was a decent improvement && board.try_null_move() { - let evaluation = -self.alpha_beta_search(board, depth - 3, ply + 1, -beta, -beta + 1, total_extensions); + let reduction = 3 + (depth - 2) / 3; + let evaluation = -self.alpha_beta_search(board, depth.saturating_sub(reduction), ply + 1, -beta, -beta + 1, total_extensions); board.undo_null_move(); @@ -298,28 +315,10 @@ impl Bot { } } - // Random NMP testing - // if depth > 1 - // && (static_eval >= beta || depth < 5) - // && board.total_material_without_pawns > 0 - // && !evaluation_is_mate(beta) - // // && board.endgame_multiplier() < 1.0 // TODO - // && board.get_last_move().capture == NO_PIECE as u8 // Moving the check from the above check to only NMP was a decent improvement - // && board.try_null_move() { - // let reduction = 3 + depth / 10; - // let evaluation = -self.alpha_beta_search(board, depth.saturating_sub(reduction), ply + 1, -beta, -beta + 1, total_extensions); - - // board.undo_null_move(); - - // if evaluation >= beta { - // return evaluation; - // } - // } - // } - + // TODO: definitely work on this, maybe allow it outside pv nodes? // Razoring if depth < 4 - && static_eval + 300 * (depth as i32) < alpha { // TODO: tweak this threshold + && static_eval + 300 * (depth as i32) < alpha { // TODO: tweak this depth -= 1; } } @@ -362,7 +361,7 @@ impl Bot { } let mut extension = 0; - if total_extensions < MAX_SEARCH_EXTENSIONS as u8 { + if total_extensions < MAX_SEARCH_EXTENSIONS { if board.king_in_check(board.white_to_move) { extension = 1; } else if m.piece == PAWN as u8 { @@ -379,7 +378,6 @@ impl Bot { // Late Move Reductions if legal_moves_found > 3 && depth > 1 - // && depth > 2 && extension == 0 && m.capture == NO_PIECE as u8 { let mut reduction = 2; @@ -389,7 +387,7 @@ impl Bot { } if not_pv { - reduction += 1; + reduction += 1; // TODO: + depth / 6 } evaluation = -self.alpha_beta_search(board, depth.saturating_sub(reduction), ply + 1, -alpha - 1, -alpha, total_extensions); diff --git a/src/main.rs b/src/main.rs index d6e2105..a47bb9e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,6 +23,7 @@ mod pv_table; mod move_sorter; mod move_list; +use crate::utils::move_str_is_valid; use crate::castling_rights::print_castling_rights; use crate::bot::{Bot, BotConfig, MAX_SEARCH_EXTENSIONS}; use crate::perft::*; @@ -64,6 +65,7 @@ fn main() { // log.write(format!("Got command: {}\n", command)); + // The length of this Vec will always be > 0 let command_split = command.trim() .split(' ') .collect::>(); @@ -80,15 +82,19 @@ fn main() { } "setoption" => { - // setoption name Hash value 32 - // I KNOW this is horrible I'm gonna rewrite all this eventually + // 0 1 2 3 4 + // setoption name