analysis: consider time constraints

Basic math for the limited time. Search is cut without caution, and as
we are using a depth-first search it might lead to awful moves, but at
least Vatu does not lose on time in 100% of bullets.
This commit is contained in:
dece 2020-06-18 22:23:43 +02:00
parent c0ff3a9794
commit d94db7708f
2 changed files with 76 additions and 16 deletions

View file

@ -3,6 +3,7 @@
use std::sync::{Arc, atomic, mpsc}; use std::sync::{Arc, atomic, mpsc};
use std::time::Instant; use std::time::Instant;
use crate::board;
use crate::engine; use crate::engine;
use crate::movement::Move; use crate::movement::Move;
use crate::node::Node; use crate::node::Node;
@ -14,13 +15,30 @@ const MIN_F32: f32 = std::f32::NEG_INFINITY;
const MAX_F32: f32 = std::f32::INFINITY; const MAX_F32: f32 = std::f32::INFINITY;
/// Analysis worker. /// Analysis worker.
///
/// Parameters specifying when to stop an analysis (e.g. `max_depth`
/// and `time_limit`) can be used together without issues and the
/// worker will try to stop as soon as the first limit is reached.
pub struct Analyzer { pub struct Analyzer {
/// Enable some debug logs.
pub debug: bool, pub debug: bool,
/// Root node for this analysis.
node: Node, node: Node,
/// Sender for engine commands.
engine_tx: mpsc::Sender<engine::Cmd>, engine_tx: mpsc::Sender<engine::Cmd>,
/// Stop working if flag is unset.
working: Option<Arc<atomic::AtomicBool>>,
/// Max depth to reach in the next analysis.
max_depth: u32, max_depth: u32,
nps_time: Instant, /// Time limit for the next analysis.
time_limit: i32,
/// Instant when the analysis began.
start_time: Option<Instant>,
/// Instant of the last "per second" stats calculation.
current_per_second_timer: Option<Instant>,
/// Nodes analyzed in this analysis.
num_nodes: u64, num_nodes: u64,
/// Node analyzed since the last NPS stat.
num_nodes_in_second: u64, num_nodes_in_second: u64,
} }
@ -49,8 +67,11 @@ impl Analyzer {
debug: false, debug: false,
node, node,
engine_tx, engine_tx,
working: None,
max_depth: 1, max_depth: 1,
nps_time: Instant::now(), time_limit: 0,
start_time: None,
current_per_second_timer: None,
num_nodes: 0, num_nodes: 0,
num_nodes_in_second: 0, num_nodes_in_second: 0,
} }
@ -75,20 +96,21 @@ impl Analyzer {
/// - `working`: flag telling whether to keep working or to stop. /// - `working`: flag telling whether to keep working or to stop.
pub fn analyze( pub fn analyze(
&mut self, &mut self,
_args: &AnalysisParams, args: &AnalysisParams,
working: Arc<atomic::AtomicBool>, working: Arc<atomic::AtomicBool>,
) { ) {
if !working.load(atomic::Ordering::Relaxed) { self.working = Some(working);
return; self.set_limits(args);
}
if self.debug { if self.debug {
self.log(format!("Analyzing node:\n{}", &self.node)); self.log(format!("Analyzing node:\n{}", &self.node));
let moves = self.node.get_player_moves(true); let moves = self.node.get_player_moves(true);
self.log(format!("Legal moves: {}", notation::move_list_to_string(&moves))); self.log(format!("Legal moves: {}", notation::move_list_to_string(&moves)));
self.log(format!("Move time: {}", self.time_limit));
} }
self.nps_time = Instant::now(); self.start_time = Some(Instant::now());
self.max_depth = 4; self.current_per_second_timer = Some(Instant::now());
let (max_score, best_move) = self.negamax(&self.node.clone(), MIN_F32, MAX_F32, 0); let (max_score, best_move) = self.negamax(&self.node.clone(), MIN_F32, MAX_F32, 0);
if best_move.is_some() { if best_move.is_some() {
@ -107,6 +129,37 @@ impl Analyzer {
} }
} }
/// Set search limits.
fn set_limits(&mut self, args: &AnalysisParams) {
self.max_depth = 4;
self.time_limit = if args.move_time != -1 {
args.move_time
} else {
let (time, inc) = if board::is_white(self.node.game_state.color) {
(args.white_time, args.white_inc)
} else {
(args.black_time, args.black_inc)
};
// If more than 2 minutes is left, use a 1m time limit.
if time > 2*60*1000 {
60*1000
}
// Else use 1/4 of the remaining time (plus the increment).
else if time > 0 {
(time / 4) + inc
}
// Or if there is not remaining time, do not use a time limit.
else {
i32::MAX
}
};
}
/// Return best score and associated move for this node.
///
/// `depth` is the current search depth. `alpha` and `beta` are
/// used for alpha-beta search tree pruning, where `alpha` is the
/// lower score bound and `beta` the upper bound.
fn negamax( fn negamax(
&mut self, &mut self,
node: &Node, node: &Node,
@ -118,21 +171,21 @@ impl Analyzer {
self.num_nodes += 1; self.num_nodes += 1;
self.num_nodes_in_second += 1; self.num_nodes_in_second += 1;
// If we reached max depth, evaluate the node and stop searching. // If we should stop searching, evaluate the node and stop.
if depth == self.max_depth { if self.should_stop_search(depth) {
let stats = node.compute_stats(); let stats = node.compute_stats();
let ev = evaluate(&stats); let ev = evaluate(&stats);
return (ev, None) return (ev, None)
} }
// Here's a good time to get some stats! // Here's a good time to get some stats!
if self.nps_time.elapsed().as_millis() >= 1000 { if self.current_per_second_timer.unwrap().elapsed().as_millis() >= 1000 {
self.report_info(vec![ self.report_info(vec![
AnalysisInfo::Nodes(self.num_nodes), AnalysisInfo::Nodes(self.num_nodes),
AnalysisInfo::Nps(self.num_nodes_in_second), AnalysisInfo::Nps(self.num_nodes_in_second),
]); ]);
self.num_nodes_in_second = 0; self.num_nodes_in_second = 0;
self.nps_time = Instant::now(); self.current_per_second_timer = Some(Instant::now());
} }
// Get negamax for playable moves. // Get negamax for playable moves.
@ -158,6 +211,15 @@ impl Analyzer {
} }
(best_score, best_move) (best_score, best_move)
} }
/// Return true if some parameter requires to stop searching.
///
/// Check for max node depth, time limit and engine stop flag.
fn should_stop_search(&self, depth: u32) -> bool {
!self.working.as_ref().unwrap().load(atomic::Ordering::Relaxed)
|| depth == self.max_depth
|| self.start_time.unwrap().elapsed().as_millis() >= self.time_limit as u128
}
} }
/// Compute a score for white/black board stats. /// Compute a score for white/black board stats.

View file

@ -66,10 +66,8 @@ impl fmt::Display for Node {
impl PartialEq for Node { impl PartialEq for Node {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
( self.board.iter().zip(other.board.iter()).all(|(a, b)| a == b)
self.board.iter().zip(other.board.iter()).all(|(a, b)| a == b) && && self.game_state == other.game_state
self.game_state == other.game_state
)
} }
} }