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:
parent
c0ff3a9794
commit
d94db7708f
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in a new issue