diff --git a/src/bin/engine.rs b/src/bin/engine.rs index 1316d32..756907a 100644 --- a/src/bin/engine.rs +++ b/src/bin/engine.rs @@ -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 //! 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"), diff --git a/src/eval.rs b/src/eval.rs index e5978a9..d6549f4 100644 --- a/src/eval.rs +++ b/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 } } diff --git a/src/lib.rs b/src/lib.rs index e3b0e50..24c40b4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; } diff --git a/src/search.rs b/src/search.rs index e54d0c8..6c7ca33 100644 --- a/src/search.rs +++ b/src/search.rs @@ -14,7 +14,7 @@ Copyright © 2024 dogeystamp //! 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 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, beta: Option, -) -> (Option, SearchEval) { +) -> (Vec, 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 = None; + let mut best_continuation: Vec = 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, SearchEval) { + let (line, eval) = minmax(board, 5, None, None); + (line, eval) } /// Find the best move. pub fn best_move(board: &mut Board) -> Option { - 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() }