Compare commits

...

4 Commits

Author SHA1 Message Date
5a9e17804c
tune: quiescence depth increased
4cb2cca doesn't seem to have any difference positive or negative in the
5+0 time control, so try this instead
2024-12-24 12:04:53 -05:00
4cb2cca02b
tune: quiescence 2024-12-24 11:27:48 -05:00
795c2c508d
feat: quiescence search
examines all captures using the new movegen, plus static exchange eval
2024-12-23 21:28:04 -05:00
cc40fb9a31
feat: capture-only movegen
also refactored movegen internally
2024-12-23 21:17:33 -05:00
3 changed files with 144 additions and 36 deletions

View File

@ -582,6 +582,24 @@ impl GenAttackers for Board {
} }
} }
/// Options for movegen.
pub struct MoveGenConfig {
/// Restricts movegen to only output capture moves.
///
/// This is more efficient than filtering captures after generating moves.
captures_only: bool,
legality: MoveGenType,
}
impl Default for MoveGenConfig {
fn default() -> Self {
MoveGenConfig {
captures_only: false,
legality: MoveGenType::Legal,
}
}
}
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
enum MoveGenType { enum MoveGenType {
/// Legal move generation. /// Legal move generation.
@ -590,19 +608,41 @@ enum MoveGenType {
_Pseudo, _Pseudo,
} }
/// Internal, slightly more general movegen interface /// Internal movegen interface with more options
trait MoveGenInternal { trait MoveGenInternal {
fn gen_moves_general(&mut self, gen_type: MoveGenType) -> impl IntoIterator<Item = Move>; fn gen_moves_general(&mut self, config: MoveGenConfig) -> impl IntoIterator<Item = Move>;
} }
pub trait MoveGen { pub trait MoveGen {
/// Legal move generation. /// Legal move generation.
fn gen_moves(&mut self) -> impl IntoIterator<Item = Move>; fn gen_moves(&mut self) -> impl IntoIterator<Item = Move>;
/// Pseudo-legal move generation (see `MoveGenType::_Pseudo` for more information).
fn gen_pseudo(&mut self) -> impl IntoIterator<Item = Move>;
/// Legal capture generation.
fn gen_captures(&mut self) -> impl IntoIterator<Item = Move>;
} }
impl<T: MoveGenInternal> MoveGen for T { impl<T: MoveGenInternal> MoveGen for T {
fn gen_moves(&mut self) -> impl IntoIterator<Item = Move> { fn gen_moves(&mut self) -> impl IntoIterator<Item = Move> {
self.gen_moves_general(MoveGenType::Legal) self.gen_moves_general(MoveGenConfig::default())
}
fn gen_pseudo(&mut self) -> impl IntoIterator<Item = Move> {
let config = MoveGenConfig {
legality: MoveGenType::_Pseudo,
..Default::default()
};
self.gen_moves_general(config)
}
fn gen_captures(&mut self) -> impl IntoIterator<Item = Move> {
let config = MoveGenConfig {
captures_only: true,
..Default::default()
};
self.gen_moves_general(config)
} }
} }
@ -651,6 +691,7 @@ fn move_slider(
move_list: &mut Vec<Move>, move_list: &mut Vec<Move>,
slide_type: SliderDirection, slide_type: SliderDirection,
keep_going: bool, keep_going: bool,
config: &MoveGenConfig,
) { ) {
let dirs = match slide_type { let dirs = match slide_type {
SliderDirection::Straight => DIRS_STRAIGHT.iter(), SliderDirection::Straight => DIRS_STRAIGHT.iter(),
@ -669,14 +710,23 @@ fn move_slider(
r = nr; r = nr;
c = nc; c = nc;
move_list.push(Move { let obstructed = board.get_piece(dest).is_some();
src,
dest,
move_type: MoveType::Normal,
});
// stop at other pieces. let mut gen_move = true;
if let Some(_cap_pc) = board.get_piece(dest) {
if config.captures_only && !obstructed {
gen_move = false;
}
if gen_move {
move_list.push(Move {
src,
dest,
move_type: MoveType::Normal,
});
}
if obstructed {
break; break;
} }
} else { } else {
@ -714,7 +764,7 @@ fn is_legal(board: &mut Board, mv: Move) -> bool {
} }
impl MoveGenInternal for Board { impl MoveGenInternal for Board {
fn gen_moves_general(&mut self, gen_type: MoveGenType) -> impl IntoIterator<Item = Move> { fn gen_moves_general(&mut self, config: MoveGenConfig) -> impl IntoIterator<Item = Move> {
let mut ret = Vec::new(); let mut ret = Vec::new();
let pl = self[self.turn]; let pl = self[self.turn];
macro_rules! squares { macro_rules! squares {
@ -724,16 +774,22 @@ impl MoveGenInternal for Board {
} }
for sq in squares!(Rook) { for sq in squares!(Rook) {
move_slider(self, sq, &mut ret, SliderDirection::Straight, true); move_slider(self, sq, &mut ret, SliderDirection::Straight, true, &config);
} }
for sq in squares!(Bishop) { for sq in squares!(Bishop) {
move_slider(self, sq, &mut ret, SliderDirection::Diagonal, true); move_slider(self, sq, &mut ret, SliderDirection::Diagonal, true, &config);
} }
for sq in squares!(Queen) { for sq in squares!(Queen) {
move_slider(self, sq, &mut ret, SliderDirection::Star, true); move_slider(self, sq, &mut ret, SliderDirection::Star, true, &config);
} }
for src in squares!(King) { for src in squares!(King) {
move_slider(self, src, &mut ret, SliderDirection::Star, false); move_slider(self, src, &mut ret, SliderDirection::Star, false, &config);
if config.captures_only {
// castling can't capture
continue;
}
let (r, c) = src.to_row_col_signed(); let (r, c) = src.to_row_col_signed();
let rights = self.castle[self.turn]; let rights = self.castle[self.turn];
let castle_sides = [(rights.k, 2, BOARD_WIDTH as isize - 1), (rights.q, -2, 0)]; let castle_sides = [(rights.k, 2, BOARD_WIDTH as isize - 1), (rights.q, -2, 0)];
@ -845,6 +901,10 @@ impl MoveGenInternal for Board {
} }
} }
if config.captures_only {
continue;
}
// single push // single push
let nc = c; let nc = c;
let dest = match Square::from_row_col_signed(nr, nc) { let dest = match Square::from_row_col_signed(nr, nc) {
@ -881,6 +941,9 @@ impl MoveGenInternal for Board {
let nr = r + dir.0; let nr = r + dir.0;
let nc = c + dir.1; let nc = c + dir.1;
if let Ok(dest) = Square::from_row_col_signed(nr, nc) { if let Ok(dest) = Square::from_row_col_signed(nr, nc) {
if config.captures_only && self.get_piece(dest).is_none() {
continue;
}
ret.push(Move { ret.push(Move {
src, src,
dest, dest,
@ -889,7 +952,7 @@ impl MoveGenInternal for Board {
} }
} }
} }
ret.retain(move |mv| match gen_type { ret.retain(move |mv| match config.legality {
MoveGenType::Legal => is_legal(self, *mv), MoveGenType::Legal => is_legal(self, *mv),
MoveGenType::_Pseudo => true, MoveGenType::_Pseudo => true,
}); });
@ -1243,10 +1306,7 @@ mod tests {
let all_cases = [augmented_test_cases, test_cases].concat(); let all_cases = [augmented_test_cases, test_cases].concat();
for (mut board, expected_moves) in all_cases { for (mut board, expected_moves) in all_cases {
let mut moves: Vec<Move> = board let mut moves: Vec<Move> = board.gen_pseudo().into_iter().collect();
.gen_moves_general(MoveGenType::_Pseudo)
.into_iter()
.collect();
moves.sort_unstable(); moves.sort_unstable();
let moves = moves; let moves = moves;
@ -1604,4 +1664,42 @@ mod tests {
assert_eq!(attackers, expected); assert_eq!(attackers, expected);
} }
} }
#[test]
fn test_capture_movegen() {
let test_cases = [(
// fen
"8/3q4/5N2/8/8/8/8/3K4 w - - 0 1",
// expected moves generated
"f6d7",
),
(
"8/8/8/3pP3/2K5/8/8/8 w - d6 0 1",
// holy hell
"e5d6 c4d5",
),
(
"8/2q5/3K4/8/8/8/8/8 w - - 0 1",
"d6c7",
),
(
"2Q5/3r2R1/2B1PN2/8/3K4/8/8/8 w - - 0 1",
"c6d7 e6d7 c8d7 f6d7 g7d7",
),
];
for (fen, expected) in test_cases {
let mut board = Board::from_fen(fen).unwrap();
let mut moves = board.gen_captures().into_iter().collect::<Vec<_>>();
moves.sort();
let mut expected = expected
.split_whitespace()
.map(Move::from_uci_algebraic)
.map(|x| x.unwrap())
.collect::<Vec<_>>();
expected.sort();
assert_eq!(moves, expected, "failed '{}'", fen);
}
}
} }

View File

@ -16,7 +16,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
pub use crate::coordination::{ pub use crate::coordination::{
GoMessage, MsgBestmove, MsgToEngine, MsgToMain, UCIMode, UCIModeMachine, UCIModeTransition, GoMessage, MsgBestmove, MsgToEngine, MsgToMain, UCIMode, UCIModeMachine, UCIModeTransition,
}; };
pub use crate::eval::{eval_metrics, Eval, EvalInt, EvalMetrics}; pub use crate::eval::{eval_metrics, Eval, EvalInt, EvalMetrics, EvalSEE};
pub use crate::fen::{FromFen, ToFen}; pub use crate::fen::{FromFen, ToFen};
pub use crate::movegen::{FromUCIAlgebraic, GenAttackers, Move, MoveGen, ToUCIAlgebraic}; pub use crate::movegen::{FromUCIAlgebraic, GenAttackers, Move, MoveGen, ToUCIAlgebraic};
pub use crate::search::{ pub use crate::search::{

View File

@ -124,7 +124,7 @@ impl Default for SearchConfig {
alpha_beta_on: true, alpha_beta_on: true,
// try to make this even to be more conservative and avoid horizon problem // try to make this even to be more conservative and avoid horizon problem
depth: 10, depth: 10,
qdepth: 1, qdepth: 3,
enable_trans_table: true, enable_trans_table: true,
transposition_size: 24, transposition_size: 24,
} }
@ -234,10 +234,12 @@ fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<M
// our best is their worst // our best is their worst
let beta = mm.beta.unwrap_or(EVAL_BEST); let beta = mm.beta.unwrap_or(EVAL_BEST);
let mut mvs: Vec<_> = board let mvs = if mm.quiesce {
.gen_moves() board.gen_captures().into_iter().collect::<Vec<_>>()
.into_iter() } else {
.collect::<Vec<_>>() board.gen_moves().into_iter().collect::<Vec<_>>()
};
let mut mvs: Vec<_> = mvs
.into_iter() .into_iter()
.map(|mv| (move_priority(board, &mv, state), mv)) .map(|mv| (move_priority(board, &mv, state), mv))
.collect(); .collect();
@ -266,14 +268,11 @@ fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<M
// determine moves that are allowed in quiescence // determine moves that are allowed in quiescence
if mm.quiesce { if mm.quiesce {
// use static exchange evaluation to prune moves
mvs.retain(|(_priority, mv): &(EvalInt, Move)| -> bool { mvs.retain(|(_priority, mv): &(EvalInt, Move)| -> bool {
if let Some(recap_sq) = board.recap_sq { let see = board.eval_see(mv.dest, board.turn);
if mv.dest == recap_sq {
return false;
}
}
false see >= 0
}); });
} }
@ -420,12 +419,23 @@ impl TimeLimits {
) -> TimeLimits { ) -> TimeLimits {
// hard timeout (max) // hard timeout (max)
let mut hard_ms = 100_000; let mut hard_ms = 100_000;
// soft timeout (max) // soft timeout (default max)
let mut soft_ms = 1_200; let mut soft_ms = 1_200;
// if we have more than 5 minutes, and we're out of the opening, we can afford to think longer // in some situations we can think longer
if ourtime_ms > 300_000 && eval.phase <= 13 { if eval.phase <= 13 {
soft_ms = 4_500 // phase 13 is a single capture of a minor/major piece, so consider that out of the
// opening
soft_ms = if ourtime_ms > 300_000 {
4_500
} else if ourtime_ms > 600_000 {
8_000
} else if ourtime_ms > 1_200_000 {
12_000
} else {
soft_ms
}
} }
let factor = if ourtime_ms > 5_000 { 10 } else { 40 }; let factor = if ourtime_ms > 5_000 { 10 } else { 40 };