Skip to content

Commit c9d74bd

Browse files
authored
Merge pull request #15 from eboatwright/dev
v3.0.8
2 parents d77ba34 + 21e83ec commit c9d74bd

File tree

5 files changed

+135
-56
lines changed

5 files changed

+135
-56
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "maxwell"
3-
version = "3.0.7"
3+
version = "3.0.8"
44
edition = "2021"
55

66
[dependencies]

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@
55
[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)
66

77
## Features
8+
#### Parameters
9+
- fen=\<FEN STRING>: Sets up the board by a fen string (Doesn't work for UCI games) (default=STARTING_FEN)
10+
- debug=\<BOOLEAN>: Toggle debug output that gets outputed per ply (default=true)
11+
- opening_book=\<BOOLEAN>: Toggle opening book (default=true)
12+
- time_management=\<BOOLEAN>: Toggle time management, if false the bot will use all the remaining time (default=true)
813
#### UCI Interface
914
- Only supports games from startpos
1015
- uci, isready, ucinewgame, position, go, stop, and quit commands
1116
#### Board Representation
1217
- Purely bitboards
1318
- Supports loading from FEN strings
1419
#### Move Generation
20+
- Basic handcrafted opening book
1521
- Magic bitboards for sliding pieces
1622
- Hardcoded pawn movement
1723
- Bitboard masks for other pieces calculated at startup
@@ -23,7 +29,7 @@
2329
- Attacked squares around kings
2430
#### Move Ordering
2531
- Hash move / best move from previous iteration
26-
- Capturing a piece of higher value
32+
- MVV-LVA
2733
- 2 Killer moves
2834
- History heuristic
2935
- Castling
@@ -33,11 +39,14 @@
3339
- Iterative deepening
3440
- Aspiration windows
3541
- Starts at 40 and multiplies by 4 if out of alpha beta bounds
36-
- Time management: if less than 7 moves have been played, it uses 2.5% of it's remaining time, otherwise 7%
42+
- Time management
43+
- If less than 7 moves have been played, it uses 2.5% of it's remaining time, otherwise 7%
44+
- This value is then also clamped between 0.25 and 20.0 seconds
3745
- Exits search if a mate is found within search depth
3846
- Alpha beta pruning
3947
- Quiescence search
40-
- Transposition table: No set max size, but moves get removed after 10 moves
48+
- Transposition table
49+
- No set max size, but entries get removed after 10 moves without hits
4150
- Null move pruning
4251
- Razoring
4352
- Reverse futility pruning

src/bot.rs

Lines changed: 89 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
use crate::piece_square_tables::QUEEN_WORTH;
2-
use crate::PAWN;
3-
use crate::NO_PIECE;
1+
use crate::STARTING_FEN;
2+
use crate::piece_square_tables::{PAWN_WORTH, QUEEN_WORTH};
3+
use crate::pieces::{PAWN, PROMOTABLE, NO_PIECE};
44
use crate::utils::{CHECKMATE_EVAL, evaluation_is_mate, moves_ply_from_mate};
55
use std::time::Instant;
66
use crate::move_sorter::MoveSorter;
@@ -11,15 +11,54 @@ use crate::Board;
1111

1212
pub const MAX_SEARCH_EXTENSIONS: u8 = 16;
1313
pub const FUTILITY_PRUNING_THESHOLD_PER_PLY: i32 = 60;
14+
pub const RAZORING_THRESHOLD_PER_PLY: i32 = 300;
15+
16+
pub const PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES: f32 = 0.025; // 2.5%
17+
pub const PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES: f32 = 0.07; // 7%
18+
19+
pub const MIN_TIME_PER_MOVE: f32 = 0.25; // seconds
20+
pub const MAX_TIME_PER_MOVE: f32 = 20.0;
21+
22+
#[derive(Clone, Debug)]
23+
pub struct BotConfig {
24+
pub fen: String,
25+
pub debug_output: bool,
26+
pub opening_book: bool,
27+
pub time_management: bool,
28+
}
29+
30+
impl BotConfig {
31+
pub fn from_args(args: Vec<String>) -> Self {
32+
let _true = "true".to_string();
33+
Self { // This is so ugly lol
34+
fen: Self::get_arg_value(&args, "fen").unwrap_or(STARTING_FEN.to_string()),
35+
debug_output: Self::get_arg_value(&args, "debug_output").unwrap_or(_true.clone()) == _true,
36+
opening_book: Self::get_arg_value(&args, "opening_book").unwrap_or(_true.clone()) == _true,
37+
time_management: Self::get_arg_value(&args, "time_management").unwrap_or(_true.clone()) == _true,
38+
}
39+
}
40+
41+
fn get_arg_value(args: &Vec<String>, key: &'static str) -> Option<String> {
42+
for arg in args.iter() {
43+
if arg.contains(key) {
44+
return Some(arg[key.len() + 1..].to_string());
45+
}
46+
}
47+
48+
None
49+
}
50+
}
1451

1552
pub struct Bot {
53+
pub config: BotConfig,
54+
1655
time_to_think: f32,
1756
think_timer: Instant,
1857
pub search_cancelled: bool,
1958
searched_one_move: bool,
2059

2160
opening_book: OpeningBook,
22-
in_opening: bool,
61+
in_opening_book: bool,
2362

2463
move_sorter: MoveSorter,
2564
pub transposition_table: TranspositionTable,
@@ -37,15 +76,19 @@ pub struct Bot {
3776
}
3877

3978
impl Bot {
40-
pub fn new(in_opening: bool) -> Self {
79+
pub fn new(config: BotConfig) -> Self {
80+
let in_opening_book = config.opening_book;
81+
4182
Self {
83+
config,
84+
4285
time_to_think: 0.0,
4386
think_timer: Instant::now(),
4487
search_cancelled: false,
4588
searched_one_move: false,
4689

4790
opening_book: OpeningBook::create(),
48-
in_opening,
91+
in_opening_book,
4992

5093
move_sorter: MoveSorter::new(),
5194
transposition_table: TranspositionTable::empty(),
@@ -63,24 +106,34 @@ impl Bot {
63106
}
64107
}
65108

109+
pub fn println(&self, output: String) {
110+
if self.config.debug_output {
111+
println!("{}", output);
112+
}
113+
}
114+
66115
pub fn start(&mut self, board: &mut Board, moves: String, my_time: f32) {
67-
if self.in_opening {
116+
if self.in_opening_book {
68117
let opening_move = self.opening_book.get_opening_move(moves);
69118
if opening_move == NULL_MOVE {
70-
self.in_opening = false;
119+
self.in_opening_book = false;
71120
} else {
72121
self.best_move = opening_move;
73122
return;
74123
}
75124
}
76125

77-
let time_percentage = if board.moves.len() / 2 <= 6 {
78-
0.025
126+
self.time_to_think = if self.config.time_management {
127+
let time_percentage = if board.moves.len() / 2 <= 6 {
128+
PERCENT_OF_TIME_TO_USE_BEFORE_6_FULL_MOVES
129+
} else {
130+
PERCENT_OF_TIME_TO_USE_AFTER_6_FULL_MOVES
131+
};
132+
133+
(my_time * time_percentage).clamp(MIN_TIME_PER_MOVE, MAX_TIME_PER_MOVE)
79134
} else {
80-
0.07
135+
my_time
81136
};
82-
self.time_to_think = (my_time * time_percentage).clamp(0.25, 20.0);
83-
// self.time_to_think = my_time;
84137

85138
self.search_cancelled = false;
86139

@@ -123,7 +176,7 @@ impl Bot {
123176
self.evaluation = self.evaluation_this_iteration;
124177
}
125178

126-
println!("Depth: {}, Window: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}",
179+
self.println(format!("Depth: {}, Window: {}, Evaluation: {}, Best move: {}, Positions searched: {} + Quiescence positions searched: {} = {}, Transposition Hits: {}",
127180
depth,
128181
window,
129182
self.evaluation * board.perspective(),
@@ -132,24 +185,26 @@ impl Bot {
132185
self.quiescence_searched,
133186
self.positions_searched + self.quiescence_searched,
134187
self.transposition_hits,
135-
);
188+
));
136189

137190
if evaluation_is_mate(self.evaluation) {
138191
let moves_until_mate = moves_ply_from_mate(self.evaluation);
139192
if moves_until_mate <= depth {
140-
println!("Mate found in {}", (moves_until_mate as f32 * 0.5).ceil());
193+
self.println(format!("Mate found in {}", (moves_until_mate as f32 * 0.5).ceil()));
141194
break;
142195
}
143196
}
144197

145198
if self.search_cancelled {
146-
println!("Search cancelled");
199+
self.println("Search cancelled".to_string());
147200
break;
148201
}
149202
}
150203

151204
self.transposition_table.update();
152-
self.transposition_table.print_size();
205+
if self.config.debug_output {
206+
self.transposition_table.print_size();
207+
}
153208
}
154209

155210
fn should_cancel_search(&mut self) -> bool {
@@ -199,11 +254,12 @@ impl Bot {
199254
if !is_pv
200255
&& depth > 0
201256
&& depth_left > 0
257+
&& board.get_last_move().capture == NO_PIECE as u8
202258
&& !board.king_in_check(board.white_to_move) {
203259
// Null Move Pruning
204260
if depth_left >= 3
205261
&& board.try_null_move() {
206-
// let reduction = 3 - (depth_left - 3) / 2;
262+
// let reduction = 3 - (depth_left - 3) / 2; // This didn't work at all lol
207263
let evaluation = -self.alpha_beta_search(board, depth + 1, depth_left - 3, -beta, -beta + 1, number_of_extensions);
208264

209265
board.undo_null_move();
@@ -217,14 +273,13 @@ impl Bot {
217273

218274
// Reverse Futility Pruning
219275
if depth_left <= 4
220-
&& static_eval - (FUTILITY_PRUNING_THESHOLD_PER_PLY * depth_left as i32) >= beta {
276+
&& static_eval - FUTILITY_PRUNING_THESHOLD_PER_PLY * (depth_left as i32) >= beta {
221277
return static_eval;
222278
}
223279

224280
// Razoring
225281
if depth_left <= 3
226-
&& board.get_last_move().capture == NO_PIECE as u8
227-
&& static_eval + QUEEN_WORTH < alpha {
282+
&& static_eval + RAZORING_THRESHOLD_PER_PLY * (depth_left as i32) < alpha {
228283
depth_left -= 1;
229284
}
230285
}
@@ -357,6 +412,18 @@ impl Bot {
357412
let sorted_moves = self.move_sorter.sort_moves(board, legal_moves, NULL_MOVE, u8::MAX);
358413

359414
for m in sorted_moves {
415+
// Delta Pruning
416+
if !board.king_in_check(board.white_to_move) {
417+
let mut threshold = QUEEN_WORTH;
418+
if PROMOTABLE.contains(&m.flag) {
419+
threshold += QUEEN_WORTH - PAWN_WORTH;
420+
}
421+
422+
if evaluation < alpha - threshold {
423+
continue;
424+
}
425+
}
426+
360427
board.make_move(m);
361428
let evaluation = -self.quiescence_search(board, -beta, -alpha);
362429
board.undo_last_move();

src/main.rs

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@ Random ideas to try (from other engines and chessprogramming.org)
1313
History reduction
1414
https://www.chessprogramming.org/History_Leaf_Pruning
1515
https://www.chessprogramming.org/Futility_Pruning#MoveCountBasedPruning
16-
https://www.chessprogramming.org/Delta_Pruning
1716
https://www.chessprogramming.org/Triangular_PV-Table
18-
https://www.chessprogramming.org/Razoring (look into a better implementation)
17+
https://www.chessprogramming.org/Static_Exchange_Evaluation
1918
2019
Some random resources I found:
2120
https://analog-hors.github.io/site/magic-bitboards/ (didn't use this for my initial implementation, but that might change ;))
@@ -48,7 +47,7 @@ mod bot;
4847
mod move_sorter;
4948

5049
use crate::castling_rights::print_castling_rights;
51-
use crate::bot::Bot;
50+
use crate::bot::{Bot, BotConfig};
5251
use crate::perft::*;
5352
use crate::move_data::MoveData;
5453
use crate::pieces::*;
@@ -68,10 +67,12 @@ pub const ENDGAME_POSITION: &str = "8/pk4p1/2prp3/3p1p2/3P2p1/R2BP3/2P2KPP/8 w
6867
pub const PAWN_EVAL_TESTING: &str = "4k3/p1pp4/8/4pp1P/2P4P/8/P5P1/4K3 w - - 0 1";
6968

7069
fn main() {
71-
let mut log = Log::none();
70+
let bot_config = BotConfig::from_args(std::env::args().collect::<Vec<String>>());
7271

73-
let mut board = Board::from_fen(STARTING_FEN);
74-
let mut bot = Bot::new(true);
72+
// let mut log = Log::none();
73+
74+
let mut board = Board::from_fen(&bot_config.fen);
75+
let mut bot = Bot::new(bot_config.clone());
7576

7677
let mut command = String::new();
7778
let mut moves = String::new();
@@ -98,7 +99,7 @@ fn main() {
9899
"ucinewgame" => {
99100
// log = Log::new();
100101
board = Board::from_fen(STARTING_FEN);
101-
bot = Bot::new(true);
102+
bot = Bot::new(bot_config.clone());
102103
}
103104

104105
// Format: position startpos (moves e2e4 e7e5 ...)
@@ -114,7 +115,7 @@ fn main() {
114115
if !board.play_move(data) {
115116
let err = format!("{}: failed to play move: {}", "FATAL ERROR".white().on_red(), coordinates);
116117
println!("{}", err);
117-
log.write(err);
118+
// log.write(err);
118119
}
119120
}
120121
moves.pop();
@@ -189,33 +190,25 @@ fn main() {
189190
}
190191

191192
// "test" => {
192-
// let mut board = Board::from_fen(STARTING_FEN);
193-
194193
// let mut old_best_time = f32::MAX;
195194
// let mut old_worst_time = -f32::MAX;
196195

197196
// let mut new_best_time = f32::MAX;
198197
// let mut new_worst_time = -f32::MAX;
199198

200-
// for _ in 0..5 {
201-
// board.clear_castling_rights();
202-
// board.zobrist.clear();
199+
// let piece = BLACK_ROOK as u8;
200+
// let capture = WHITE_KNIGHT as u8;
203201

202+
// for _ in 0..5 {
204203
// let timer = Instant::now();
205-
// for _ in 0..100_000 {
206-
// let legal_moves = board.get_legal_moves_for_color(board.white_to_move, false);
207-
// let _ = sort_moves(&board, legal_moves, None);
204+
// for _ in 0..1_000_000_000 {
205+
// let score = MVV_LVA[get_piece_type(piece as usize) * 6 + get_piece_type(capture as usize)];
208206
// }
209207
// let old_time = timer.elapsed().as_secs_f32();
210208

211-
212-
// board.clear_castling_rights();
213-
// board.zobrist.clear();
214-
215209
// let timer = Instant::now();
216-
// for _ in 0..100_000 {
217-
// let legal_moves = board.get_legal_moves_for_color(board.white_to_move, false);
218-
// let _ = new_sort_moves(&board, legal_moves, None);
210+
// for _ in 0..1_000_000_000 {
211+
// let score = (5 - get_piece_type(piece as usize)) + (get_piece_type(capture as usize) + 1) * 10;
219212
// }
220213
// let new_time = timer.elapsed().as_secs_f32();
221214

@@ -241,7 +234,8 @@ fn main() {
241234
// println!("New: worst: {}, best: {}", new_worst_time, new_best_time);
242235
// }
243236

244-
_ => log.write(format!("Unknown command: {}", command)),
237+
// _ => log.write(format!("Unknown command: {}", command)),
238+
_ => {}
245239
}
246240
}
247241
}

0 commit comments

Comments
 (0)