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.
|
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"),
|
||||||
|
16
src/eval.rs
16
src/eval.rs
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user