#[cfg(not(feature = "search-no-pass-rules"))]
use crate::eval::evaluate_pass_rights;
use crate::position::Position;
use crate::types::{Bound, DEPTH_QS, DEPTH_UNSEARCHED, MAX_PLY, Move, Value};
use super::alpha_beta::{SearchContext, SearchState, draw_jitter, to_corrected_static_eval};
use super::eval_helpers::correction_value;
use super::movepicker::piece_value;
use super::search_helpers::{
check_abort, clear_cont_history_for_null, cont_history_tables, do_move_and_push, nnue_evaluate,
nnue_pop, set_cont_history_for_move,
};
use super::stats::{inc_stat, inc_stat_by_depth};
#[cfg(feature = "tt-trace")]
use super::tt_sanity::{
InvalidTtLog, TtCutoffTrace, TtProbeTrace, TtWriteTrace, helper_tt_write_enabled_for_depth,
maybe_log_invalid_tt_data, maybe_trace_tt_cutoff, maybe_trace_tt_probe, maybe_trace_tt_write,
};
use super::tt_sanity::{is_valid_tt_eval, is_valid_tt_stored_value};
use super::types::{NodeType, OrderedMovesBuffer, draw_value, value_from_tt, value_to_tt};
use super::{LimitsType, MovePicker, TimeManagement};
#[allow(clippy::too_many_arguments)]
pub(super) fn qsearch<const NT: u8>(
st: &mut SearchState,
ctx: &SearchContext<'_>,
pos: &mut Position,
alpha: Value,
beta: Value,
ply: i32,
limits: &LimitsType,
time_manager: &mut TimeManagement,
) -> Value {
let pv_node = NT == NodeType::PV as u8;
let in_check = pos.in_check();
inc_stat!(st, qs_nodes);
#[cfg(feature = "search-stats")]
{
if in_check {
st.stats.qs_in_check_nodes += 1;
}
}
if ply >= MAX_PLY {
return if in_check {
Value::ZERO
} else {
nnue_evaluate(st, pos)
};
}
if pv_node && st.sel_depth < ply + 1 {
st.sel_depth = ply + 1;
}
if check_abort(st, ctx, limits, time_manager) {
return Value::ZERO;
}
let rep_state = pos.repetition_state(ply);
if rep_state.is_repetition() || rep_state.is_superior_inferior() {
let v = draw_value(rep_state, pos.side_to_move(), &ctx.draw_value_table);
if v != Value::NONE {
if rep_state == crate::types::RepetitionState::Draw {
let jittered = Value::new(v.raw() + draw_jitter(st.nodes, ctx.tune_params));
return jittered;
}
return value_from_tt(v, ply);
}
}
if ctx.max_moves_to_draw > 0 && pos.game_ply() > ctx.max_moves_to_draw {
return Value::new(
ctx.draw_value_table[pos.side_to_move() as usize].raw()
+ draw_jitter(st.nodes, ctx.tune_params),
);
}
let key = pos.key();
let tt_result = ctx.tt.probe(key, pos);
let tt_hit = tt_result.found;
let mut tt_data = tt_result.data;
let pv_hit = tt_hit && tt_data.is_pv;
st.stack[ply as usize].tt_hit = tt_hit;
let tt_move = if tt_hit { tt_data.mv } else { Move::NONE };
let mut tt_value = if tt_hit {
value_from_tt(tt_data.value, ply)
} else {
Value::NONE
};
if tt_hit && !is_valid_tt_stored_value(tt_data.value) {
#[cfg(feature = "tt-trace")]
maybe_log_invalid_tt_data(InvalidTtLog {
reason: "invalid_value",
stage: "qsearch_probe",
thread_id: ctx.thread_id,
ply,
key,
depth: tt_data.depth,
bound: tt_data.bound,
tt_move,
stored_value: tt_data.value,
converted_value: tt_value,
eval: tt_data.eval,
});
tt_value = Value::NONE;
}
if tt_hit && !is_valid_tt_eval(tt_data.eval) {
#[cfg(feature = "tt-trace")]
maybe_log_invalid_tt_data(InvalidTtLog {
reason: "invalid_eval",
stage: "qsearch_probe",
thread_id: ctx.thread_id,
ply,
key,
depth: tt_data.depth,
bound: tt_data.bound,
tt_move,
stored_value: tt_data.value,
converted_value: tt_value,
eval: tt_data.eval,
});
tt_data.eval = Value::NONE;
}
#[cfg(feature = "tt-trace")]
maybe_trace_tt_probe(TtProbeTrace {
stage: "qsearch_probe",
thread_id: ctx.thread_id,
ply,
key,
hit: tt_hit,
depth: tt_data.depth,
bound: tt_data.bound,
tt_move,
stored_value: tt_data.value,
converted_value: tt_value,
eval: tt_data.eval,
root_move: if ply >= 1 {
st.stack[0].current_move
} else {
Move::NONE
},
});
if tt_hit {
inc_stat!(st, qs_tt_hit);
}
if !pv_node
&& tt_hit
&& tt_data.depth >= DEPTH_QS
&& tt_value != Value::NONE
&& tt_data.bound.can_cutoff(tt_value, beta)
{
#[cfg(feature = "tt-trace")]
maybe_trace_tt_cutoff(TtCutoffTrace {
stage: "qsearch_cutoff",
thread_id: ctx.thread_id,
ply,
key,
search_depth: DEPTH_QS,
depth: tt_data.depth,
bound: tt_data.bound,
value: tt_value,
beta,
root_move: if ply >= 1 {
st.stack[0].current_move
} else {
Move::NONE
},
});
inc_stat!(st, qs_tt_cutoff);
return tt_value;
}
let mut best_move = Move::NONE;
let corr_value = correction_value(st, ctx, pos, ply);
let mut unadjusted_static_eval = Value::NONE;
let mut static_eval = if in_check {
Value::NONE
} else if tt_hit && tt_data.eval != Value::NONE {
unadjusted_static_eval = tt_data.eval;
unadjusted_static_eval
} else {
if !tt_hit {
let mate_move = pos.mate_1ply();
if mate_move.is_some() {
let mate_value = Value::mate_in(ply + 1);
#[cfg(feature = "tt-trace")]
let allow_write = ctx.allow_tt_write
&& helper_tt_write_enabled_for_depth(ctx.thread_id, Bound::Exact, DEPTH_QS);
#[cfg(not(feature = "tt-trace"))]
let allow_write = ctx.allow_tt_write;
if allow_write {
#[cfg(feature = "tt-trace")]
maybe_trace_tt_write(TtWriteTrace {
stage: "qsearch_mate1_store",
thread_id: ctx.thread_id,
ply,
key,
depth: DEPTH_QS,
bound: Bound::Exact,
is_pv: st.stack[ply as usize].tt_pv,
tt_move: mate_move,
stored_value: mate_value,
eval: unadjusted_static_eval,
root_move: if ply >= 1 {
st.stack[0].current_move
} else {
Move::NONE
},
});
tt_result.write(
key,
mate_value,
st.stack[ply as usize].tt_pv,
Bound::Exact,
DEPTH_QS,
mate_move,
unadjusted_static_eval,
ctx.tt.generation(),
);
inc_stat_by_depth!(st, tt_write_by_depth, 0);
}
return mate_value;
}
}
unadjusted_static_eval = nnue_evaluate(st, pos);
unadjusted_static_eval
};
if !in_check && unadjusted_static_eval != Value::NONE {
static_eval = to_corrected_static_eval(unadjusted_static_eval, corr_value);
#[cfg(not(feature = "search-no-pass-rules"))]
{
static_eval += evaluate_pass_rights(pos, pos.game_ply() as u16);
}
}
st.stack[ply as usize].static_eval = static_eval;
let mut alpha = alpha;
let mut best_value = if in_check {
-Value::INFINITE
} else {
static_eval
};
if !in_check && tt_hit && tt_value != Value::NONE && !tt_value.is_mate_score() {
let bound_matches = if tt_value > best_value {
tt_data.bound.is_lower_or_exact()
} else {
matches!(tt_data.bound, Bound::Upper | Bound::Exact)
};
if bound_matches {
best_value = tt_value;
}
}
if !in_check && best_value >= beta {
inc_stat!(st, qs_stand_pat_cutoff);
let mut v = best_value;
if !v.is_mate_score() {
v = Value::new((v.raw() + beta.raw()) / 2);
}
if !tt_hit {
#[cfg(feature = "tt-trace")]
let allow_write = ctx.allow_tt_write
&& helper_tt_write_enabled_for_depth(ctx.thread_id, Bound::Lower, DEPTH_UNSEARCHED);
#[cfg(not(feature = "tt-trace"))]
let allow_write = ctx.allow_tt_write;
if allow_write {
#[cfg(feature = "tt-trace")]
maybe_trace_tt_write(TtWriteTrace {
stage: "qsearch_stand_pat_store",
thread_id: ctx.thread_id,
ply,
key,
depth: DEPTH_UNSEARCHED,
bound: Bound::Lower,
is_pv: false,
tt_move: Move::NONE,
stored_value: value_to_tt(v, ply),
eval: unadjusted_static_eval,
root_move: if ply >= 1 {
st.stack[0].current_move
} else {
Move::NONE
},
});
tt_result.write(
key,
value_to_tt(v, ply),
false,
Bound::Lower,
DEPTH_UNSEARCHED,
Move::NONE,
unadjusted_static_eval,
ctx.tt.generation(),
);
inc_stat_by_depth!(st, tt_write_by_depth, 0);
}
}
return v;
}
if !in_check && best_value > alpha {
alpha = best_value;
}
let futility_base = if in_check {
Value::NONE
} else {
static_eval + Value::new(ctx.tune_params.qsearch_futility_base)
};
let prev_move = if ply >= 1 {
st.stack[(ply - 1) as usize].current_move
} else {
Move::NONE
};
let ordered_moves = {
let cont_tables = cont_history_tables(st, ctx, ply);
let mut buf_moves = OrderedMovesBuffer::new();
{
let mut mp = if in_check {
MovePicker::new_evasions(
pos,
tt_move,
ply,
cont_tables,
ctx.generate_all_legal_moves,
)
} else {
MovePicker::new(
pos,
tt_move,
DEPTH_QS,
ply,
cont_tables,
ctx.generate_all_legal_moves,
)
};
loop {
let mv = {
let h = unsafe { ctx.history.as_ref_unchecked() };
mp.next_move(pos, h)
};
if mv == Move::NONE {
break;
}
buf_moves.push(mv);
}
}
buf_moves
};
#[cfg(feature = "search-stats")]
{
st.stats.qs_moves_generated += ordered_moves.len() as u64;
}
let mut move_count = 0;
for mv in ordered_moves.iter() {
if mv.is_pass() {
continue;
}
if !pos.is_legal(mv) {
continue;
}
let gives_check = pos.gives_check(mv);
let capture = pos.capture_stage(mv);
move_count += 1;
if !best_value.is_loss() {
if !gives_check
&& (!prev_move.is_normal() || mv.to() != prev_move.to())
&& futility_base != Value::NONE
{
if move_count > 2 {
inc_stat!(st, qs_futility_pruned);
continue;
}
let futility_value = futility_base + Value::new(piece_value(pos.piece_on(mv.to())));
if futility_value <= alpha {
inc_stat!(st, qs_futility_pruned);
best_value = best_value.max(futility_value);
continue;
}
if !pos.see_ge(mv, alpha - futility_base) {
inc_stat!(st, qs_futility_pruned);
best_value = alpha.min(futility_base);
continue;
}
}
if !capture {
continue;
}
if !pos.see_ge(mv, Value::new(-78)) {
inc_stat!(st, qs_see_margin_pruned);
continue;
}
}
st.stack[ply as usize].current_move = mv;
inc_stat!(st, qs_moves_searched);
do_move_and_push(st, pos, mv, gives_check, ctx.tt);
if mv.is_pass() {
clear_cont_history_for_null(st, ctx, ply);
} else {
let cont_hist_pc = mv.moved_piece_after();
let cont_hist_to = mv.to();
set_cont_history_for_move(st, ctx, ply, in_check, capture, cont_hist_pc, cont_hist_to);
}
let value = -qsearch::<NT>(st, ctx, pos, -beta, -alpha, ply + 1, limits, time_manager);
nnue_pop(st);
pos.undo_move(mv);
if st.abort {
return Value::ZERO;
}
if value > best_value {
best_value = value;
if value > alpha {
best_move = mv;
if value >= beta {
break;
}
alpha = value;
}
}
}
if in_check && move_count == 0 {
return Value::mated_in(ply);
}
if !best_value.is_mate_score() && best_value > beta {
best_value = Value::new((best_value.raw() + beta.raw()) / 2);
}
let bound = if best_value >= beta {
Bound::Lower
} else {
Bound::Upper
};
#[cfg(feature = "tt-trace")]
let allow_write =
ctx.allow_tt_write && helper_tt_write_enabled_for_depth(ctx.thread_id, bound, DEPTH_QS);
#[cfg(not(feature = "tt-trace"))]
let allow_write = ctx.allow_tt_write;
if allow_write {
#[cfg(feature = "tt-trace")]
maybe_trace_tt_write(TtWriteTrace {
stage: "qsearch_store",
thread_id: ctx.thread_id,
ply,
key,
depth: DEPTH_QS,
bound,
is_pv: pv_hit,
tt_move: best_move,
stored_value: value_to_tt(best_value, ply),
eval: unadjusted_static_eval,
root_move: if ply >= 1 {
st.stack[0].current_move
} else {
Move::NONE
},
});
tt_result.write(
key,
value_to_tt(best_value, ply),
pv_hit,
bound,
DEPTH_QS,
best_move,
unadjusted_static_eval,
ctx.tt.generation(),
);
inc_stat_by_depth!(st, tt_write_by_depth, 0);
}
best_value
}