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:
stefiosif
2024-11-13 21:13:05 +02:00
parent 429485ae73
commit c2414c3f6e
11 changed files with 207 additions and 128 deletions

View File

@@ -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());

View File

@@ -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,
}
}
}

View File

@@ -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(())
}

View File

@@ -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(())
}

View File

@@ -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
View 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,
}
}
}

View File

@@ -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());