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

11
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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"] }

View File

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

View File

@@ -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\

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