diff --git a/.gitignore b/.gitignore
index 6a7808d..996a570 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,4 +13,4 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
-sharpener/
\ No newline at end of file
+sharpener
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index 00ea054..a9fce0b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "maxwell"
-version = "3.0.8"
+version = "3.1.0"
edition = "2021"
[dependencies]
diff --git a/README.md b/README.md
index c4f4bd8..e7f2a88 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,30 @@
![](/icon/Maxwell_316x316.png)
# Maxwell Chess Engine
- A Chess engine written from scratch in Rust!
+ A Chess engine written from scratch in Rust.
If you use this code verbatim, or as a reference, please credit me!
+
+ 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)
+## Rough Roadmap
+ - Tweak search values & thresholds
+ - Internal Iterative Deepening
+ - Static Exchange Evaluation
+ - Late Move Pruning
+ - Write an NNUE implementation! I've learned how Neural Networks work, so I'm really excited to get started on that
+ - Multithreading
+
## Features
#### 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)
+ - hash_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
@@ -21,6 +33,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
@@ -28,36 +41,58 @@
- 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 whatever move from the transposition table
- MVV-LVA
- - 2 Killer moves
- - History heuristic
- - Castling
- - Promotions
- - Penalty for moving a piece to a square an opponent's piece attacks
+ - 2 Killer Moves
+ - History Heuristic
+ - Indexed by side to move, move start square, move end square
#### 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
- - No set max size, but entries get removed after 10 moves without hits
- - Null move pruning
+ - Single Threaded ~ For now ;)
+ - Negamax
+ - Iterative Deepening
+ - Alpha-Beta Pruning
+ - Late Move Reductions
+ - Principal Variation Search
+ - Reverse Futility Pruning (Static Null Move Pruning)
+ - Null Move Pruning
- Razoring
- - Reverse futility pruning
- - Late move reduction
- - Search extensions
- - Promotions
+ - Internal Iterative Reductions
+ - Quiescence Search
+ - Delta Pruning
+ - No TT Lookups
+ - Transposition Table
+ - UCI "Hash" option to change max size, default is 256 MB
+ - Replacement scheme prefers higher depth and exact evaluation bound
+ - Search Extensions
- 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.05 and 30.0 seconds
-## Helpful Sources
+## Helpful Sources & References
+ #### Thanks to Sebastian Lague for making his YouTube series, which inspired me to make my own engine!
- [Sebastian Lague's Chess Programming series](https://www.youtube.com/playlist?list=PLFt_AvWsXl0cvHyu32ajwh2qU1i6hl77c)
+
+ #### When I'm not sure what to do next, I like to read through other engine's code for ideas. I try not to copy line for line, but in any case here are the engine's I've referenced:
+ - [Boychesser](https://github.com/analog-hors/Boychesser/)
+ - [Weiawaga](https://github.com/Heiaha/Weiawaga/)
+ - [Rustic (Engine and Book)](https://github.com/mvanthoor/rustic)
+ - [Lynx](https://github.com/lynx-chess/Lynx/)
+ - [Fruit 2.1](https://github.com/Warpten/Fruit-2.1/)
+ - [Tcheran](https://github.com/jgilchrist/tcheran/)
+ - [MadChess](https://github.com/ekmadsen/MadChess/)
+ - [Black Marlin](https://github.com/jnlt3/blackmarlin/)
+ - [Ethereal](https://github.com/AndyGrant/Ethereal/)
+
+ #### And some other helpful resources
- [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
+ - [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/)
+ - [Chess Programming Reddit](https://www.reddit.com/r/chessprogramming/)
+ - [TalkChess Forum](https://talkchess.com/forum3/index.php)
+ - [Stockfish Features List](https://www.chessprogramming.org/Stockfish#Search)
\ No newline at end of file
diff --git a/Test results.txt b/Test results.txt
new file mode 100644
index 0000000..54f3cbd
--- /dev/null
+++ b/Test results.txt
@@ -0,0 +1,207 @@
+For X - Y - Z the order is: Wins, Losses, Draws
+I used to use 30+0.5 and 10+0.2, but lately I've been using 8+0.08
+================================================================
+
+
+Re-write 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%
+
+
+This is for when to return a TT evaluation
+Re-write v3 (Not root) vs Re-write v3 (Not PV): 131 - 60 - 109
+Re-write v3 (Not root) vs Re-write v3 (All): 33 - 33 - 36
+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%
+
+
+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%
+
+
+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%
+
+
+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
+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%
+
+
+Re-write v7 (RFP before NMP) vs Re-write v6: 136 - 120 - 144
+
+
+Re-write v8 (New PVS) vs Re-write v7: 161 - 117 - 122
+
+
+Re-write v9 (LMR & PVS changes) vs Re-write v8 (New PVS): 418 - 241 - 341
+
+
+Re-write v10 vs Re-write v9: 138 - 135 - 127
+
+
+Re-write v10 vs Maxwell v3.0.8: 754 - 171 - 331 (1256 games!)
+
+
+Re-write v10 vs Added extension to PVS searches: 99 - 85 - 116 (?)
+
+
+Re-write v11 vs Re-write v10: 376 - 343 - 481
+
+
+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
+
+
+v12 vs v11 was equal, because v12 was just a bit of code cleanup
+
+
+Strelka razoring vs Re-write v12: 234 - 304 - 368
+
+
+NMP changes (> 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
+
+
+History indexed by [from][to] vs Re-write v13: 76 - 77 - 113
+
+
+Re-write v14 (History indexed by [side to move][from][to]) vs Re-write v13: 152 - 125 - 223
+
+
+The move order penalty when the target square is attacked by the enemy
+ Elo +/- Games Points Wins Draws
+Re-write v15 (Removed) 27 16 1000 539.0 320 438
+Re-write v14 -13 16 1000 481.0 259 444
+Re-write v15 (Quiets only) -14 16 1000 480.0 262 436
+
+
+Countermoves vs Re-write v15: 122 - 177 - 201
+
+
+v16 changed TT to use a Vec instead of a HashMap, so there should be no strength difference
+
+
+Re-write v17 (Re-wrote killer moves) vs Re-write v16: 173 - 122 - 205
+
+
+Re-write v17 vs Maxwell v3.0.8 (Patch 2): 968 - 136 - 476 (1580 games, and an estimated +190 Elo!!)
+
+
+Count plies since null moves in is_repetition vs Re-write v17: 140 - 149 - 211
+
+
+Re-write v18 (Add null moves to repetition lookback) vs Re-write v17: 350 - 327 - 523
+
+
+Re-write v19 (FINALLY FIXED REPETITION DETECTION) vs Re-write v18: 509 - 429 - 662
+
+
+Incremental move sorter like Rustic vs Re-write v19: 153 - 171 - 176
+
+
+Extensions outside the move loop vs Re-write v19: 576 - 581 - 593
+
+
+Only check extension, outside the move loop vs Re-write v19: 272 - 276 - 252
+
+
+Re-write v20 (Rewrote extension code) vs Re-write v19: 171 - 163 - 166
+
+
+(Re-write v20's RFP Threshold is 60 centipawns per ply)
+RFP = 80 vs Re-write v20: 165 - 172 - 173
+RFP = 90 vs Re-write v20: 168 - 181 - 151
+RFP = 100 vs Re-write v20: 423 - 519 - 508
+
+At this point I changed RFP to return beta (because of fail hard)
+
+RFP = 16 * (depth - 1).pow(2) + 50 vs Re-write v20: 196 - 217 - 191
+RFP = 30 * (depth - 1).pow(2) + 60 vs Re-write v20: 133 - 150 - 176
+RFP = 20 * (depth - 1).pow(2) + 65 vs Re-write v20: 140 - 163 - 147
+RFP = 40 * (depth - 1).pow(2) + 70 vs Re-write v20: 486 - 498 - 516
+
+FINALLY
+Re-write v21 (RFP = 50 * (depth - 1).pow(2) + 55) vs Re-write v20: 276 - 252 - 272
+
+
+Hmm
+Store Alpha bound TT entries vs Re-write v21: 283 - 308 - 311
+
+
+No TT Cutoff on PV-nodes vs Re-write v21: 514 - 521 - 573
+
+
+Internal Iterative Reductions
+ Only on PV, Depth > 2, Depth -= 1 vs Re-write v21: 254 - 255 - 253
+ Re-write v22 (All nodes, Depth > 1, Depth -= 1) vs Re-write v21: 285 - 234 - 247
+ All nodes, Depth > 2, Depth -= 2 vs Re-write v22: It lost horribly :P
+
+
+Only return TT eval if not PV, or depth == 0 vs Re-write v22: 498 - 554 - 618
+
+
+RFP = 55 * (depth - 1).pow(2) + 60 vs Re-write v22: 142 - 145 - 153
+
+
+Razoring change (depth -= 2) vs Re-write v22: 533 - 556 - 687
+Razoring change (threshold <= alpha) vs Re-write v22: 122 - 132 - 146
+Razoring change (285 + 200 * (depth - 1)) vs Re-write v22: 205 - 198 - 207
+Razoring change (300 + 150 * (depth - 1)) vs Re-write v22: 265 - 225 - 266
+
+
+Only NMP if no hash move vs Re-write v23: 485 - 520 - 597
+
+
+Aspiration Windows
+ Elo +/- Games Points Wins Draws Draw
+AW=30 12 24 500 258.5 165 187 37.4%
+AW=60 -2 25 500 248.5 167 163 32.6%
+Re-write v23 (40) -5 25 500 246.5 164 165 33.0%
+AW=50 -5 24 500 246.5 148 197 39.4%
+
+
+Hmm
+AW=30 vs Re-write v32: 147 - 148 - 145
+
+
+Maxwell v3.1 (Re-write v23) vs Maxwell v3.0.8 (Patch 2): 1533 - 155 - 312
+Elo difference: 293.9 +/- 17.6, LOS: 100.0 %
+
+
+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..8d6ae07
--- /dev/null
+++ b/To-do list.txt
@@ -0,0 +1,44 @@
+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
+
+ Update:
+ I've learned how to write neural networks, and trained one to evaluate Tic-Tac-Toe positions!
+ So for either v3.2 or v3.3 I'm gonna work on writing my own version of NNUE from scratch
+
+figure out some sort of multithreading:
+ to implement pondering I think I'll have to add multithreading
+
+ maybe one thread that waits for UCI commands like "stop"
+ and all the rest of the threads search
+
+ https://www.chessprogramming.org/Lazy_SMP
+
+transposition table:
+ buckets
+ aging
+ make it multithreading safe
+ prefetching
+
+try removing all the attacked squares bitboards stuff, and just make a function that detects whether one square is attacked?
+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
+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
+ I tried this a couple times, but haven't got it faster than my current solution
+re-implement PV table with a different approach; I don't like the 2d array
+
+History reductions / pruning
+https://www.chessprogramming.org/Internal_Iterative_Deepening
+https://www.chessprogramming.org/Static_Exchange_Evaluation
+https://www.chessprogramming.org/Futility_Pruning#MoveCountBasedPruning (Late move pruning)
+https://www.chessprogramming.org/History_Leaf_Pruning
+https://www.chessprogramming.org/ProbCut
+https://www.chessprogramming.org/Razoring#Strelka
+https://www.chessprogramming.org/Texel's_Tuning_Method
+
+Some random resources I found: (Not using them right now but they could be useful)
+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 140a21a..d02bb64 100644
--- a/src/board.rs
+++ b/src/board.rs
@@ -1,6 +1,6 @@
use crate::value_holder::ValueHolder;
-use crate::utils::{pop_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::utils::{pop_lsb, get_lsb, print_bitboard, coordinate_to_index};
+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;
@@ -8,34 +8,47 @@ use crate::pieces::*;
use crate::castling_rights::*;
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
+
+#[derive(Copy, Clone)]
+pub struct BoardState {
+ pub castling_rights: u8,
+ pub fifty_move_counter: u8,
+ pub attacked_squares: [Option; 2],
+}
+
+impl BoardState {
+ pub fn new(castling_rights: u8, fifty_move_counter: u8) -> Self {
+ Self {
+ castling_rights,
+ fifty_move_counter,
+ attacked_squares: [None, None],
+ }
+ }
+}
pub struct Board {
pub precalculated_move_data: PrecalculatedMoveData,
- pub piece_bitboards: [u64; BITBOARD_COUNT],
+ pub piece_bitboards: [u64; PIECE_COUNT],
pub color_bitboards: [u64; 2],
- pub attacked_squares_bitboards: [u64; 2],
-
- pub castling_rights: ValueHolder,
- pub fifty_move_draw: ValueHolder,
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],
+ // TODO: smaller version of the transposition table for evaluation cache?
+
+ pub board_state: ValueHolder,
}
impl Board {
@@ -49,29 +62,25 @@ 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(),
- piece_bitboards: [0; BITBOARD_COUNT],
+ piece_bitboards: [0; PIECE_COUNT],
color_bitboards: [0; 2],
- attacked_squares_bitboards: [0; 2],
-
- castling_rights: ValueHolder::new(castling_rights),
- fifty_move_draw: ValueHolder::new(fifty_move_draw),
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 },
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],
+ board_state: ValueHolder::new(BoardState::new(castling_rights, fifty_move_counter)),
};
let piece_rows = fen[0].split('/').collect::>();
@@ -88,22 +97,19 @@ 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 = get_base_worth_of_piece(piece);
- board.total_material_without_pawns += piece_worth;
+ let piece_worth = BASE_WORTHS_OF_PIECE_TYPE[piece_type];
+ board.total_material_without_pawns[piece_is_white as usize] += piece_worth;
}
}
}
}
- // 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
@@ -114,9 +120,14 @@ 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.board_state.current.attacked_squares[color].unwrap()
+ }
+
// This is SLOOOOOOOOOOOOOWWWWWWW :[
pub fn calculate_attacked_squares_for_color(&mut self, color: usize) {
- if self.attacked_squares_calculated[color] {
+ if self.board_state.current.attacked_squares[color].is_some() {
return;
}
@@ -141,8 +152,7 @@ impl Board {
}
}
- self.attacked_squares_bitboards[color] = attacked_squares;
- self.attacked_squares_calculated[color] = true;
+ self.board_state.current.attacked_squares[color] = Some(attacked_squares);
}
pub fn print(&self) {
@@ -174,8 +184,8 @@ impl Board {
println!("{}", output);
}
- pub fn print_bitboards(&self) {
- for piece in 0..BITBOARD_COUNT {
+ pub fn print_bitboards(&mut self) {
+ for piece in 0..PIECE_COUNT {
let c = piece_to_char(piece);
print_bitboard(
&format!("{}", c),
@@ -190,8 +200,11 @@ 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]);
- 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 {
@@ -224,45 +237,42 @@ 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;
}
}
}
- println!("Illegal move");
+ println!("Illegal move: {}", data.to_coordinates());
false
}
- pub fn make_move(&mut self, data: MoveData) {
- let piece_color = is_piece_white(data.piece as usize) as usize;
- let other_color = !is_piece_white(data.piece as usize) as usize;
-
- // if data.piece >= NO_PIECE as u8 {
- // println!("Illegal piece! Move: {:#?}", data);
- // return;
- // }
+ pub fn make_move(&mut self, data: MoveData) -> bool {
+ let piece_is_white = is_piece_white(data.piece as usize);
+ let piece_color = piece_is_white as usize;
+ let other_color = (!piece_is_white) as usize;
self.piece_bitboards[data.piece as usize] ^= 1 << data.from;
if !PROMOTABLE.contains(&data.flag) {
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.piece_bitboards[build_piece(piece_is_white, data.flag as usize)] ^= 1 << data.to;
+ self.total_material_without_pawns[piece_color] += 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[other_color] -= BASE_WORTHS_OF_PIECE_TYPE[capture_type];
}
if data.flag == EN_PASSANT_FLAG {
@@ -282,7 +292,7 @@ impl Board {
// I dunno if there's a better way to do this :/
if data.piece == WHITE_KING as u8 {
- self.castling_rights.current &= !ALL_WHITE_CASTLING_RIGHTS;
+ self.board_state.current.castling_rights &= !ALL_WHITE_CASTLING_RIGHTS;
if data.flag == SHORT_CASTLE_FLAG {
self.piece_bitboards[WHITE_ROOK] ^= 1 << 63;
@@ -298,7 +308,7 @@ impl Board {
self.color_bitboards[1] ^= 1 << 59;
}
} else if data.piece == BLACK_KING as u8 {
- self.castling_rights.current &= !ALL_BLACK_CASTLING_RIGHTS;
+ self.board_state.current.castling_rights &= !ALL_BLACK_CASTLING_RIGHTS;
if data.flag == SHORT_CASTLE_FLAG {
self.piece_bitboards[BLACK_ROOK] ^= 1 << 7;
@@ -315,47 +325,53 @@ impl Board {
}
}
+ // This is so ugly :`(
if data.from == 0 {
- self.castling_rights.current &= !BLACK_CASTLE_LONG;
+ self.board_state.current.castling_rights &= !BLACK_CASTLE_LONG;
} else if data.from == 7 {
- self.castling_rights.current &= !BLACK_CASTLE_SHORT;
+ self.board_state.current.castling_rights &= !BLACK_CASTLE_SHORT;
} else if data.from == 56 {
- self.castling_rights.current &= !WHITE_CASTLE_LONG;
+ self.board_state.current.castling_rights &= !WHITE_CASTLE_LONG;
} else if data.from == 63 {
- self.castling_rights.current &= !WHITE_CASTLE_SHORT;
+ self.board_state.current.castling_rights &= !WHITE_CASTLE_SHORT;
}
if data.to == 0 {
- self.castling_rights.current &= !BLACK_CASTLE_LONG;
+ self.board_state.current.castling_rights &= !BLACK_CASTLE_LONG;
} else if data.to == 7 {
- self.castling_rights.current &= !BLACK_CASTLE_SHORT;
+ self.board_state.current.castling_rights &= !BLACK_CASTLE_SHORT;
} else if data.to == 56 {
- self.castling_rights.current &= !WHITE_CASTLE_LONG;
+ self.board_state.current.castling_rights &= !WHITE_CASTLE_LONG;
} else if data.to == 63 {
- self.castling_rights.current &= !WHITE_CASTLE_SHORT;
+ self.board_state.current.castling_rights &= !WHITE_CASTLE_SHORT;
}
- if data.capture == NO_PIECE as u8
- && get_piece_type(data.piece as usize) != PAWN {
- self.fifty_move_draw.current += 1;
+ if data.capture != NO_PIECE as u8
+ || get_piece_type(data.piece as usize) == PAWN {
+ self.board_state.current.fifty_move_counter = 0;
} else {
- self.fifty_move_draw.current = 0;
+ self.board_state.current.fifty_move_counter += 1;
}
- self.fifty_move_draw.push();
- self.castling_rights.push();
+ self.board_state.current.attacked_squares = [None; 2];
+ self.board_state.push();
self.zobrist.make_move(
data,
self.get_last_move(),
- self.castling_rights.current,
- self.castling_rights.history[self.castling_rights.index - 1],
+ self.board_state.current.castling_rights,
+ self.board_state.history[self.board_state.index - 1].castling_rights,
);
- self.attacked_squares_calculated = [false; 2];
-
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 {
@@ -364,24 +380,26 @@ impl Board {
}
let last_move = self.moves.pop().unwrap();
- let piece_color = is_piece_white(last_move.piece as usize) as usize;
- let other_color = !is_piece_white(last_move.piece as usize) as usize;
+ let piece_is_white = is_piece_white(last_move.piece as usize);
+ let piece_color = piece_is_white as usize;
+ let other_color = (!piece_is_white) as usize;
self.piece_bitboards[last_move.piece as usize] ^= 1 << last_move.from;
if !PROMOTABLE.contains(&last_move.flag) {
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.piece_bitboards[build_piece(piece_is_white, last_move.flag as usize)] ^= 1 << last_move.to;
+ 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;
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[other_color] += BASE_WORTHS_OF_PIECE_TYPE[capture_type];
}
if last_move.flag == EN_PASSANT_FLAG {
@@ -398,43 +416,38 @@ 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;
}
}
- self.fifty_move_draw.pop();
- self.castling_rights.pop();
- self.zobrist.pop();
-
- self.attacked_squares_calculated = [false; 2];
+ self.board_state.pop();
+ self.zobrist.key.pop();
self.white_to_move = !self.white_to_move;
@@ -442,11 +455,11 @@ 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 {
+ 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 {
@@ -460,14 +473,24 @@ 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
}
- 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);
@@ -842,22 +865,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
}
@@ -885,7 +892,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)
}
@@ -922,7 +929,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 {
@@ -946,56 +953,67 @@ 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;
- 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);
- 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 = 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 = ((
+ // TODO: weak squares, weak lines, or none?
+ // TODO: Or count how many friendly pieces are around the 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;
- ((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()
+ // 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: rooks on open lines
+
+ ((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.board_state.current.castling_rights & SHORT_CASTLING_RIGHTS[white as usize] != 0
+ && (self.occupied_bitboard() | self.board_state.current.attacked_squares[(!white) as usize].unwrap()) & SHORT_CASTLE_MASK[white as usize] == 0
}
pub fn can_long_castle(&mut self, white: bool) -> bool {
let occupied = self.occupied_bitboard();
!self.king_in_check(white)
- && self.castling_rights.current & LONG_CASTLING_RIGHTS[white as usize] != 0
+ && self.board_state.current.castling_rights & 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.board_state.current.attacked_squares[(!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
}
+ // Must be called when not in check!
pub fn try_null_move(&mut self) -> bool {
- if self.king_in_check(self.white_to_move)
- || self.king_in_check(!self.white_to_move) {
+ if self.get_last_move() == NULL_MOVE {
return false;
}
@@ -1003,12 +1021,83 @@ impl Board {
self.zobrist.make_null_move();
self.moves.push(NULL_MOVE);
+ self.board_state.current.fifty_move_counter = 0;
+ self.board_state.push();
+
true
}
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();
+
+ self.board_state.pop();
+ }
+
+ // Counting only one repetition as a draw seems to perform better than detecting a threefold repetition
+ pub fn is_repetition(&self) -> bool {
+ // Before the third move, it's impossible to have a repetition
+ if self.zobrist.key.index < 2 {
+ return false;
+ }
+
+ let lookback = self.zobrist.key.index - self.board_state.current.fifty_move_counter as usize;
+ let mut i = self.zobrist.key.index - 2;
+
+ while i >= lookback {
+ if self.zobrist.key.history[i] == self.zobrist.key.current {
+ return true;
+ }
+
+ if i < 2 {
+ break;
+ }
+
+ i -= 2;
+ }
+
+ false
+ }
+
+ pub fn is_draw(&self) -> bool {
+ self.board_state.current.fifty_move_counter >= 100
+ || self.insufficient_checkmating_material()
+ || self.is_repetition()
+ }
+
+ // This isn't used anywhere and I haven't tested it, so it might be bugged
+ pub fn square_is_attacked_by_color(&self, square_bitboard: u64, white_pieces: bool) -> bool {
+ let color = white_pieces as usize;
+
+ let pieces =
+ if white_pieces {
+ WHITE_PAWN..=WHITE_KING
+ } else {
+ BLACK_PAWN..=BLACK_KING
+ };
+
+ for piece in pieces {
+ let piece_type = get_piece_type(piece);
+ let mut bitboard = self.piece_bitboards[piece];
+
+ while bitboard != 0 {
+ let piece_index = pop_lsb(&mut bitboard) as usize;
+
+ if (match piece_type {
+ PAWN => self.precalculated_move_data.pawn_attacks[color][piece_index],
+ KNIGHT => self.precalculated_move_data.knight_attacks[piece_index],
+ BISHOP => self.calculate_bishop_attack_bitboard(piece_index),
+ ROOK => self.calculate_rook_attack_bitboard(piece_index),
+ QUEEN => self.calculate_queen_attack_bitboard(piece_index),
+ KING => self.precalculated_move_data.king_attacks[piece_index],
+ _ => 0,
+ }) & square_bitboard != 0 {
+ return true;
+ }
+ }
+ }
+
+ false
}
}
\ No newline at end of file
diff --git a/src/bot.rs b/src/bot.rs
index ba51174..99e2281 100644
--- a/src/bot.rs
+++ b/src/bot.rs
@@ -1,23 +1,15 @@
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;
-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 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;
+pub const MAX_SEARCH_EXTENSIONS: u8 = 20;
#[derive(Clone, Debug)]
pub struct BotConfig {
@@ -25,16 +17,20 @@ pub struct BotConfig {
pub debug_output: bool,
pub opening_book: bool,
pub time_management: bool,
+ pub hash_size: usize,
}
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()) == _true,
time_management: Self::get_arg_value(&args, "time_management").unwrap_or(_true.clone()) == _true,
+ hash_size: (Self::get_arg_value(&args, "hash_size").unwrap_or("256".to_string())).parse::().unwrap_or(256),
}
}
@@ -65,22 +61,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 +80,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.hash_size),
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,
}
}
@@ -112,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 {
@@ -124,55 +114,51 @@ 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
+ 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.05, 30.0)
} else {
my_time
};
self.search_cancelled = false;
- let last_evaluation = self.evaluation;
-
self.best_move = NULL_MOVE;
- self.evaluation = 0;
self.positions_searched = 0;
self.quiescence_searched = 0;
- self.transposition_hits = 0;
+ self.transposition_table.hits = 0;
self.move_sorter.clear();
+ // TODO: tweak this
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;
-
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, 0, depth, alpha, beta, 0);
+ let evaluation = self.alpha_beta_search(board, current_depth, 0, alpha, beta, 0);
- if alpha < evaluation && evaluation < beta {
+ if evaluation > alpha
+ && evaluation < beta {
break;
}
window *= 4;
}
-
if !self.search_cancelled
|| self.searched_one_move {
self.best_move = self.best_move_this_iteration;
@@ -180,19 +166,19 @@ 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(),
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);
- if moves_until_mate <= depth {
+ let moves_until_mate = ply_from_mate(self.evaluation);
+ if moves_until_mate <= current_depth {
self.println(format!("Mate found in {}", (moves_until_mate as f32 * 0.5).ceil()));
break;
}
@@ -204,9 +190,20 @@ 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, because all moves get undone upon receiving the "position" command, but makes me happy :>
+ self.best_move = m;
+ break;
+ }
+ }
+ self.println("Failed to find a move in time, defaulting to first legal move :(".to_string());
+ }
+
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();
}
@@ -220,52 +217,94 @@ 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,
- number_of_extensions: u8,
+ 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 ply > 0 {
+ 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 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 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;
+ }
}
- if let Some(data) = self.transposition_table.lookup(board.zobrist.key, depth_left, depth, alpha, beta) {
- self.positions_searched -= 1;
- self.transposition_hits += 1;
+ let (tt_eval, hash_move) = self.transposition_table.lookup(board.zobrist.key.current, ply, 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 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;
}
- return data.evaluation;
+ // Internal Iterative Reductions
+ if depth > 1
+ && hash_move.is_none() {
+ depth -= 1;
+ }
}
- let is_pv = alpha != beta - 1;
+ // This detects a null / zero window search, which is used in non PV nodes
+ // 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;
+ let in_check = board.king_in_check(board.white_to_move);
- if !is_pv
+ 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) {
+ && !in_check
+ && !evaluation_is_mate(alpha)
+ && !evaluation_is_mate(beta) {
+ let static_eval = board.evaluate();
+
+ // Reverse Futility Pruning
+ if depth < 8 // TODO: mess around with this
+ && static_eval - (55 + 50 * (depth as i32 - 1).pow(2)) >= beta { // TODO: continue tweaking this
+ return beta;
+ }
+
+ // 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_left >= 3
+ 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 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);
+ 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();
@@ -274,85 +313,90 @@ 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 {
- return static_eval;
- }
-
// Razoring
- if depth_left <= 3
- && static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha {
- depth_left -= 1;
+ if depth < 4 // TODO: try different values for this
+ && static_eval + (300 + 150 * (depth as i32 - 1)) < alpha { // TODO: tweak this some more
+ depth -= 1;
}
}
- if depth_left == 0 {
- return self.quiescence_search(board, alpha, beta);
+ if depth == 0 {
+ return self.quiescence_search(board, alpha, beta, true);
}
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 hash_move =
- if depth == 0 {
+ // let mut eval_bound = EvalBound::UpperBound;
+
+ let sorted_moves = self.move_sorter.sort_moves(
+ board.white_to_move,
+ board.get_pseudo_legal_moves_for_color(board.white_to_move, false),
+ /*
+ 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 ply == 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
- };
+ hash_move.unwrap_or(NULL_MOVE)
+ },
+ ply as usize,
+ );
- let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, hash_move, depth);
+ let mut found_pv = false;
- for i in 0..sorted_moves.len() {
- let m = sorted_moves[i];
- board.make_move(m);
+ let mut legal_moves_found = 0;
+ for (_score, m) in sorted_moves {
+ if !board.make_move(m) {
+ continue;
+ }
- let mut search_extension = 0;
- if number_of_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;
- }
- }
- }
+ let mut extension = 0;
+ if board.king_in_check(board.white_to_move) {
+ extension += 1;
}
+ if m.piece == PAWN as u8 {
+ let rank = m.to / 8;
+ if rank == 1 || rank == 6 {
+ extension += 1;
+ }
+ }
+ extension = u8::min(extension, MAX_SEARCH_EXTENSIONS - total_extensions);
- // Late Move Reduction / (Kind of) Principal Variation Search
let mut evaluation = 0;
- let mut needs_full_search = true;
+ let mut needs_fuller_search = true;
- if search_extension == 0
- && i >= 3
- && depth_left >= 3
+ // Late Move Reductions
+ if legal_moves_found > 3
+ && depth > 1
+ && extension == 0
&& m.capture == NO_PIECE as u8 {
- evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 1 - 1, -alpha - 1, -alpha, number_of_extensions);
- needs_full_search = evaluation > alpha;
- }
+ let mut reduction = 2;
+
+ if found_pv {
+ reduction += 1;
+ }
+
+ if not_pv {
+ reduction += 1; // TODO: + depth / 6
+ }
- 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.saturating_sub(reduction), ply + 1, -alpha - 1, -alpha, total_extensions);
+ needs_fuller_search = evaluation > alpha; // && evaluation < beta?
}
+ // Principal Variation Search
+ if needs_fuller_search
+ && found_pv {
+ evaluation = -self.alpha_beta_search(board, depth + extension - 1, ply + 1, -alpha - 1, -alpha, total_extensions + extension);
+ needs_fuller_search = evaluation > alpha; // && evaluation < beta?
+ }
+ if needs_fuller_search {
+ evaluation = -self.alpha_beta_search(board, depth + extension - 1, ply + 1, -beta, -alpha, total_extensions + extension);
+ }
board.undo_last_move();
@@ -361,42 +405,58 @@ 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.current, depth, ply, beta, m, EvalBound::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;
+ self.move_sorter.add_killer_move(m, ply as usize);
+ self.move_sorter.history[board.white_to_move as usize][m.from as usize][m.to as usize] += (depth * depth) as i32;
}
return beta;
}
if evaluation > alpha {
+ found_pv = true;
+
best_move_this_search = m;
- node_type = NodeType::Exact;
+ // 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.best_move_depth_searched_at = depth_left;
self.evaluation_this_iteration = evaluation;
}
}
+
+ legal_moves_found += 1;
+ }
+
+ if legal_moves_found == 0 {
+ if in_check {
+ let mate_score = CHECKMATE_EVAL - ply as i32;
+ return -mate_score;
+ }
+ return 0;
}
+ // I've seen a small improvement if I don't store EvalBound::UpperBound, is this normal?
if best_move_this_search != NULL_MOVE {
- self.transposition_table.store(board.zobrist.key, depth_left, depth, alpha, best_move_this_search, node_type);
+ self.transposition_table.store(board.zobrist.key.current, depth, ply, alpha, best_move_this_search, EvalBound::Exact);
}
alpha
}
- fn quiescence_search(&mut self, board: &mut Board, mut alpha: i32, beta: i32) -> i32 {
+ fn quiescence_search(&mut self, board: &mut Board, mut alpha: i32, beta: i32, is_root: bool) -> i32 {
if self.should_cancel_search() {
return 0;
}
+ if is_root {
+ self.positions_searched -= 1;
+ }
+
self.quiescence_searched += 1;
let evaluation = board.evaluate();
@@ -408,29 +468,32 @@ impl Bot {
alpha = evaluation;
}
- let legal_moves = board.get_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;
}
- // 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);
-
- for m in sorted_moves {
+ let sorted_moves = self.move_sorter.sort_moves(board.white_to_move, moves, NULL_MOVE, usize::MAX);
+ for (_score, m) in sorted_moves {
// 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;
- }
+ let threshold = QUEEN_WORTH +
+ if PROMOTABLE.contains(&m.flag) {
+ QUEEN_WORTH - PAWN_WORTH
+ } else {
+ 0
+ };
if evaluation < alpha - threshold {
continue;
}
}
- board.make_move(m);
- let evaluation = -self.quiescence_search(board, -beta, -alpha);
+ if !board.make_move(m) {
+ continue;
+ }
+
+ let evaluation = -self.quiescence_search(board, -beta, -alpha, false);
board.undo_last_move();
if 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];
diff --git a/src/killer_moves.rs b/src/killer_moves.rs
index 5802761..af1bfe3 100644
--- a/src/killer_moves.rs
+++ b/src/killer_moves.rs
@@ -2,25 +2,26 @@ use crate::move_data::{MoveData, NULL_MOVE};
#[derive(Copy, Clone)]
pub struct KillerMoves {
- pub a: MoveData,
- pub b: MoveData,
+ pub moves: [MoveData; 2],
}
impl KillerMoves {
pub fn new() -> Self {
Self {
- a: NULL_MOVE,
- b: NULL_MOVE,
+ moves: [NULL_MOVE; 2],
}
}
- pub fn push(&mut self, new_move: MoveData) {
- self.b = self.a;
- self.a = new_move;
+ pub fn add_killer_move(&mut self, new_move: MoveData) {
+ if self.moves[0] == new_move {
+ return;
+ }
+
+ self.moves.rotate_right(1);
+ self.moves[0] = new_move;
}
- pub fn is_killer(&self, check: MoveData) -> bool {
- check == self.a
- || check == self.b
+ pub fn is_killer(&self, data: MoveData) -> bool {
+ self.moves.contains(&data)
}
}
\ No newline at end of file
diff --git a/src/main.rs b/src/main.rs
index 6d49c8c..673c6b4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,26 +1,3 @@
-/* TODO
-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
-https://www.chessprogramming.org/Principal_Variation_Search
-https://www.chessprogramming.org/Internal_Iterative_Deepening
-
-Random ideas to try (from other engines and chessprogramming.org)
-History reduction
-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
-
-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)]
#![allow(unused_variables)]
#![allow(unused_imports)]
@@ -42,8 +19,11 @@ mod board;
mod zobrist;
mod perft;
mod bot;
+mod pv_table;
mod move_sorter;
+mod scored_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::*;
@@ -55,17 +35,18 @@ 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 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();
@@ -80,10 +61,11 @@ 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));
+ // The length of this Vec will always be > 0
let command_split = command.trim()
.split(' ')
.collect::>();
@@ -92,12 +74,30 @@ fn main() {
// UCI protocol
"uci" => {
- println!("id name Maxwell v3.0.8-2");
+ 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" => {
+ // 0 1 2 3 4
+ // setoption name