stub: check extension

it does find mate in 4 often but it's broken for actually playing
This commit is contained in:
dogeystamp 2024-12-20 21:43:50 -05:00
parent 86e5780f26
commit b9c4e03ae6
3 changed files with 150 additions and 48 deletions

View File

@ -466,6 +466,9 @@ pub struct Board {
/// Hash state to incrementally update.
zobrist: Zobrist,
/// Last captured square
recap_sq: Option<Square>,
}
impl Board {
@ -541,6 +544,7 @@ impl Board {
castle: CastleRights(self.castle.0),
eval: Default::default(),
zobrist: Zobrist::default(),
recap_sq: self.recap_sq.map(|sq| sq.mirror_vert()),
};
new_board.castle.0.reverse();

View File

@ -93,6 +93,8 @@ pub struct AntiMove {
castle: CastleRights,
/// En passant target square prior to this move.
ep_square: Option<Square>,
/// En passant target square prior to this move.
recap_sq: Option<Square>,
}
impl AntiMove {
@ -104,6 +106,7 @@ impl AntiMove {
pos.half_moves = self.half_moves;
pos.castle = self.castle;
pos.ep_square = self.ep_square;
pos.recap_sq = self.recap_sq;
/// Restore captured piece at a given square.
macro_rules! cap_sq {
@ -182,6 +185,7 @@ impl Move {
half_moves: pos.half_moves,
castle: pos.castle,
ep_square: pos.ep_square,
recap_sq: pos.recap_sq,
};
// undo hashes (we will update them at the end of this function)
@ -191,6 +195,9 @@ impl Move {
let ep_square = pos.ep_square;
pos.ep_square = None;
// reset recapture square
pos.recap_sq = None;
if pos.turn == Color::Black {
pos.full_moves += 1;
}
@ -235,6 +242,9 @@ impl Move {
col: pc_src.col,
},
);
if cap_pc.is_some() {
pos.recap_sq = Some(self.dest);
}
anti_move.cap = cap_pc.map(|pc| pc.pc);
}
MoveType::Normal => {
@ -243,6 +253,9 @@ impl Move {
let pc_dest: Option<ColPiece> = pos.get_piece(self.dest);
anti_move.cap = pc_dest.map(|pc| pc.pc);
if pc_dest.is_some() {
pos.recap_sq = Some(self.dest);
}
let (src_row, src_col) = self.src.to_row_col_signed();
let (dest_row, dest_col) = self.dest.to_row_col_signed();
@ -1431,4 +1444,15 @@ mod tests {
assert_eq!(mv.to_uci_algebraic(), tc);
}
}
#[test]
fn test_recap_sq() {
let mut board = Board::from_fen("R1b2r1k/6pp/2pqQ3/2n2P2/5P1b/1rN1Pp2/1P5P/2B2KNR b - - 1 22").unwrap();
let mv = Move::from_uci_algebraic("c8e6").unwrap();
let anti_mv = mv.make(&mut board);
let dest_sq: Square = "e6".parse().unwrap();
assert_eq!(board.recap_sq, Some(dest_sq));
anti_mv.unmake(&mut board);
assert_eq!(board.recap_sq, None);
}
}

View File

@ -110,6 +110,8 @@ pub struct SearchConfig {
pub alpha_beta_on: bool,
/// Limit regular search depth
pub depth: usize,
/// Limit quiescence search depth
pub qdepth: usize,
/// Enable transposition table.
pub enable_trans_table: bool,
/// Transposition table size (2^n where this is n)
@ -122,6 +124,7 @@ impl Default for SearchConfig {
alpha_beta_on: true,
// try to make this even to be more conservative and avoid horizon problem
depth: 10,
qdepth: 6,
enable_trans_table: true,
transposition_size: 24,
}
@ -155,26 +158,35 @@ fn move_priority(board: &mut Board, mv: &Move, state: &mut EngineState) -> EvalI
eval
}
/// State specifically for a minmax call.
struct MinmaxState {
/// how many plies left to search in this call
depth: usize,
/// best score (absolute, from current player perspective) guaranteed for current player.
alpha: Option<EvalInt>,
/// best score (absolute, from current player perspective) guaranteed for other player.
beta: Option<EvalInt>,
/// quiescence search flag
quiesce: bool,
/// how recently (plies) in quiescence search was there check
was_qcheck: u8,
}
/// Search the game tree to find the absolute (positive good) move and corresponding eval for the
/// current player.
///
/// This also integrates quiescence search, which looks for a calm (quiescent) position where
/// there are no recaptures, no checks.
///
/// # Arguments
///
/// * board: board position to analyze.
/// * depth: how deep to analyze the game tree.
/// * alpha: best score (absolute, from current player perspective) guaranteed for current player.
/// * beta: best score (absolute, from current player perspective) guaranteed for other player.
///
/// # Returns
///
/// The best line (in reverse move order), and its corresponding absolute eval for the current player.
fn minmax(
board: &mut Board,
state: &mut EngineState,
depth: usize,
alpha: Option<EvalInt>,
beta: Option<EvalInt>,
) -> (Vec<Move>, SearchEval) {
fn minmax(board: &mut Board, state: &mut EngineState, mm: MinmaxState) -> (Vec<Move>, SearchEval) {
// these operations are relatively expensive, so only run them occasionally
if state.node_count % (1 << 16) == 0 {
// respect the hard stop if given
@ -199,15 +211,36 @@ fn minmax(
}
}
// default to worst, then gradually improve
let mut alpha = alpha.unwrap_or(EVAL_WORST);
// our best is their worst
let beta = beta.unwrap_or(EVAL_BEST);
if depth == 0 {
let eval = board.eval() * EvalInt::from(board.turn.sign());
return (Vec::new(), SearchEval::Exact(eval));
// only determine if in check during qsearch
let mut is_in_qcheck: bool = false;
if mm.quiesce || mm.depth == 0 {
is_in_qcheck = board.is_check(board.turn);
}
let is_in_qcheck = is_in_qcheck;
if mm.depth == 0 {
if mm.quiesce {
let eval = board.eval() * EvalInt::from(board.turn.sign());
return (Vec::new(), SearchEval::Exact(eval));
} else {
return minmax(
board,
state,
MinmaxState {
depth: state.config.qdepth,
alpha: mm.alpha,
beta: mm.beta,
quiesce: true,
was_qcheck: mm.was_qcheck,
},
);
}
}
// default to worst, then gradually improve
let mut alpha = mm.alpha.unwrap_or(EVAL_WORST);
// our best is their worst
let beta = mm.beta.unwrap_or(EVAL_BEST);
let mut mvs: Vec<_> = board
.gen_moves()
@ -220,7 +253,7 @@ fn minmax(
// get transposition table entry
if state.config.enable_trans_table {
if let Some(entry) = &state.cache[board.zobrist] {
if entry.depth >= depth {
if entry.depth >= mm.depth {
if let SearchEval::Exact(_) | SearchEval::Upper(_) = entry.eval {
// no point looking for a better move
return (vec![entry.best_move], entry.eval);
@ -237,18 +270,58 @@ fn minmax(
let mut best_move: Option<Move> = None;
let mut best_continuation: Vec<Move> = Vec::new();
if mvs.is_empty() {
if board.is_check(board.turn) {
let n_non_qmoves = mvs.len();
// determine moves are allowed in quiescence
if mm.quiesce {
mvs.retain(|(_priority, mv): &(EvalInt, Move)| -> bool {
if let Some(recap_sq) = board.recap_sq {
if mv.dest == recap_sq {
//return true;
}
}
// allow responding to checks, and giving check again
if mm.was_qcheck <= 2 || is_in_qcheck {
return true;
}
false
});
}
if n_non_qmoves == 0 {
let is_in_check = if mm.quiesce {
is_in_qcheck
} else {
board.is_check(board.turn)
};
if is_in_check {
return (Vec::new(), SearchEval::Checkmate(-1));
} else {
// stalemate
return (Vec::new(), SearchEval::Exact(0));
}
} else if mvs.is_empty() {
// pruned all the moves due to quiescence
let eval = board.eval() * EvalInt::from(board.turn.sign());
return (Vec::new(), SearchEval::Exact(eval));
}
for (_priority, mv) in mvs {
let anti_mv = mv.make(board);
let (continuation, score) = minmax(board, state, depth - 1, Some(-beta), Some(-alpha));
let (continuation, score) = minmax(
board,
state,
MinmaxState {
depth: mm.depth - 1,
alpha: Some(-beta),
beta: Some(-alpha),
quiesce: mm.quiesce,
was_qcheck: if is_in_qcheck { 1 } else { mm.was_qcheck + 1 },
},
);
// propagate hard stops
if matches!(score, SearchEval::Stopped) {
@ -279,11 +352,11 @@ fn minmax(
if let Some(best_move) = best_move {
best_continuation.push(best_move);
if state.config.enable_trans_table {
if state.config.enable_trans_table && !mm.quiesce {
state.cache[board.zobrist] = Some(TranspositionEntry {
best_move,
eval: abs_best,
depth,
depth: mm.depth,
});
}
}
@ -306,42 +379,43 @@ pub type TranspositionTable = ZobristTable<TranspositionEntry>;
/// Iteratively deepen search until it is stopped.
fn iter_deep(board: &mut Board, state: &mut EngineState) -> (Vec<Move>, SearchEval) {
// always preserve two lines (1 is most recent)
let (mut line1, mut eval1) = minmax(board, state, 1, None, None);
let (mut line2, mut eval2) = (line1.clone(), eval1);
let (mut prev_line, mut prev_eval) = minmax(
board,
state,
MinmaxState {
depth: 1,
alpha: None,
beta: None,
quiesce: false,
was_qcheck: 5,
},
);
for depth in 2..=state.config.depth {
let (line, eval) = minmax(board, state, depth, None, None);
let mut have_to_ret = false;
// depth of the line we're about to return.
// our knock-off "quiescence" is skeptical of odd depths, so we need to know this.
let mut ret_depth = depth;
let (line, eval) = minmax(
board,
state,
MinmaxState {
depth,
alpha: None,
beta: None,
quiesce: false,
was_qcheck: 5,
},
);
if matches!(eval, SearchEval::Stopped) {
ret_depth -= 1;
have_to_ret = true;
return (prev_line, prev_eval);
} else {
(line2, eval2) = (line1, eval1);
(line1, eval1) = (line, eval);
if let Some(soft_lim) = state.time_lims.soft {
if Instant::now() > soft_lim {
have_to_ret = true;
return (line, eval);
}
}
}
if have_to_ret {
if ret_depth & 1 == 1 && (EvalInt::from(eval1) - EvalInt::from(eval2) > 300) {
// be skeptical if we move last and we suddenly earn a lot of
// centipawns. this may be a sign of horizon problem
return (line2, eval2);
} else {
return (line1, eval1);
}
(prev_line, prev_eval) = (line, eval);
}
}
(line1, eval1)
(prev_line, prev_eval)
}
/// Deadlines for the engine to think of a move.