feat: some extra uci info about the best line, etc
This commit is contained in:
parent
b7b3c6c5b8
commit
96b4816f84
@ -1,7 +1,6 @@
|
||||
/*
|
||||
|
||||
This file is part of chess_inator.
|
||||
|
||||
chess_inator is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
chess_inator is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
@ -13,9 +12,10 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
|
||||
//! Main UCI engine binary.
|
||||
|
||||
use chess_inator::eval::EvalInt;
|
||||
use chess_inator::fen::FromFen;
|
||||
use chess_inator::movegen::{FromUCIAlgebraic, Move, ToUCIAlgebraic};
|
||||
use chess_inator::search::best_move;
|
||||
use chess_inator::search::{best_line, SearchEval};
|
||||
use chess_inator::Board;
|
||||
use std::io;
|
||||
|
||||
@ -88,7 +88,24 @@ fn cmd_position(mut tokens: std::str::SplitWhitespace<'_>) -> Board {
|
||||
|
||||
/// Play the game.
|
||||
fn cmd_go(mut _tokens: std::str::SplitWhitespace<'_>, board: &mut Board) {
|
||||
let chosen = best_move(board);
|
||||
let (line, eval) = best_line(board);
|
||||
let chosen = line.last().copied();
|
||||
println!(
|
||||
"info pv{}",
|
||||
line.iter()
|
||||
.rev()
|
||||
.map(|mv| mv.to_uci_algebraic())
|
||||
.fold(String::new(), |a, b| a + " " + &b)
|
||||
);
|
||||
match eval {
|
||||
SearchEval::Checkmate(n) => println!("info score mate {}", n / 2),
|
||||
SearchEval::Centipawns(eval) => {
|
||||
println!(
|
||||
"info score cp {}",
|
||||
eval,
|
||||
)
|
||||
}
|
||||
}
|
||||
match chosen {
|
||||
Some(mv) => println!("bestmove {}", mv.to_uci_algebraic()),
|
||||
None => println!("bestmove 0000"),
|
||||
|
16
src/eval.rs
16
src/eval.rs
@ -23,6 +23,8 @@ pub type EvalInt = i16;
|
||||
|
||||
pub trait Eval {
|
||||
/// Evaluate a position and assign it a score.
|
||||
///
|
||||
/// Negative for Black advantage and positive for White.
|
||||
fn eval(&self) -> EvalInt;
|
||||
}
|
||||
|
||||
@ -46,6 +48,7 @@ pub(crate) mod eval_score {
|
||||
/// Score from a given perspective (e.g. midgame, endgame).
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Default, Debug)]
|
||||
pub struct EvalScore {
|
||||
/// Signed score.
|
||||
pub score: EvalInt,
|
||||
}
|
||||
|
||||
@ -234,8 +237,21 @@ pub const PST_MIDGAME: Pst = Pst([
|
||||
], 100),
|
||||
]);
|
||||
|
||||
/// Calculate evaluation without incremental updates.
|
||||
pub(crate) fn refresh_eval(board: &Board) -> EvalInt {
|
||||
let mut eval: EvalInt = 0;
|
||||
for sq in Board::squares() {
|
||||
if let Some(pc) = board.get_piece(sq) {
|
||||
eval += PST_MIDGAME[pc.pc][pc.col][sq] * EvalInt::from(pc.col.sign());
|
||||
}
|
||||
}
|
||||
eval
|
||||
}
|
||||
|
||||
impl Eval for Board {
|
||||
fn eval(&self) -> EvalInt {
|
||||
let score_incremental = self.eval.midgame.score;
|
||||
debug_assert_eq!(refresh_eval(self), score_incremental);
|
||||
self.eval.midgame.score
|
||||
}
|
||||
}
|
||||
|
@ -585,6 +585,11 @@ impl Board {
|
||||
false
|
||||
}
|
||||
|
||||
/// Get the current player to move.
|
||||
pub fn get_turn(&self) -> Color {
|
||||
self.turn
|
||||
}
|
||||
|
||||
/// Maximum amount of moves in the counter to parse before giving up
|
||||
const MAX_MOVES: usize = 9_999;
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
|
||||
//! Game-tree search.
|
||||
|
||||
use crate::eval::{Eval, EvalInt};
|
||||
use crate::movegen::{Move, MoveGen, ToUCIAlgebraic};
|
||||
use crate::movegen::{Move, MoveGen};
|
||||
use crate::Board;
|
||||
use std::cmp::max;
|
||||
|
||||
@ -35,8 +35,8 @@ mod test_eval_int {
|
||||
}
|
||||
|
||||
/// Eval in the context of search.
|
||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
||||
enum SearchEval {
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
pub enum SearchEval {
|
||||
/// Mate in |n| - 1 half moves, negative for own mate.
|
||||
Checkmate(i8),
|
||||
/// Centipawn score.
|
||||
@ -66,7 +66,7 @@ impl From<SearchEval> for EvalInt {
|
||||
SearchEval::Checkmate(n) => {
|
||||
debug_assert_ne!(n, 0);
|
||||
if n < 0 {
|
||||
EVAL_WORST + EvalInt::from(n)
|
||||
EVAL_WORST - EvalInt::from(n)
|
||||
} else {
|
||||
EVAL_BEST - EvalInt::from(n)
|
||||
}
|
||||
@ -102,13 +102,13 @@ impl PartialOrd for SearchEval {
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// The best move, and its corresponding absolute eval for the current player.
|
||||
/// The best line (in reverse move order), 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) {
|
||||
) -> (Vec<Move>, SearchEval) {
|
||||
// default to worst, then gradually improve
|
||||
let mut alpha = alpha.unwrap_or(EVAL_WORST);
|
||||
// our best is their worst
|
||||
@ -116,33 +116,35 @@ fn minmax(
|
||||
|
||||
if depth == 0 {
|
||||
let eval = board.eval();
|
||||
match board.turn {
|
||||
crate::Color::White => return (None, SearchEval::Centipawns(eval)),
|
||||
crate::Color::Black => return (None, SearchEval::Centipawns(-eval)),
|
||||
}
|
||||
return (
|
||||
Vec::new(),
|
||||
SearchEval::Centipawns(eval * EvalInt::from(board.turn.sign())),
|
||||
);
|
||||
}
|
||||
|
||||
let mvs: Vec<_> = board.gen_moves().into_iter().collect();
|
||||
|
||||
let mut abs_best = SearchEval::Centipawns(EVAL_WORST);
|
||||
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) {
|
||||
return (None, SearchEval::Checkmate(-1));
|
||||
return (Vec::new(), SearchEval::Checkmate(-1));
|
||||
} else {
|
||||
// stalemate
|
||||
return (None, SearchEval::Centipawns(0));
|
||||
return (Vec::new(), SearchEval::Centipawns(0));
|
||||
}
|
||||
}
|
||||
|
||||
for mv in mvs {
|
||||
let anti_mv = mv.make(board);
|
||||
let (_best_continuation, score) = minmax(board, depth - 1, Some(-beta), Some(-alpha));
|
||||
let (continuation, score) = minmax(board, depth - 1, Some(-beta), Some(-alpha));
|
||||
let abs_score = score.increment();
|
||||
if abs_score >= abs_best {
|
||||
if abs_score > abs_best {
|
||||
abs_best = abs_score;
|
||||
best_move = Some(mv);
|
||||
best_continuation = continuation;
|
||||
}
|
||||
alpha = max(alpha, abs_best.into());
|
||||
anti_mv.unmake(board);
|
||||
@ -150,22 +152,28 @@ fn minmax(
|
||||
// alpha-beta prune.
|
||||
//
|
||||
// Beta represents the best eval that the other player can get in sibling branches
|
||||
// (different moves in the parent node). Alpha >= beta means the eval here is _worse_
|
||||
// (different moves in the parent node). Alpha > beta means the eval here is _worse_
|
||||
// for the other player, so they will never make the move that leads into this branch.
|
||||
// Therefore, we stop evaluating this branch at all.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(best_move, abs_best)
|
||||
if let Some(mv) = best_move {
|
||||
best_continuation.push(mv);
|
||||
}
|
||||
|
||||
(best_continuation, abs_best)
|
||||
}
|
||||
|
||||
/// Find the best line (in reverse order) and its evaluation.
|
||||
pub fn best_line(board: &mut Board) -> (Vec<Move>, SearchEval) {
|
||||
let (line, eval) = minmax(board, 5, None, None);
|
||||
(line, eval)
|
||||
}
|
||||
|
||||
/// Find the best move.
|
||||
pub fn best_move(board: &mut Board) -> Option<Move> {
|
||||
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
|
||||
let (line, _eval) = best_line(board);
|
||||
line.last().copied()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user