From d94db7708f5b91cc7b861cbd2bf5e8ee3d1d238f Mon Sep 17 00:00:00 2001 From: dece Date: Thu, 18 Jun 2020 22:23:43 +0200 Subject: [PATCH] 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. --- src/analysis.rs | 86 ++++++++++++++++++++++++++++++++++++++++++------- src/node.rs | 6 ++-- 2 files changed, 76 insertions(+), 16 deletions(-) diff --git a/src/analysis.rs b/src/analysis.rs index d6ff859..8c878da 100644 --- a/src/analysis.rs +++ b/src/analysis.rs @@ -3,6 +3,7 @@ use std::sync::{Arc, atomic, mpsc}; use std::time::Instant; +use crate::board; use crate::engine; use crate::movement::Move; use crate::node::Node; @@ -14,13 +15,30 @@ const MIN_F32: f32 = std::f32::NEG_INFINITY; const MAX_F32: f32 = std::f32::INFINITY; /// 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 { + /// Enable some debug logs. pub debug: bool, + /// Root node for this analysis. node: Node, + /// Sender for engine commands. engine_tx: mpsc::Sender, + /// Stop working if flag is unset. + working: Option>, + /// Max depth to reach in the next analysis. max_depth: u32, - nps_time: Instant, + /// Time limit for the next analysis. + time_limit: i32, + /// Instant when the analysis began. + start_time: Option, + /// Instant of the last "per second" stats calculation. + current_per_second_timer: Option, + /// Nodes analyzed in this analysis. num_nodes: u64, + /// Node analyzed since the last NPS stat. num_nodes_in_second: u64, } @@ -49,8 +67,11 @@ impl Analyzer { debug: false, node, engine_tx, + working: None, max_depth: 1, - nps_time: Instant::now(), + time_limit: 0, + start_time: None, + current_per_second_timer: None, num_nodes: 0, num_nodes_in_second: 0, } @@ -75,20 +96,21 @@ impl Analyzer { /// - `working`: flag telling whether to keep working or to stop. pub fn analyze( &mut self, - _args: &AnalysisParams, + args: &AnalysisParams, working: Arc, ) { - if !working.load(atomic::Ordering::Relaxed) { - return; - } + self.working = Some(working); + self.set_limits(args); + if self.debug { self.log(format!("Analyzing node:\n{}", &self.node)); let moves = self.node.get_player_moves(true); 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.max_depth = 4; + self.start_time = Some(Instant::now()); + self.current_per_second_timer = Some(Instant::now()); let (max_score, best_move) = self.negamax(&self.node.clone(), MIN_F32, MAX_F32, 0); 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( &mut self, node: &Node, @@ -118,21 +171,21 @@ impl Analyzer { 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 { + // If we should stop searching, evaluate the node and stop. + if self.should_stop_search(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 { + if self.current_per_second_timer.unwrap().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(); + self.current_per_second_timer = Some(Instant::now()); } // Get negamax for playable moves. @@ -158,6 +211,15 @@ impl Analyzer { } (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. diff --git a/src/node.rs b/src/node.rs index eb4f0e9..2c95367 100644 --- a/src/node.rs +++ b/src/node.rs @@ -66,10 +66,8 @@ impl fmt::Display for Node { impl PartialEq for Node { fn eq(&self, other: &Self) -> bool { - ( - self.board.iter().zip(other.board.iter()).all(|(a, b)| a == b) && - self.game_state == other.game_state - ) + self.board.iter().zip(other.board.iter()).all(|(a, b)| a == b) + && self.game_state == other.game_state } }