Make mvv_lva to map pieces to their material score instead of abritrary matrix
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,20 +1,10 @@
|
||||
use crate::{board::mailbox::Mailbox, movegen::r#move::Move};
|
||||
use crate::{
|
||||
board::mailbox::Mailbox, evaluation::evaluation::material_score, movegen::r#move::Move,
|
||||
};
|
||||
|
||||
// Rows: Aggressors (P, N, B, R, Q, K)
|
||||
// Columns: Victims (K, Q, R, B, N, P)
|
||||
#[rustfmt::skip]
|
||||
const MVV_LVA: [[usize; 6]; 6] = [
|
||||
[30, 29, 28, 27, 26, 0],
|
||||
[25, 24, 23, 22, 21, 0],
|
||||
[20, 19, 18, 17, 16, 0],
|
||||
[15, 14, 13, 12, 11, 0],
|
||||
[10, 9, 8, 7, 6, 0],
|
||||
[5, 4, 3, 2, 1, 0],
|
||||
];
|
||||
|
||||
pub const fn score_by_mvv_lva(mailbox: &Mailbox, mv: Move) -> usize {
|
||||
pub const fn mvv_lva(mailbox: &Mailbox, mv: Move) -> i32 {
|
||||
match (mailbox.piece_at(mv.src), mailbox.piece_at(mv.dst)) {
|
||||
(Some(aggressor), Some(victim)) => MVV_LVA[aggressor.idx()][victim.idx()],
|
||||
(Some(aggressor), Some(victim)) => material_score(victim) - material_score(aggressor),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -22,9 +12,9 @@ pub const fn score_by_mvv_lva(mailbox: &Mailbox, mv: Move) -> usize {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
board::{board::PieceType, fen::from_fen, square::Square},
|
||||
board::{fen::from_fen, square::Square},
|
||||
movegen::r#move::{Move, MoveType},
|
||||
search::move_ordering::{score_by_mvv_lva, MVV_LVA},
|
||||
search::move_ordering::mvv_lva,
|
||||
};
|
||||
|
||||
const FEN: &'static str = "1r2k2r/2P1pq1p/2npb3/1p3ppP/p3P3/P2B1Q2/1P1PNPP1/R3K2R w KQk g6 0 1";
|
||||
@@ -33,10 +23,8 @@ mod tests {
|
||||
fn test_score_by_mvv_lva() -> Result<(), String> {
|
||||
let game = from_fen(FEN)?;
|
||||
let f3f5 = Move::with_type(Square::F3, Square::F5, MoveType::Capture);
|
||||
let actual = score_by_mvv_lva(&game.mailbox, f3f5);
|
||||
let expected = MVV_LVA[PieceType::Queen.idx()][PieceType::Pawn.idx()];
|
||||
|
||||
assert_eq!(expected, actual);
|
||||
assert_eq!(mvv_lva(&game.mailbox, f3f5), -800);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -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