Add time limits, use anyhow crate to improve error handling, remove depth limit on quiescence search
This commit is contained in:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -51,6 +51,12 @@ dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c95c10ba0b00a02636238b814946408b1322d5ac4760326e6fb8ec956d85775"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
@@ -194,9 +200,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.77"
|
||||
version = "2.0.87"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
|
||||
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -298,6 +304,7 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
name = "zeal"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"clap",
|
||||
"rand",
|
||||
]
|
||||
|
||||
@@ -4,6 +4,7 @@ version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.93"
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
rand = { version = "0.8.5", features = ["small_rng"] }
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
|
||||
use super::psqt::piece_square_score_endgame;
|
||||
|
||||
const fn piece_score(piece_type: PieceType) -> i32 {
|
||||
pub const fn material_score(piece_type: PieceType) -> i32 {
|
||||
match piece_type {
|
||||
PieceType::Pawn => 100,
|
||||
PieceType::Knight => 320,
|
||||
@@ -23,11 +23,11 @@ const fn piece_score(piece_type: PieceType) -> i32 {
|
||||
|
||||
fn is_end_game(board: &Board) -> bool {
|
||||
let white_pieces = board.white_pieces.iter().fold(0, |acc, p| {
|
||||
acc + bitboard::bit_count(p.bitboard) * piece_score(p.piece_type) as usize
|
||||
acc + bitboard::bit_count(p.bitboard) * material_score(p.piece_type) as usize
|
||||
});
|
||||
|
||||
let black_pieces = board.black_pieces.iter().fold(0, |acc, p| {
|
||||
acc + bitboard::bit_count(p.bitboard) * piece_score(p.piece_type) as usize
|
||||
acc + bitboard::bit_count(p.bitboard) * material_score(p.piece_type) as usize
|
||||
});
|
||||
|
||||
(white_pieces + black_pieces) < 2000
|
||||
@@ -55,7 +55,7 @@ fn evaluate_side_for(board: &Board, color: Color) -> i32 {
|
||||
Color::Black => mirror_index(lsb(bitboard)),
|
||||
};
|
||||
|
||||
score += piece_score(piece_type);
|
||||
score += material_score(piece_type);
|
||||
score += psqt(piece_type, psqt_index);
|
||||
bitboard &= bitboard - 1;
|
||||
}
|
||||
@@ -81,7 +81,7 @@ mod tests {
|
||||
board::{Color, PieceType},
|
||||
fen::from_fen,
|
||||
},
|
||||
evaluation::evaluation::{evaluate_position, evaluate_side_for, piece_score},
|
||||
evaluation::evaluation::{evaluate_position, evaluate_side_for, material_score},
|
||||
};
|
||||
|
||||
const FEN_QUIET: [&str; 2] = [
|
||||
@@ -90,13 +90,13 @@ mod tests {
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_piece_score() -> Result<(), String> {
|
||||
assert_eq!(100, piece_score(PieceType::Pawn));
|
||||
assert_eq!(320, piece_score(PieceType::Knight));
|
||||
assert_eq!(330, piece_score(PieceType::Bishop));
|
||||
assert_eq!(500, piece_score(PieceType::Rook));
|
||||
assert_eq!(900, piece_score(PieceType::Queen));
|
||||
assert_eq!(0, piece_score(PieceType::King));
|
||||
fn test_material_score() -> Result<(), String> {
|
||||
assert_eq!(100, material_score(PieceType::Pawn));
|
||||
assert_eq!(320, material_score(PieceType::Knight));
|
||||
assert_eq!(330, material_score(PieceType::Bishop));
|
||||
assert_eq!(500, material_score(PieceType::Rook));
|
||||
assert_eq!(900, material_score(PieceType::Queen));
|
||||
assert_eq!(0, material_score(PieceType::King));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
use std::{
|
||||
io::{BufRead, Write},
|
||||
str::SplitWhitespace,
|
||||
time::Instant,
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
|
||||
use crate::{
|
||||
board::{board::Color, game::Game},
|
||||
movegen::r#move::Move,
|
||||
@@ -22,7 +25,7 @@ pub enum Command {
|
||||
Quit,
|
||||
}
|
||||
|
||||
fn parse_command(parts: &mut SplitWhitespace) -> Result<Command, String> {
|
||||
fn parse_command(parts: &mut SplitWhitespace) -> anyhow::Result<Command> {
|
||||
match parts.next() {
|
||||
Some("uci") => Ok(Command::Uci),
|
||||
Some("isready") => Ok(Command::IsReady),
|
||||
@@ -30,7 +33,7 @@ fn parse_command(parts: &mut SplitWhitespace) -> Result<Command, String> {
|
||||
Some("position") => Ok(Command::Position),
|
||||
Some("go") => Ok(Command::Go),
|
||||
Some("quit") => Ok(Command::Quit),
|
||||
_ => Err("Unrecognised command".to_string()),
|
||||
_ => bail!("Unrecognised command"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +44,9 @@ pub enum Response {
|
||||
Info(String),
|
||||
}
|
||||
|
||||
fn write_response(handle_out: &mut impl Write, response: &Response) -> Result<(), String> {
|
||||
writeln!(handle_out, "{response}").map_err(|e| e.to_string())?;
|
||||
handle_out.flush().map_err(|e| e.to_string())?;
|
||||
fn write_response(handle_out: &mut impl Write, response: &Response) -> anyhow::Result<()> {
|
||||
writeln!(handle_out, "{response}").map_err(|e| anyhow!(e))?;
|
||||
handle_out.flush().map_err(|e| anyhow!(e))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -107,16 +110,18 @@ impl Default for UciParameters {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn uci_position(position: &mut SplitWhitespace) -> Result<Game, String> {
|
||||
let state = position.next().ok_or("Expected startpos or fen")?;
|
||||
pub fn uci_position(position: &mut SplitWhitespace) -> anyhow::Result<Game> {
|
||||
let state = position
|
||||
.next()
|
||||
.ok_or_else(|| anyhow!("Expected startpos or fen"))?;
|
||||
let mut game = match state {
|
||||
"startpos" => Game::new(),
|
||||
"fen" => {
|
||||
let fen_parts: Vec<&str> = position.take_while(|&part| part != "moves").collect();
|
||||
let fen = fen_parts.join(" ");
|
||||
Game::from_fen(&fen)?
|
||||
Game::from_fen(&fen).map_err(|e| anyhow!("Failed to parse FEN: {fen}, {e}"))?
|
||||
}
|
||||
_ => return Err("Expected startpos or fen".to_string()),
|
||||
_ => bail!("Expected startpos or fen"),
|
||||
};
|
||||
|
||||
if Some("moves") != position.next() {
|
||||
@@ -124,7 +129,8 @@ pub fn uci_position(position: &mut SplitWhitespace) -> Result<Game, String> {
|
||||
}
|
||||
|
||||
for mv_str in position {
|
||||
let mv = Move::parse_from_str(&game, mv_str)?;
|
||||
let mv = Move::parse_from_str(&game, mv_str)
|
||||
.map_err(|e| anyhow!("Failed to parse move: {e}"))?;
|
||||
game.make_move(&mv);
|
||||
}
|
||||
|
||||
@@ -135,7 +141,7 @@ pub fn uci_go(
|
||||
go_iter: &mut SplitWhitespace,
|
||||
game: &mut Game,
|
||||
tt: &mut TranspositionTable,
|
||||
) -> Result<Move, String> {
|
||||
) -> anyhow::Result<Move> {
|
||||
let mut params = UciParameters::new();
|
||||
while let Some(subcommand) = go_iter.next() {
|
||||
match subcommand {
|
||||
@@ -147,23 +153,30 @@ pub fn uci_go(
|
||||
}
|
||||
}
|
||||
|
||||
let time = Instant::now();
|
||||
let remaining_time = match game.current_player() {
|
||||
Color::White => params.wtime.unwrap_or(REMAINING_TIME_DEFAULT),
|
||||
Color::Black => params.btime.unwrap_or(REMAINING_TIME_DEFAULT),
|
||||
};
|
||||
|
||||
iterative_deepening::iterative_deepening(game, MAX_DEPTH, remaining_time, tt)
|
||||
.ok_or_else(|| "No move selected".to_string())
|
||||
iterative_deepening::iterative_deepening(game, MAX_DEPTH, remaining_time, tt)?.ok_or_else(
|
||||
|| {
|
||||
anyhow!(
|
||||
"No stored best move found. Time: {}",
|
||||
remaining_time - time.elapsed().as_millis()
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn parse_next<T: std::str::FromStr>(go_iter: &mut SplitWhitespace, val: &str) -> Result<T, String> {
|
||||
fn parse_next<T: std::str::FromStr>(go_iter: &mut SplitWhitespace, val: &str) -> anyhow::Result<T> {
|
||||
go_iter
|
||||
.next()
|
||||
.ok_or_else(|| format!("Expected {val}"))
|
||||
.and_then(|v| v.parse::<T>().map_err(|_| format!("Invalid {val}")))
|
||||
.ok_or_else(|| anyhow!("Expected {val}"))
|
||||
.and_then(|v| v.parse::<T>().map_err(|_| anyhow!("Invalid {val}")))
|
||||
}
|
||||
|
||||
pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> Result<(), String> {
|
||||
pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> anyhow::Result<()> {
|
||||
let mut params = UciParameters::new();
|
||||
let mut tt = TranspositionTable::new(MAX_TT_SIZE);
|
||||
|
||||
@@ -182,19 +195,19 @@ pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> Result<(), Str
|
||||
params.add_game(uci_position(&mut parts)?);
|
||||
Response::Info("Initialized position".to_string())
|
||||
}
|
||||
Command::Go => {
|
||||
if let Some(ref mut game) = params.game {
|
||||
let best_move = uci_go(&mut parts, game, &mut tt)?;
|
||||
Response::BestMove(best_move.parse_into_str())
|
||||
} else {
|
||||
Response::Info("Going?".to_string())
|
||||
}
|
||||
}
|
||||
Command::Go => params.game.as_mut().map_or_else(
|
||||
|| Response::Info("Failed to unwrap from UciParameter".to_string()),
|
||||
|game| match uci_go(&mut parts, game, &mut tt) {
|
||||
Ok(best_move) => Response::BestMove(best_move.parse_into_str()),
|
||||
Err(e) => Response::Info(e.to_string()),
|
||||
},
|
||||
),
|
||||
// TODO: Command::Stop => (),
|
||||
Command::Quit => break,
|
||||
};
|
||||
|
||||
write_response(&mut output, &response)?;
|
||||
output.flush().map_err(|e| e.to_string())?;
|
||||
output.flush().map_err(|e| anyhow!(e))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -221,7 +234,7 @@ mod tests {
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_uci_position() -> Result<(), String> {
|
||||
fn test_uci_position() -> anyhow::Result<()> {
|
||||
let command_position = "position startpos";
|
||||
let mut parts = command_position.split_whitespace();
|
||||
let command = parse_command(&mut parts)?;
|
||||
@@ -242,10 +255,10 @@ mod tests {
|
||||
const FEN_MATE_IN_1: &str = "8/8/8/8/8/4q1k1/8/5K2 b - - 0 1";
|
||||
|
||||
#[test]
|
||||
fn test_uci_go() -> Result<(), String> {
|
||||
fn test_uci_go() -> anyhow::Result<()> {
|
||||
init_attacks();
|
||||
let mut tt = TranspositionTable::new(MAX_TT_SIZE);
|
||||
let mut game = from_fen(FEN_MATE_IN_1)?;
|
||||
let mut game = from_fen(FEN_MATE_IN_1).unwrap();
|
||||
let command_go = "go depth 2";
|
||||
let mut parts = command_go.split_whitespace();
|
||||
let response = uci_go(&mut parts, &mut game, &mut tt)?;
|
||||
@@ -256,7 +269,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_uci_loop() -> Result<(), String> {
|
||||
fn test_uci_loop() -> anyhow::Result<()> {
|
||||
init_attacks();
|
||||
let commands = "uci\n\
|
||||
ucinewgame\n\
|
||||
@@ -282,7 +295,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cute_chess_bug() -> Result<(), String> {
|
||||
fn test_cute_chess_bug() -> anyhow::Result<()> {
|
||||
init_attacks();
|
||||
let commands = "uci\n\
|
||||
ucinewgame\n\
|
||||
|
||||
@@ -7,8 +7,8 @@ use crate::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
negamax, transposition_table::TranspositionTable, HARD_LIMIT_DIVISION, SOFT_EVAL_THRESHOLD,
|
||||
SOFT_LIMIT_DIVISION,
|
||||
negamax, time::TimeInfo, transposition_table::TranspositionTable, HARD_LIMIT_DIVISION,
|
||||
SOFT_EVAL_THRESHOLD, SOFT_LIMIT_DIVISION,
|
||||
};
|
||||
|
||||
pub fn iterative_deepening(
|
||||
@@ -16,30 +16,47 @@ pub fn iterative_deepening(
|
||||
max_depth: u8,
|
||||
remaining_time: u128,
|
||||
tt: &mut TranspositionTable,
|
||||
) -> Option<Move> {
|
||||
let (mut best_move, mut best_eval): (Option<Move>, i32) = (None, MIN_SCORE);
|
||||
let time_now = std::time::Instant::now();
|
||||
) -> anyhow::Result<Option<Move>> {
|
||||
let (mut best_move, mut best_score) = (None, MIN_SCORE);
|
||||
let time = std::time::Instant::now();
|
||||
|
||||
for depth in 1..=max_depth {
|
||||
if time_limit_reached(&time_now, remaining_time, best_eval) {
|
||||
return best_move;
|
||||
if time_limit_reached(&time, remaining_time, best_score) {
|
||||
return Ok(best_move);
|
||||
}
|
||||
|
||||
(best_move, best_eval) = negamax::negamax(game, MIN_SCORE, MAX_SCORE, depth, 0, tt);
|
||||
let search_result = negamax::negamax(
|
||||
game,
|
||||
MIN_SCORE,
|
||||
MAX_SCORE,
|
||||
depth,
|
||||
0,
|
||||
&TimeInfo::new(time, remaining_time),
|
||||
tt,
|
||||
);
|
||||
|
||||
if let Ok(search_result) = search_result {
|
||||
if search_result.best_move.is_some() {
|
||||
best_move = search_result.best_move;
|
||||
}
|
||||
best_score = search_result.best_score;
|
||||
} else {
|
||||
return Ok(best_move);
|
||||
}
|
||||
}
|
||||
|
||||
best_move
|
||||
Ok(best_move)
|
||||
}
|
||||
|
||||
fn time_limit_reached(time: &Instant, remaining_time: u128, eval: i32) -> bool {
|
||||
hard_limit(time, remaining_time) || soft_limit(time, remaining_time, eval)
|
||||
}
|
||||
|
||||
fn hard_limit(time_now: &Instant, remaining_time: u128) -> bool {
|
||||
pub fn hard_limit(time_now: &Instant, remaining_time: u128) -> bool {
|
||||
time_now.elapsed().as_millis() >= remaining_time / HARD_LIMIT_DIVISION
|
||||
}
|
||||
|
||||
fn soft_limit(time: &Instant, remaining_time: u128, eval: i32) -> bool {
|
||||
pub fn soft_limit(time: &Instant, remaining_time: u128, eval: i32) -> bool {
|
||||
time.elapsed().as_millis() >= remaining_time / SOFT_LIMIT_DIVISION && eval > SOFT_EVAL_THRESHOLD
|
||||
}
|
||||
|
||||
@@ -57,9 +74,9 @@ mod tests {
|
||||
const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1";
|
||||
|
||||
#[test]
|
||||
fn test_iterative_deepening() -> Result<(), String> {
|
||||
fn test_iterative_deepening() -> anyhow::Result<()> {
|
||||
init_attacks();
|
||||
let mut game = from_fen(FEN)?;
|
||||
let mut game = from_fen(FEN).unwrap();
|
||||
let mut tt = TranspositionTable::new(MAX_TT_SIZE);
|
||||
let time_now = std::time::Instant::now();
|
||||
|
||||
@@ -68,7 +85,7 @@ mod tests {
|
||||
MAX_DEPTH,
|
||||
REMAINING_TIME_DEFAULT,
|
||||
&mut tt,
|
||||
);
|
||||
)?;
|
||||
|
||||
dbg!(time_now.elapsed());
|
||||
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
use crate::movegen::r#move::Move;
|
||||
|
||||
pub mod iterative_deepening;
|
||||
pub mod move_ordering;
|
||||
pub mod negamax;
|
||||
pub mod perft;
|
||||
pub mod quiescence;
|
||||
pub mod time;
|
||||
pub mod transposition_table;
|
||||
|
||||
pub const MAX_DEPTH: u8 = 7;
|
||||
pub const QUIESCENCE_DEPTH: u8 = 3;
|
||||
pub const REMAINING_TIME_DEFAULT: u128 = 100000; // in ms
|
||||
pub const HARD_LIMIT_DIVISION: u128 = 10;
|
||||
pub const SOFT_LIMIT_DIVISION: u128 = 2;
|
||||
pub const SOFT_LIMIT_DIVISION: u128 = 20;
|
||||
pub const SOFT_EVAL_THRESHOLD: i32 = 500;
|
||||
pub const MAX_TT_SIZE: u64 = 1000000;
|
||||
pub const MAX_TT_SIZE: u64 = 2000000;
|
||||
|
||||
pub struct SearchResult {
|
||||
pub best_move: Option<Move>,
|
||||
pub best_score: i32,
|
||||
}
|
||||
|
||||
impl SearchResult {
|
||||
pub const fn new(best_move: Option<Move>, best_score: i32) -> Self {
|
||||
Self {
|
||||
best_move,
|
||||
best_score,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
|
||||
use crate::{
|
||||
board::game::Game,
|
||||
evaluation::{MATE_SCORE, MIN_SCORE},
|
||||
movegen::r#move::Move,
|
||||
};
|
||||
|
||||
use super::{
|
||||
move_ordering::score_by_mvv_lva,
|
||||
iterative_deepening::hard_limit,
|
||||
move_ordering::mvv_lva,
|
||||
quiescence::quiescence,
|
||||
time::TimeInfo,
|
||||
transposition_table::{Bound, TTEntry, TranspositionTable},
|
||||
QUIESCENCE_DEPTH,
|
||||
SearchResult,
|
||||
};
|
||||
|
||||
pub fn negamax(
|
||||
@@ -17,11 +20,16 @@ pub fn negamax(
|
||||
beta: i32,
|
||||
depth: u8,
|
||||
plies: u8,
|
||||
time_info: &TimeInfo,
|
||||
tt: &mut TranspositionTable,
|
||||
) -> (Option<Move>, i32) {
|
||||
) -> Result<SearchResult> {
|
||||
if hard_limit(&time_info.time, time_info.remaining_time_in_ms) {
|
||||
bail!("Time is up! In Negamax");
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
let q = quiescence(game, alpha, beta, QUIESCENCE_DEPTH).1;
|
||||
return (None, q);
|
||||
let q_score = quiescence(game, alpha, beta, time_info).map_err(|e| anyhow!("{e}"))?;
|
||||
return Ok(SearchResult::new(None, q_score));
|
||||
}
|
||||
|
||||
let mut tt_move = None;
|
||||
@@ -32,7 +40,7 @@ pub fn negamax(
|
||||
|| (matches!(entry.bound, Bound::Lower) && entry.score >= beta)
|
||||
|| (matches!(entry.bound, Bound::Upper) && entry.score <= alpha))
|
||||
{
|
||||
return (entry.mv, entry.score);
|
||||
return Ok(SearchResult::new(entry.mv, entry.score));
|
||||
}
|
||||
tt_move = entry.mv;
|
||||
}
|
||||
@@ -41,7 +49,7 @@ pub fn negamax(
|
||||
let (mut best_move, mut best_score, mate_score) = (None, MIN_SCORE, -MATE_SCORE + plies as i32);
|
||||
let mut legal_moves = 0;
|
||||
let mut pseudo_legal_moves = game.board.pseudo_moves_all();
|
||||
pseudo_legal_moves.sort_unstable_by_key(|mv| score_by_mvv_lva(&game.mailbox, *mv));
|
||||
pseudo_legal_moves.sort_unstable_by_key(|mv| mvv_lva(&game.mailbox, *mv));
|
||||
|
||||
if let Some(tt_move) = tt_move {
|
||||
if let Some(tt_move_index) = pseudo_legal_moves.iter().position(|&mv| mv == tt_move) {
|
||||
@@ -59,7 +67,8 @@ pub fn negamax(
|
||||
}
|
||||
|
||||
legal_moves += 1;
|
||||
let move_score = -negamax(game, -beta, -alpha, depth - 1, plies + 1, tt).1;
|
||||
let move_score =
|
||||
-negamax(game, -beta, -alpha, depth - 1, plies + 1, time_info, tt)?.best_score;
|
||||
game.unmake_move();
|
||||
|
||||
if move_score > best_score {
|
||||
@@ -75,7 +84,7 @@ pub fn negamax(
|
||||
best_move,
|
||||
Bound::Lower,
|
||||
));
|
||||
return (best_move, beta);
|
||||
return Ok(SearchResult::new(best_move, beta));
|
||||
}
|
||||
|
||||
alpha = alpha.max(move_score);
|
||||
@@ -90,10 +99,10 @@ pub fn negamax(
|
||||
best_move,
|
||||
Bound::Exact,
|
||||
));
|
||||
return (None, mate_score);
|
||||
return Ok(SearchResult::new(None, mate_score));
|
||||
}
|
||||
tt.insert(TTEntry::new(game.hash, depth, 0, best_move, Bound::Exact));
|
||||
return (None, 0);
|
||||
return Ok(SearchResult::new(None, 0));
|
||||
}
|
||||
|
||||
if best_score < alpha {
|
||||
@@ -114,7 +123,7 @@ pub fn negamax(
|
||||
));
|
||||
}
|
||||
|
||||
(best_move, best_score)
|
||||
Ok(SearchResult::new(best_move, best_score))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -124,6 +133,7 @@ mod tests {
|
||||
use crate::movegen::attack_generator::init_attacks;
|
||||
use crate::movegen::r#move::Move;
|
||||
use crate::search::negamax::negamax;
|
||||
use crate::search::time::TimeInfo;
|
||||
use crate::search::transposition_table::TranspositionTable;
|
||||
use crate::search::MAX_TT_SIZE;
|
||||
|
||||
@@ -133,24 +143,29 @@ mod tests {
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn test_negamax() -> Result<(), String> {
|
||||
fn test_negamax() -> anyhow::Result<()> {
|
||||
init_attacks();
|
||||
let mut game = from_fen(FEN_MATE_IN_1[0])?;
|
||||
let mut game = from_fen(FEN_MATE_IN_1[0]).unwrap();
|
||||
|
||||
let mut tt = TranspositionTable::new(MAX_TT_SIZE);
|
||||
let e3f2 = Move::new(Square::E3, Square::F2);
|
||||
let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &mut tt)
|
||||
.0
|
||||
.unwrap();
|
||||
let time_info = TimeInfo::new(std::time::Instant::now(), 1000);
|
||||
let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut tt)
|
||||
.expect("Expected a search result")
|
||||
.best_move
|
||||
.expect("Expected a move");
|
||||
|
||||
assert_eq!(e3f2, anointed_move);
|
||||
|
||||
// let mut game = from_fen(FEN_MATE_IN_1[1])?;
|
||||
let mut game = from_fen(FEN_MATE_IN_1[1]).unwrap();
|
||||
|
||||
// let e3f2 = Move::new(Square::E3, Square::F2);
|
||||
// let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0).0.unwrap();
|
||||
let e3f2 = Move::new(Square::E3, Square::F2);
|
||||
let anointed_move = negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut tt)
|
||||
.expect("Expected a search result")
|
||||
.best_move
|
||||
.expect("Expected a move");
|
||||
|
||||
// assert_eq!(e3f2, anointed_move);
|
||||
assert_eq!(e3f2, anointed_move);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use crate::{board::game::Game, evaluation::evaluation::evaluate_position, movegen::r#move::Move};
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use super::move_ordering::score_by_mvv_lva;
|
||||
use crate::{board::game::Game, evaluation::evaluation::evaluate_position};
|
||||
|
||||
pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, depth: u8) -> (Option<Move>, i32) {
|
||||
if depth == 0 {
|
||||
return (None, evaluate_position(&game.board));
|
||||
use super::{iterative_deepening::hard_limit, move_ordering::mvv_lva, time::TimeInfo};
|
||||
|
||||
pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, time_info: &TimeInfo) -> Result<i32> {
|
||||
if hard_limit(&time_info.time, time_info.remaining_time_in_ms) {
|
||||
bail!("Time is up! In Quiescence");
|
||||
}
|
||||
|
||||
let color = game.current_player();
|
||||
let stand_pat = evaluate_position(&game.board);
|
||||
|
||||
if stand_pat >= beta {
|
||||
return (None, beta);
|
||||
return Ok(beta);
|
||||
}
|
||||
|
||||
if alpha < stand_pat {
|
||||
@@ -19,7 +21,7 @@ pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, depth: u8) -> (Opt
|
||||
}
|
||||
|
||||
let mut captures: Vec<_> = game.board.pseudo_moves_all_captures();
|
||||
captures.sort_unstable_by_key(|mv| score_by_mvv_lva(&game.mailbox, *mv));
|
||||
captures.sort_unstable_by_key(|mv| mvv_lva(&game.mailbox, *mv));
|
||||
|
||||
for mv in captures {
|
||||
game.make_move(&mv);
|
||||
@@ -28,16 +30,17 @@ pub fn quiescence(game: &mut Game, mut alpha: i32, beta: i32, depth: u8) -> (Opt
|
||||
game.unmake_move();
|
||||
continue;
|
||||
}
|
||||
|
||||
let move_score = -quiescence(game, -beta, -alpha, depth - 1).1;
|
||||
let move_score = -quiescence(game, -beta, -alpha, time_info)?;
|
||||
game.unmake_move();
|
||||
|
||||
if move_score >= beta {
|
||||
return (Some(mv), beta);
|
||||
return Ok(beta);
|
||||
}
|
||||
|
||||
alpha = alpha.max(move_score);
|
||||
if alpha > move_score {
|
||||
alpha = move_score
|
||||
}
|
||||
}
|
||||
|
||||
(None, alpha)
|
||||
Ok(alpha)
|
||||
}
|
||||
|
||||
15
src/search/time.rs
Normal file
15
src/search/time.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use std::time::Instant;
|
||||
|
||||
pub struct TimeInfo {
|
||||
pub time: Instant,
|
||||
pub remaining_time_in_ms: u128,
|
||||
}
|
||||
|
||||
impl TimeInfo {
|
||||
pub const fn new(time: Instant, remaining_time_in_ms: u128) -> Self {
|
||||
Self {
|
||||
time,
|
||||
remaining_time_in_ms,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,31 +75,35 @@ mod tests {
|
||||
board::fen::from_fen,
|
||||
evaluation::{MAX_SCORE, MIN_SCORE},
|
||||
movegen::attack_generator::init_attacks,
|
||||
search::{self, transposition_table::TranspositionTable, MAX_TT_SIZE},
|
||||
search::{
|
||||
negamax::negamax, time::TimeInfo, transposition_table::TranspositionTable, MAX_TT_SIZE,
|
||||
},
|
||||
};
|
||||
|
||||
const FEN: &str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1";
|
||||
const FEN_WILL_BE: &str = "1r2k2r/2P1pq1p/2npb3/1B3ppP/p3P3/P4Q2/1P1PNPP1/R3K2R b KQk - 0 1";
|
||||
const FEN_WONT_BE: &str = "1Q2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B4/1P1PNPP1/R3K2R b KQk - 0 1";
|
||||
const FEN_POSSIBLE: &str = "1r2k2r/2P1pq1p/2npb3/1B3ppP/p3P3/P4Q2/1P1PNPP1/R3K2R b KQk - 0 1";
|
||||
const FEN_IMPOSSIBLE: &str = "1Q2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B4/1P1PNPP1/R3K2R b KQk - 0 1";
|
||||
|
||||
#[test]
|
||||
fn test_transposition_table() -> Result<(), String> {
|
||||
fn test_transposition_table() -> anyhow::Result<()> {
|
||||
init_attacks();
|
||||
let mut game = from_fen(FEN)?;
|
||||
let mut game = from_fen(FEN).unwrap();
|
||||
let mut tt = TranspositionTable::new(MAX_TT_SIZE);
|
||||
let time_now = std::time::Instant::now();
|
||||
let time_info = TimeInfo::new(std::time::Instant::now(), 30000);
|
||||
|
||||
// fill Transposition Table
|
||||
search::negamax::negamax(&mut game, MIN_SCORE, MAX_SCORE, 4, 0, &mut tt);
|
||||
dbg!(time_now.elapsed());
|
||||
negamax(&mut game, MIN_SCORE, MAX_SCORE, 2, 0, &time_info, &mut tt)
|
||||
.expect("Expected a search result")
|
||||
.best_move
|
||||
.expect("Expected a move");
|
||||
|
||||
let will_be_hash = from_fen(FEN_WILL_BE)?.hash;
|
||||
dbg!(time_info.time.elapsed());
|
||||
|
||||
let will_be_hash = from_fen(FEN_POSSIBLE).unwrap().hash;
|
||||
let tt_entry = tt.lookup(will_be_hash);
|
||||
|
||||
assert!(tt_entry.is_some());
|
||||
|
||||
// needs a bigger tt size because it could be a collision entry
|
||||
let wont_be_hash = from_fen(FEN_WONT_BE)?.hash;
|
||||
let wont_be_hash = from_fen(FEN_IMPOSSIBLE).unwrap().hash;
|
||||
let tt_entry = tt.lookup(wont_be_hash);
|
||||
|
||||
assert!(tt_entry.is_none());
|
||||
|
||||
Reference in New Issue
Block a user