feat: some extra uci info about the best line, etc

This commit is contained in:
dogeystamp 2024-11-02 19:33:29 -04:00
parent b7b3c6c5b8
commit 96b4816f84
4 changed files with 71 additions and 25 deletions

View File

@ -1,7 +1,6 @@
/* /*
This file is part of chess_inator. 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 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. 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. //! Main UCI engine binary.
use chess_inator::eval::EvalInt;
use chess_inator::fen::FromFen; use chess_inator::fen::FromFen;
use chess_inator::movegen::{FromUCIAlgebraic, Move, ToUCIAlgebraic}; 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 chess_inator::Board;
use std::io; use std::io;
@ -88,7 +88,24 @@ fn cmd_position(mut tokens: std::str::SplitWhitespace<'_>) -> Board {
/// Play the game. /// Play the game.
fn cmd_go(mut _tokens: std::str::SplitWhitespace<'_>, board: &mut Board) { 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 { match chosen {
Some(mv) => println!("bestmove {}", mv.to_uci_algebraic()), Some(mv) => println!("bestmove {}", mv.to_uci_algebraic()),
None => println!("bestmove 0000"), None => println!("bestmove 0000"),

View File

@ -23,6 +23,8 @@ pub type EvalInt = i16;
pub trait Eval { pub trait Eval {
/// Evaluate a position and assign it a score. /// Evaluate a position and assign it a score.
///
/// Negative for Black advantage and positive for White.
fn eval(&self) -> EvalInt; fn eval(&self) -> EvalInt;
} }
@ -46,6 +48,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 {
/// Signed score.
pub score: EvalInt, pub score: EvalInt,
} }
@ -234,8 +237,21 @@ pub const PST_MIDGAME: Pst = Pst([
], 100), ], 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 { impl Eval for Board {
fn eval(&self) -> EvalInt { fn eval(&self) -> EvalInt {
let score_incremental = self.eval.midgame.score;
debug_assert_eq!(refresh_eval(self), score_incremental);
self.eval.midgame.score self.eval.midgame.score
} }
} }

View File

@ -585,6 +585,11 @@ impl Board {
false 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 /// Maximum amount of moves in the counter to parse before giving up
const MAX_MOVES: usize = 9_999; const MAX_MOVES: usize = 9_999;
} }

View File

@ -14,7 +14,7 @@ Copyright © 2024 dogeystamp <dogeystamp@disroot.org>
//! Game-tree search. //! Game-tree search.
use crate::eval::{Eval, EvalInt}; use crate::eval::{Eval, EvalInt};
use crate::movegen::{Move, MoveGen, ToUCIAlgebraic}; use crate::movegen::{Move, MoveGen};
use crate::Board; use crate::Board;
use std::cmp::max; use std::cmp::max;
@ -35,8 +35,8 @@ mod test_eval_int {
} }
/// Eval in the context of search. /// Eval in the context of search.
#[derive(PartialEq, Eq, Clone, Copy)] #[derive(PartialEq, Eq, Clone, Copy, Debug)]
enum SearchEval { pub enum SearchEval {
/// Mate in |n| - 1 half moves, negative for own mate. /// Mate in |n| - 1 half moves, negative for own mate.
Checkmate(i8), Checkmate(i8),
/// Centipawn score. /// Centipawn score.
@ -66,7 +66,7 @@ impl From<SearchEval> for EvalInt {
SearchEval::Checkmate(n) => { SearchEval::Checkmate(n) => {
debug_assert_ne!(n, 0); debug_assert_ne!(n, 0);
if n < 0 { if n < 0 {
EVAL_WORST + EvalInt::from(n) EVAL_WORST - EvalInt::from(n)
} else { } else {
EVAL_BEST - EvalInt::from(n) EVAL_BEST - EvalInt::from(n)
} }
@ -102,13 +102,13 @@ impl PartialOrd for SearchEval {
/// ///
/// # Returns /// # 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( fn minmax(
board: &mut Board, board: &mut Board,
depth: usize, depth: usize,
alpha: Option<EvalInt>, alpha: Option<EvalInt>,
beta: Option<EvalInt>, beta: Option<EvalInt>,
) -> (Option<Move>, SearchEval) { ) -> (Vec<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
@ -116,33 +116,35 @@ fn minmax(
if depth == 0 { if depth == 0 {
let eval = board.eval(); let eval = board.eval();
match board.turn { return (
crate::Color::White => return (None, SearchEval::Centipawns(eval)), Vec::new(),
crate::Color::Black => return (None, SearchEval::Centipawns(-eval)), SearchEval::Centipawns(eval * EvalInt::from(board.turn.sign())),
} );
} }
let mvs: Vec<_> = board.gen_moves().into_iter().collect(); let mvs: Vec<_> = board.gen_moves().into_iter().collect();
let mut abs_best = SearchEval::Centipawns(EVAL_WORST); let mut abs_best = SearchEval::Centipawns(EVAL_WORST);
let mut best_move: Option<Move> = None; let mut best_move: Option<Move> = None;
let mut best_continuation: Vec<Move> = Vec::new();
if mvs.is_empty() { if mvs.is_empty() {
if board.is_check(board.turn) { if board.is_check(board.turn) {
return (None, SearchEval::Checkmate(-1)); return (Vec::new(), SearchEval::Checkmate(-1));
} else { } else {
// stalemate // stalemate
return (None, SearchEval::Centipawns(0)); return (Vec::new(), SearchEval::Centipawns(0));
} }
} }
for mv in mvs { for mv in mvs {
let anti_mv = mv.make(board); 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(); let abs_score = score.increment();
if abs_score >= abs_best { if abs_score > abs_best {
abs_best = abs_score; abs_best = abs_score;
best_move = Some(mv); best_move = Some(mv);
best_continuation = continuation;
} }
alpha = max(alpha, abs_best.into()); alpha = max(alpha, abs_best.into());
anti_mv.unmake(board); anti_mv.unmake(board);
@ -150,22 +152,28 @@ fn minmax(
// 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
// (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. // for the other player, so they will never make the move that leads into this branch.
// Therefore, we stop evaluating this branch at all. // Therefore, we stop evaluating this branch at all.
break; 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. /// Find the best move.
pub fn best_move(board: &mut Board) -> Option<Move> { pub fn best_move(board: &mut Board) -> Option<Move> {
let (mv, eval) = minmax(board, 5, None, None); let (line, _eval) = best_line(board);
match eval { line.last().copied()
SearchEval::Checkmate(n) => println!("info score mate {}", n / 2),
SearchEval::Centipawns(eval) => println!("info score cp {eval}"),
}
mv
} }