fix: not going for checkmate
engine will now prioritize closer mates rather than continuously going "ah i'll checkmate next move" then never checkkmating
This commit is contained in:
parent
b36faba3ef
commit
b7b3c6c5b8
28
src/eval.rs
28
src/eval.rs
@ -46,7 +46,7 @@ pub(crate) mod eval_score {
|
|||||||
/// Score from a given perspective (e.g. midgame, endgame).
|
/// Score from a given perspective (e.g. midgame, endgame).
|
||||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
|
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
|
||||||
pub struct EvalScore {
|
pub struct EvalScore {
|
||||||
pub(crate) score: EvalInt,
|
pub score: EvalInt,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EvalScore {
|
impl EvalScore {
|
||||||
@ -157,12 +157,12 @@ const fn make_pst(val: PstSide, base_val: EvalInt) -> PstPiece {
|
|||||||
pub const PST_MIDGAME: Pst = Pst([
|
pub const PST_MIDGAME: Pst = Pst([
|
||||||
// rook
|
// rook
|
||||||
make_pst([
|
make_pst([
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 8
|
1, 3, 2, 1, 4, 3, 2, 1, // 8
|
||||||
20, 20, 20, 20, 20, 20, 20, 20, // 7
|
20, 20, 20, 20, 20, 20, 20, 20, // 7
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 6
|
1, 2, 3, 1, 2, 1, 2, 1, // 6
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 5
|
-1, -2, 1, 2, 1, -1, 1, -1, // 5
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 4
|
-1, -1, 2, -1, 1, -1, 2, 1, // 4
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 3
|
2, 1, 1, 0, 0, 0, 0, 0, // 3
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
||||||
0, 0, 0, 10, 10, 5, 0, 0, // 1
|
0, 0, 0, 10, 10, 5, 0, 0, // 1
|
||||||
// a b c d e f g h
|
// a b c d e f g h
|
||||||
@ -183,14 +183,14 @@ pub const PST_MIDGAME: Pst = Pst([
|
|||||||
|
|
||||||
// knight
|
// knight
|
||||||
make_pst([
|
make_pst([
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 8
|
-5, -5, -5, -5, -5, -5, -5, -5, // 8
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 7
|
-5, 0, 0, 0, 0, 0, 0, -5, // 7
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 6
|
-5, 1, 0, 0, 0, 0, 0, -5, // 6
|
||||||
0, 0, 0, 10, 10, 0, 0, 0, // 5
|
-5, 2, 0, 10, 10, 0, 0, -5, // 5
|
||||||
0, 0, 0, 10, 10, 0, 0, 0, // 4
|
-5, 0, 1, 10, 10, 0, 0, -5, // 4
|
||||||
0, 0, 10, 0, 0, 10, 0, 0, // 3
|
-5, 2, 10, 0, 0, 10, 0, -5, // 3
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 2
|
-5, 1, 0, 0, 0, 0, 0, -5, // 2
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, // 1
|
-5, -5, -5, -5, -5, -5, -5, -5, // 1
|
||||||
// a b c d e f g h
|
// a b c d e f g h
|
||||||
], 300),
|
], 300),
|
||||||
|
|
||||||
|
124
src/search.rs
124
src/search.rs
@ -34,7 +34,64 @@ mod test_eval_int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Search the game tree to find the absolute (positive good) eval for the current player.
|
/// Eval in the context of search.
|
||||||
|
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||||
|
enum SearchEval {
|
||||||
|
/// Mate in |n| - 1 half moves, negative for own mate.
|
||||||
|
Checkmate(i8),
|
||||||
|
/// Centipawn score.
|
||||||
|
Centipawns(EvalInt),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SearchEval {
|
||||||
|
/// Flip side, and increment the "mate in n" counter.
|
||||||
|
fn increment(self) -> Self {
|
||||||
|
match self {
|
||||||
|
SearchEval::Checkmate(n) => {
|
||||||
|
debug_assert_ne!(n, 0);
|
||||||
|
if n < 0 {
|
||||||
|
Self::Checkmate(-(n - 1))
|
||||||
|
} else {
|
||||||
|
Self::Checkmate(-(n + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SearchEval::Centipawns(eval) => Self::Centipawns(-eval),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SearchEval> for EvalInt {
|
||||||
|
fn from(value: SearchEval) -> Self {
|
||||||
|
match value {
|
||||||
|
SearchEval::Checkmate(n) => {
|
||||||
|
debug_assert_ne!(n, 0);
|
||||||
|
if n < 0 {
|
||||||
|
EVAL_WORST + EvalInt::from(n)
|
||||||
|
} else {
|
||||||
|
EVAL_BEST - EvalInt::from(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SearchEval::Centipawns(eval) => eval,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Ord for SearchEval {
|
||||||
|
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||||
|
let e1 = EvalInt::from(*self);
|
||||||
|
let e2 = EvalInt::from(*other);
|
||||||
|
e1.cmp(&e2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialOrd for SearchEval {
|
||||||
|
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||||
|
Some(self.cmp(other))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search the game tree to find the absolute (positive good) move and corresponding eval for the
|
||||||
|
/// current player.
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
///
|
///
|
||||||
@ -42,7 +99,16 @@ mod test_eval_int {
|
|||||||
/// * depth: how deep to analyze the game tree.
|
/// * depth: how deep to analyze the game tree.
|
||||||
/// * alpha: best score (absolute, from current player perspective) guaranteed for current player.
|
/// * alpha: best score (absolute, from current player perspective) guaranteed for current player.
|
||||||
/// * beta: best score (absolute, from current player perspective) guaranteed for other player.
|
/// * beta: best score (absolute, from current player perspective) guaranteed for other player.
|
||||||
fn minmax(board: &mut Board, depth: usize, alpha: Option<EvalInt>, beta: Option<EvalInt>) -> EvalInt {
|
///
|
||||||
|
/// # Returns
|
||||||
|
///
|
||||||
|
/// The best move, and its corresponding absolute eval for the current player.
|
||||||
|
fn minmax(
|
||||||
|
board: &mut Board,
|
||||||
|
depth: usize,
|
||||||
|
alpha: Option<EvalInt>,
|
||||||
|
beta: Option<EvalInt>,
|
||||||
|
) -> (Option<Move>, SearchEval) {
|
||||||
// default to worst, then gradually improve
|
// default to worst, then gradually improve
|
||||||
let mut alpha = alpha.unwrap_or(EVAL_WORST);
|
let mut alpha = alpha.unwrap_or(EVAL_WORST);
|
||||||
// our best is their worst
|
// our best is their worst
|
||||||
@ -51,31 +117,36 @@ fn minmax(board: &mut Board, depth: usize, alpha: Option<EvalInt>, beta: Option<
|
|||||||
if depth == 0 {
|
if depth == 0 {
|
||||||
let eval = board.eval();
|
let eval = board.eval();
|
||||||
match board.turn {
|
match board.turn {
|
||||||
crate::Color::White => return eval,
|
crate::Color::White => return (None, SearchEval::Centipawns(eval)),
|
||||||
crate::Color::Black => return -eval,
|
crate::Color::Black => return (None, SearchEval::Centipawns(-eval)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mvs: Vec<_> = board.gen_moves().into_iter().collect();
|
let mvs: Vec<_> = board.gen_moves().into_iter().collect();
|
||||||
|
|
||||||
let mut abs_best = EVAL_WORST;
|
let mut abs_best = SearchEval::Centipawns(EVAL_WORST);
|
||||||
|
let mut best_move: Option<Move> = None;
|
||||||
|
|
||||||
if mvs.is_empty() {
|
if mvs.is_empty() {
|
||||||
if board.is_check(board.turn) {
|
if board.is_check(board.turn) {
|
||||||
return EVAL_WORST;
|
return (None, SearchEval::Checkmate(-1));
|
||||||
} else {
|
} else {
|
||||||
// stalemate
|
// stalemate
|
||||||
return 0;
|
return (None, SearchEval::Centipawns(0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for mv in mvs {
|
for mv in mvs {
|
||||||
let anti_mv = mv.make(board);
|
let anti_mv = mv.make(board);
|
||||||
let abs_score = -minmax(board, depth - 1, Some(-beta), Some(-alpha));
|
let (_best_continuation, score) = minmax(board, depth - 1, Some(-beta), Some(-alpha));
|
||||||
abs_best = max(abs_best, abs_score);
|
let abs_score = score.increment();
|
||||||
alpha = max(alpha, abs_best);
|
if abs_score >= abs_best {
|
||||||
|
abs_best = abs_score;
|
||||||
|
best_move = Some(mv);
|
||||||
|
}
|
||||||
|
alpha = max(alpha, abs_best.into());
|
||||||
anti_mv.unmake(board);
|
anti_mv.unmake(board);
|
||||||
if alpha >= beta {
|
if alpha >= beta {
|
||||||
// alpha-beta prune.
|
// alpha-beta prune.
|
||||||
//
|
//
|
||||||
// Beta represents the best eval that the other player can get in sibling branches
|
// Beta represents the best eval that the other player can get in sibling branches
|
||||||
@ -86,32 +157,15 @@ fn minmax(board: &mut Board, depth: usize, alpha: Option<EvalInt>, beta: Option<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
abs_best
|
(best_move, abs_best)
|
||||||
}
|
|
||||||
|
|
||||||
/// Find the best move for a position (internal interface).
|
|
||||||
fn search(board: &mut Board) -> Option<Move> {
|
|
||||||
const DEPTH: usize = 4;
|
|
||||||
let mvs: Vec<_> = board.gen_moves().into_iter().collect();
|
|
||||||
|
|
||||||
// absolute eval value
|
|
||||||
let mut best_eval = EVAL_WORST;
|
|
||||||
let mut best_mv: Option<Move> = None;
|
|
||||||
|
|
||||||
for mv in mvs {
|
|
||||||
let anti_mv = mv.make(board);
|
|
||||||
let abs_eval = -minmax(board, DEPTH, None, None);
|
|
||||||
if abs_eval >= best_eval {
|
|
||||||
best_eval = abs_eval;
|
|
||||||
best_mv = Some(mv);
|
|
||||||
}
|
|
||||||
anti_mv.unmake(board);
|
|
||||||
}
|
|
||||||
|
|
||||||
best_mv
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find the best move.
|
/// Find the best move.
|
||||||
pub fn best_move(board: &mut Board) -> Option<Move> {
|
pub fn best_move(board: &mut Board) -> Option<Move> {
|
||||||
search(board)
|
let (mv, eval) = minmax(board, 5, None, None);
|
||||||
|
match eval {
|
||||||
|
SearchEval::Checkmate(n) => println!("info score mate {}", n / 2),
|
||||||
|
SearchEval::Centipawns(eval) => println!("info score cp {eval}"),
|
||||||
|
}
|
||||||
|
mv
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user