analysis: provide "nodes" and "nps" UCI info

This commit is contained in:
dece 2020-06-15 02:33:08 +02:00
parent 685039d98a
commit a426e2f777
3 changed files with 80 additions and 29 deletions

View file

@ -1,8 +1,8 @@
//! Analysis functions.
use std::sync::{Arc, atomic, mpsc};
use std::time::Instant;
use crate::board;
use crate::engine;
use crate::movement::Move;
use crate::node::Node;
@ -19,6 +19,9 @@ pub struct Analyzer {
node: Node,
engine_tx: mpsc::Sender<engine::Cmd>,
max_depth: u32,
nps_time: Instant,
num_nodes: u64,
num_nodes_in_second: u64,
}
/// Analysis parameters.
@ -31,16 +34,40 @@ pub struct AnalysisParams {
pub black_inc: i32,
}
/// Analysis info to report.
#[derive(Debug, Clone)]
pub enum AnalysisInfo {
Nodes(u64),
Nps(u64),
CurrentMove(Move),
}
impl Analyzer {
/// Create a new worker to analyze from `node`.
pub fn new(node: Node, engine_tx: mpsc::Sender<engine::Cmd>) -> Analyzer {
Analyzer { debug: false, node, engine_tx, max_depth: 1 }
Analyzer {
debug: false,
node,
engine_tx,
max_depth: 1,
nps_time: Instant::now(),
num_nodes: 0,
num_nodes_in_second: 0,
}
}
fn log(&self, message: String) {
self.engine_tx.send(engine::Cmd::Log(message)).unwrap();
}
fn report_info(&self, infos: Vec<AnalysisInfo>) {
self.engine_tx.send(engine::Cmd::WorkerInfo(infos)).unwrap();
}
fn report_best_move(&self, m: Option<Move>) {
self.engine_tx.send(engine::Cmd::WorkerBestMove(m)).unwrap();
}
/// Analyse best moves for the node.
///
/// - `args`: parameters provided for this analysis.
@ -60,8 +87,9 @@ impl Analyzer {
self.log(format!("Legal moves: {}", notation::move_list_to_string(&moves)));
}
self.max_depth = 3;
let (max_score, best_move) = self.negamax(&self.node, MIN_F32, MAX_F32, 0);
self.nps_time = Instant::now();
self.max_depth = 4;
let (max_score, best_move) = self.negamax(&self.node.clone(), MIN_F32, MAX_F32, 0);
if best_move.is_some() {
let log_str = format!(
@ -69,34 +97,50 @@ impl Analyzer {
notation::move_to_string(&best_move.unwrap()), max_score
);
self.log(log_str);
self.engine_tx.send(engine::Cmd::TmpBestMove(best_move)).unwrap();
self.report_best_move(best_move);
} else {
// If no best move could be found, checkmate is unavoidable; send the first legal move.
self.log("Checkmate is unavoidable.".to_string());
let moves = rules::get_player_moves(&self.node.board, &self.node.game_state, true);
let m = if moves.len() > 0 { Some(moves[0]) } else { None };
self.engine_tx.send(engine::Cmd::TmpBestMove(m)).unwrap();
self.report_best_move(m);
}
}
fn negamax(
&self,
&mut self,
node: &Node,
alpha: f32,
beta: f32,
depth: u32,
) -> (f32, Option<Move>) {
// Increment number of nodes for stats.
self.num_nodes += 1;
self.num_nodes_in_second += 1;
// If we reached max depth, evaluate the node and stop searching.
if depth == self.max_depth {
let stats = node.compute_stats();
let ev = evaluate(&stats);
return (ev, None)
}
// Here's a good time to get some stats!
if self.nps_time.elapsed().as_millis() >= 1000 {
self.report_info(vec![
AnalysisInfo::Nodes(self.num_nodes),
AnalysisInfo::Nps(self.num_nodes_in_second),
]);
self.num_nodes_in_second = 0;
self.nps_time = Instant::now();
}
// Get negamax for playable moves.
let moves = node.get_player_moves(true);
let mut alpha = alpha;
let mut best_score = MIN_F32;
let mut best_move = None;
for m in moves {
self.log(format!("negamax: depth {} move {}...", depth, notation::move_to_string(&m)));
let mut sub_node = node.clone();
sub_node.apply_move(&m);
let result = self.negamax(&sub_node, -beta, -alpha, depth + 1);
@ -104,14 +148,11 @@ impl Analyzer {
if score > best_score {
best_score = score;
best_move = Some(m);
self.log(format!("negamax: depth {} new best score {}.", depth, best_score));
}
if best_score > alpha {
alpha = best_score;
self.log(format!("negamax: depth {} new alpha {}.", depth, alpha));
}
if alpha >= beta {
self.log(format!("negamax: depth {} alpha above beta {}, cut.", depth, beta));
break
}
}

View file

@ -47,30 +47,33 @@ enum Mode {
#[derive(Debug)]
pub enum Cmd {
// Commands that can be received by the engine.
UciChannel(mpsc::Sender<Cmd>), // Provide a sender to UCI to start receiving commands.
UciDebug(bool), // UCI "debug" command.
UciPosition(Vec<uci::PositionArgs>), // UCI "position" command.
UciGo(Vec<uci::GoArgs>), // UCI "go" command.
Stop, // Stop working ASAP.
TmpBestMove(Option<Move>), // Send best move found by analysis worker (TEMPORARY).
WorkerInfo(Vec<Info>), // Informations from a worker.
/// Provide a sender to UCI to start receiving commands.
UciChannel(mpsc::Sender<Cmd>),
/// UCI "debug" command.
UciDebug(bool),
/// UCI "position" command.
UciPosition(Vec<uci::PositionArgs>),
/// UCI "go" command.
UciGo(Vec<uci::GoArgs>),
/// Stop working ASAP.
Stop,
/// Informations from a worker.
WorkerInfo(Vec<analysis::AnalysisInfo>),
/// Send best move found by analysis worker.
WorkerBestMove(Option<Move>),
// Commands that can be sent by the engine.
/// Ask for a string to be logged or printed.
///
/// Note that workers can send this command to engine, expecting
/// the message to be forwarded to whatever can log.
Log(String),
/// Report ongoing analysis information.
Info(Vec<analysis::AnalysisInfo>),
/// Report found best move.
BestMove(Option<Move>),
/// Report ongoing analysis information.
Info(Vec<Info>),
}
/// Information to be transmitted back to whatever is listening.
#[derive(Debug, Clone)]
pub enum Info {
CurrentMove(Move),
}
/// General engine implementation.
@ -120,7 +123,7 @@ impl Engine {
// Workers commands.
Cmd::Log(s) => self.reply(Cmd::Log(s.to_string())),
Cmd::WorkerInfo(infos) => self.reply(Cmd::Info(infos.to_vec())),
Cmd::TmpBestMove(m) => self.reply(Cmd::BestMove(*m)),
Cmd::WorkerBestMove(m) => self.reply(Cmd::BestMove(*m)),
_ => eprintln!("Not an engine input command: {:?}", cmd),
}
}

View file

@ -5,6 +5,7 @@ use std::io::{self, Write};
use std::sync::mpsc;
use std::thread;
use crate::analysis::AnalysisInfo;
use crate::engine;
use crate::movement::Move;
use crate::notation;
@ -259,11 +260,17 @@ impl Uci {
}
/// Send engine analysis information.
fn send_infos(&mut self, infos: &Vec<engine::Info>) {
fn send_infos(&mut self, infos: &Vec<AnalysisInfo>) {
let mut s = "info".to_string();
for i in infos {
match i {
engine::Info::CurrentMove(m) => {
AnalysisInfo::Nodes(n) => {
s.push_str(&format!(" nodes {}", n));
}
AnalysisInfo::Nps(n) => {
s.push_str(&format!(" nps {}", n));
}
AnalysisInfo::CurrentMove(m) => {
s.push_str(&format!(" currmove {}", notation::move_to_string(m)));
}
}