use super::{LimitsType, TimeOptions, TimePoint};
use crate::time::Instant;
use crate::types::Color;
use log::debug;
use rand::Rng;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
const DEFAULT_MINIMUM_THINKING_TIME: TimePoint = 2000;
const DEFAULT_NETWORK_DELAY: TimePoint = 120;
const DEFAULT_NETWORK_DELAY2: TimePoint = 1120;
const DEFAULT_SLOW_MOVER: i32 = 100;
const MIN_MINIMUM_THINKING_TIME: TimePoint = 1000;
pub const DEFAULT_MAX_MOVES_TO_DRAW: i32 = 100000;
const SINGLE_MOVE_TIME_LIMIT: TimePoint = 500;
const BEST_MOVE_INSTABILITY_BASE: f64 = 0.9929;
const BEST_MOVE_INSTABILITY_FACTOR: f64 = 1.8519;
pub fn calculate_move_horizon(time_forfeit: bool, ply: i32) -> i32 {
const MOVE_HORIZON: i32 = 160;
if time_forfeit {
MOVE_HORIZON + 40 - ply.min(40)
} else {
MOVE_HORIZON + 20 - ply.min(80)
}
}
pub fn calculate_best_move_instability(tot_best_move_changes: f64, thread_count: usize) -> f64 {
BEST_MOVE_INSTABILITY_BASE
+ BEST_MOVE_INSTABILITY_FACTOR * tot_best_move_changes / thread_count.max(1) as f64
}
#[inline]
pub fn calculate_falling_eval(best_prev_avg: i32, iter_value: i32, best_value: i32) -> f64 {
let delta_avg = (best_prev_avg - best_value) as f64;
let delta_iter = (iter_value - best_value) as f64;
let eval = (11.396 + 2.035 * delta_avg + 0.968 * delta_iter) / 100.0;
eval.clamp(0.5786, 1.6752)
}
#[inline]
pub fn calculate_time_reduction(completed_depth: i32, last_best_move_depth: i32) -> f64 {
let k = 0.527;
let center = last_best_move_depth as f64 + 11.0;
0.8 + 0.84 / (1.077 + (-k * (completed_depth as f64 - center)).exp())
}
#[inline]
pub fn normalize_nodes_effort(effort: f64, nodes_total: u64) -> f64 {
effort * 100000.0 / nodes_total.max(1) as f64
}
pub struct TimeManagement {
start_time: Instant,
optimum_time: TimePoint,
maximum_time: TimePoint,
minimum_time: TimePoint,
search_end: TimePoint,
ponderhit_time: Instant,
is_final_push: bool,
minimum_thinking_time: TimePoint,
network_delay: TimePoint,
network_delay2: TimePoint,
slow_mover: i32,
remain_time: TimePoint,
stop: Arc<AtomicBool>,
ponderhit: Arc<AtomicBool>,
single_move_limit: bool,
previous_time_reduction: f64,
usi_ponder: bool,
stochastic_ponder: bool,
stop_on_ponderhit: bool,
is_pondering: bool,
last_stop_threshold: Option<TimePoint>,
}
impl TimeManagement {
pub fn new(stop: Arc<AtomicBool>, ponderhit: Arc<AtomicBool>) -> Self {
let now = Instant::now();
Self {
start_time: now,
optimum_time: 0,
maximum_time: 0,
minimum_time: 0,
search_end: 0,
ponderhit_time: now,
is_final_push: false,
minimum_thinking_time: DEFAULT_MINIMUM_THINKING_TIME,
network_delay: DEFAULT_NETWORK_DELAY,
network_delay2: DEFAULT_NETWORK_DELAY2,
slow_mover: DEFAULT_SLOW_MOVER,
remain_time: TimePoint::MAX / 2,
stop,
ponderhit,
single_move_limit: false,
previous_time_reduction: 0.85,
usi_ponder: false,
stochastic_ponder: false,
stop_on_ponderhit: false,
is_pondering: false,
last_stop_threshold: None,
}
}
pub fn set_options(&mut self, opts: &TimeOptions) {
self.network_delay = opts.network_delay.max(0);
self.network_delay2 = opts.network_delay2.max(0);
self.minimum_thinking_time = opts.minimum_thinking_time.max(MIN_MINIMUM_THINKING_TIME);
self.slow_mover = opts.slow_mover.clamp(1, 1000);
self.usi_ponder = opts.usi_ponder;
self.stochastic_ponder = opts.stochastic_ponder;
}
pub fn set_previous_time_reduction(&mut self, value: f64) {
self.previous_time_reduction = value;
}
#[cfg(test)]
pub fn previous_time_reduction_mut(&mut self) -> &mut f64 {
&mut self.previous_time_reduction
}
pub fn previous_time_reduction(&self) -> f64 {
self.previous_time_reduction
}
pub fn is_final_push(&self) -> bool {
self.is_final_push
}
#[cfg(test)]
pub fn remain_time(&self) -> TimePoint {
self.remain_time
}
pub fn round_up(&self, t0: TimePoint) -> TimePoint {
let mut t = ((t0 + 999) / 1000 * 1000).max(self.minimum_thinking_time);
t = t.saturating_sub(self.network_delay);
if t < t0 {
t += 1000;
}
t = t.min(self.remain_time);
t.max(0)
}
pub fn init(&mut self, limits: &LimitsType, us: Color, ply: i32, max_moves_to_draw: i32) {
self.start_time = limits.start_time.unwrap_or_else(Instant::now);
self.ponderhit_time = self.start_time;
self.search_end = 0;
self.is_final_push = false;
self.is_pondering = limits.ponder;
self.ponderhit.store(false, Ordering::Relaxed);
self.single_move_limit = false;
self.stop_on_ponderhit = false;
self.last_stop_threshold = None;
if limits.has_movetime() {
let movetime = limits.movetime;
self.remain_time = movetime;
self.optimum_time = movetime;
self.maximum_time = movetime;
self.minimum_time = movetime;
self.search_end = movetime;
self.last_stop_threshold = Some(movetime);
return;
}
if !limits.use_time_management() {
self.optimum_time = TimePoint::MAX / 2;
self.maximum_time = TimePoint::MAX / 2;
self.remain_time = TimePoint::MAX / 2;
self.minimum_time = 0;
return;
}
let time_left = limits.time_left(us);
let increment = limits.increment(us);
let byoyomi = limits.byoyomi_time(us);
let is_byoyomi_mode =
byoyomi > 0 && increment == 0 && time_left < (byoyomi as f64 * 1.2) as TimePoint;
self.remain_time = if is_byoyomi_mode {
(time_left + byoyomi - self.network_delay).max(100)
} else {
(time_left + increment + byoyomi - self.network_delay2).max(100)
};
if limits.rtime > 0 {
let mut r = limits.rtime;
if ply > 0 {
let max_rand = (r as f64 * 0.5).min(r as f64 * 10.0 / ply as f64);
if max_rand > 0.0 {
let mut rng = rand::rng();
let extra = rng.random_range(0..=max_rand as TimePoint);
r = r.saturating_add(extra);
}
}
self.remain_time = r;
self.minimum_time = r;
self.optimum_time = r;
self.maximum_time = r;
self.search_end = r;
self.last_stop_threshold = Some(r);
return;
}
let max_moves = if max_moves_to_draw > 0 {
max_moves_to_draw
} else {
DEFAULT_MAX_MOVES_TO_DRAW
};
let time_forfeit = increment == 0 && byoyomi == 0;
let move_horizon = calculate_move_horizon(time_forfeit, ply);
let mtg = (max_moves - ply + 2).min(move_horizon) / 2;
if mtg <= 0 {
self.minimum_time = 500;
self.optimum_time = 500;
self.maximum_time = 500;
return;
}
if mtg == 1 {
self.minimum_time = self.remain_time;
self.optimum_time = self.remain_time;
self.maximum_time = self.remain_time;
return;
}
self.minimum_time = (self.minimum_thinking_time - self.network_delay).max(1000);
self.optimum_time = self.remain_time;
self.maximum_time = self.remain_time;
let mtg_i64 = mtg as TimePoint;
let mut remain_estimate = time_left
.saturating_add(increment.saturating_mul(mtg_i64))
.saturating_add(byoyomi.saturating_mul(mtg_i64));
remain_estimate = remain_estimate.saturating_sub((mtg_i64 + 1) * 1000);
if remain_estimate < 0 {
remain_estimate = 0;
}
let t1 = self.minimum_time + remain_estimate / mtg_i64;
let mut max_ratio: f64 = 5.0;
if time_forfeit {
let ratio = (time_left as f64) / (60.0 * 1000.0);
max_ratio = max_ratio.min(ratio.max(1.0));
}
let mut t2 =
self.minimum_time + (remain_estimate as f64 * max_ratio / mtg_i64 as f64) as TimePoint;
let max_cap = (remain_estimate as f64 * 0.3) as TimePoint;
t2 = t2.min(max_cap);
self.optimum_time = t1.min(self.optimum_time);
self.maximum_time = t2.min(self.maximum_time);
self.optimum_time = self.optimum_time * self.slow_mover as i64 / 100;
if self.usi_ponder && !self.stochastic_ponder {
self.optimum_time += self.optimum_time / 4;
}
self.is_final_push = false;
if is_byoyomi_mode {
self.minimum_time = byoyomi + time_left;
self.optimum_time = byoyomi + time_left;
self.maximum_time = byoyomi + time_left;
self.is_final_push = true;
}
self.minimum_time = self.round_up(self.minimum_time);
self.optimum_time = self.optimum_time.min(self.remain_time).max(1);
self.maximum_time = self.round_up(self.maximum_time);
if self.optimum_time < self.minimum_time {
self.optimum_time = self.minimum_time;
}
if self.maximum_time < self.optimum_time {
self.maximum_time = self.optimum_time;
}
}
pub fn init_with_root_moves_count(
&mut self,
limits: &LimitsType,
us: Color,
ply: i32,
max_moves_to_draw: i32,
root_moves_count: usize,
) {
self.init(limits, us, ply, max_moves_to_draw);
if root_moves_count == 1 {
self.apply_single_move_limit();
self.single_move_limit = true;
} else {
self.single_move_limit = false;
}
}
pub fn apply_single_move_limit(&mut self) {
self.single_move_limit = true;
}
pub fn apply_best_move_instability(&mut self, tot_best_move_changes: f64, thread_count: usize) {
let _ = self.compute_time_factor(1.0, 1.0, tot_best_move_changes, thread_count);
}
pub fn compute_time_factor(
&mut self,
falling_eval: f64,
time_reduction: f64,
tot_best_move_changes: f64,
thread_count: usize,
) -> f64 {
if self.is_final_push {
return 1.0;
}
let instability = calculate_best_move_instability(tot_best_move_changes, thread_count);
let reduction =
(1.4540 + self.previous_time_reduction) / (2.1593 * time_reduction.max(0.0001));
self.previous_time_reduction = time_reduction;
falling_eval * reduction * instability
}
pub fn total_time_for_iteration(
&mut self,
falling_eval: f64,
time_reduction: f64,
tot_best_move_changes: f64,
thread_count: usize,
) -> f64 {
let factor = self.compute_time_factor(
falling_eval,
time_reduction,
tot_best_move_changes,
thread_count,
);
self.optimum_time as f64 * factor
}
pub fn apply_iteration_timing(
&mut self,
elapsed: TimePoint,
total_time: f64,
nodes_effort: f64,
completed_depth: i32,
) {
let is_pondering = self.is_pondering;
let effective_elapsed = self.effective_elapsed(elapsed);
if completed_depth >= 10
&& nodes_effort >= 97056.0
&& (effective_elapsed as f64) > total_time * 0.6540
&& !is_pondering
{
self.set_search_end(elapsed);
}
let mut stop_threshold =
(total_time.min(self.maximum_time as f64).ceil() as TimePoint).max(0);
if self.single_move_limit {
stop_threshold = stop_threshold.min(SINGLE_MOVE_TIME_LIMIT);
}
self.last_stop_threshold = Some(stop_threshold);
if (effective_elapsed as f64) > total_time.min(self.maximum_time as f64) {
if is_pondering {
self.stop_on_ponderhit = true;
} else {
self.set_search_end(elapsed);
}
}
debug!(
target: "rshogi_core::search",
"apply_iteration_timing: elapsed={}ms total_time={:.3} max_time={} min_time={} stop_threshold={:?} search_end={} nodes_effort={:.1} depth={} ponder={} final_push={} single_move_limit={} stop_on_ponderhit={}",
effective_elapsed,
total_time,
self.maximum_time,
self.minimum_time,
self.last_stop_threshold,
self.search_end,
nodes_effort,
completed_depth,
is_pondering,
self.is_final_push,
self.single_move_limit,
self.stop_on_ponderhit,
);
}
#[inline]
pub fn optimum(&self) -> TimePoint {
self.optimum_time
}
#[inline]
pub fn maximum(&self) -> TimePoint {
self.maximum_time
}
#[inline]
pub fn minimum(&self) -> TimePoint {
self.minimum_time
}
#[inline]
pub fn search_end(&self) -> TimePoint {
self.search_end
}
pub fn reset_search_end(&mut self) {
self.search_end = 0;
self.stop_on_ponderhit = false;
self.last_stop_threshold = None;
}
#[inline]
pub fn stop_on_ponderhit(&self) -> bool {
self.stop_on_ponderhit
}
#[inline]
pub fn is_pondering(&self) -> bool {
self.is_pondering
}
pub fn reset_stop_on_ponderhit(&mut self) {
self.stop_on_ponderhit = false;
}
#[inline]
pub fn elapsed(&self) -> TimePoint {
self.start_time.elapsed().as_millis() as TimePoint
}
#[inline]
pub fn elapsed_from_ponderhit(&self) -> TimePoint {
self.ponderhit_time.elapsed().as_millis() as TimePoint
}
pub fn take_ponderhit(&self) -> bool {
self.ponderhit.swap(false, Ordering::Relaxed)
}
pub fn should_stop(&mut self, depth: i32) -> bool {
let _ = depth; let elapsed = self.elapsed();
self.should_stop_internal(elapsed)
}
#[inline]
pub fn should_stop_immediately(&mut self) -> bool {
let elapsed = self.elapsed();
self.should_stop_internal(elapsed)
}
fn should_stop_internal(&mut self, elapsed: TimePoint) -> bool {
let effective_elapsed = self.effective_elapsed(elapsed);
if self.stop.load(Ordering::Relaxed) {
debug!(
target: "rshogi_core::search",
"stop check: external stop elapsed={} effective_elapsed={} search_end={} last_stop_threshold={:?} max_time={}",
elapsed,
effective_elapsed,
self.search_end,
self.last_stop_threshold,
self.maximum_time
);
return true;
}
if self.is_pondering {
return false;
}
if self.search_end == 0 && self.stop_on_ponderhit {
self.set_search_end(elapsed);
}
if self.search_end > 0 {
if elapsed >= self.search_end {
debug!(
target: "rshogi_core::search",
"stop check: search_end reached elapsed={} search_end={}",
elapsed,
self.search_end
);
return true;
}
return false;
}
if let Some(threshold) = self.last_stop_threshold
&& effective_elapsed >= threshold
{
debug!(
target: "rshogi_core::search",
"stop check: last_stop_threshold reached effective_elapsed={} threshold={}",
effective_elapsed,
threshold
);
return true;
}
if effective_elapsed >= self.maximum_time {
debug!(
target: "rshogi_core::search",
"stop check: maximum_time reached effective_elapsed={} max_time={}",
effective_elapsed,
self.maximum_time
);
return true;
}
false
}
pub fn set_ponderhit(&mut self) {
self.ponderhit_time = Instant::now();
}
fn ponderhit_offset(&self) -> TimePoint {
if self.ponderhit_time >= self.start_time {
self.ponderhit_time.duration_since(self.start_time).as_millis() as TimePoint
} else {
0
}
}
fn effective_elapsed(&self, elapsed_raw: TimePoint) -> TimePoint {
elapsed_raw.saturating_sub(self.ponderhit_offset()).max(0)
}
pub fn on_ponderhit(&mut self) {
if !self.is_pondering {
self.ponderhit.store(false, Ordering::Relaxed);
return;
}
self.set_ponderhit();
self.is_pondering = false;
self.last_stop_threshold = None;
if self.search_end == 0 && self.stop_on_ponderhit {
let elapsed = self.elapsed();
self.set_search_end(elapsed);
}
self.ponderhit.store(false, Ordering::Relaxed);
}
pub fn set_search_end(&mut self, elapsed_ms: TimePoint) {
let duration_start_to_ponderhit = self.ponderhit_offset();
let t1 = elapsed_ms.saturating_sub(duration_start_to_ponderhit);
let t2 = if self.is_final_push {
self.minimum_time
} else {
self.minimum_time.saturating_sub(duration_start_to_ponderhit)
};
let max_time = std::cmp::max(t1, t2);
let rounded = self.round_up(max_time);
self.search_end = rounded.saturating_add(duration_start_to_ponderhit);
}
pub fn request_stop(&self) {
self.stop.store(true, Ordering::Relaxed);
}
pub fn reset_stop(&self) {
self.stop.store(false, Ordering::Relaxed);
}
#[inline]
pub fn stop_requested(&self) -> bool {
self.stop.load(Ordering::Relaxed)
}
}
impl Default for TimeManagement {
fn default() -> Self {
Self::new(Arc::new(AtomicBool::new(false)), Arc::new(AtomicBool::new(false)))
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
fn create_time_manager() -> TimeManagement {
TimeManagement::new(Arc::new(AtomicBool::new(false)), Arc::new(AtomicBool::new(false)))
}
#[test]
fn test_time_manager_rtime_sets_budget() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.rtime = 2500;
limits.set_start_time();
tm.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
assert_eq!(tm.minimum(), 2500);
assert_eq!(tm.optimum(), 2500);
assert_eq!(tm.maximum(), 2500);
assert_eq!(tm.search_end(), 2500, "rtime は固定時間として search_end も設定されるべき");
}
#[test]
fn test_optimum_scales_with_ponder_option() {
let mut base = create_time_manager();
let mut ponder = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60_000;
limits.set_start_time();
base.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
ponder.set_options(&TimeOptions {
usi_ponder: true,
stochastic_ponder: false,
..TimeOptions::default()
});
ponder.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
assert_eq!(ponder.optimum(), base.optimum() + base.optimum() / 4);
}
#[test]
fn test_compute_time_factor_uses_yaneura_coeffs() {
let mut tm = create_time_manager();
tm.optimum_time = 1000;
tm.maximum_time = 2000;
tm.remain_time = TimePoint::MAX / 4;
tm.single_move_limit = false;
tm.is_final_push = false;
tm.previous_time_reduction = 0.85;
let falling_eval = 1.1;
let time_reduction = 1.2;
let tot_best_move_changes = 0.0;
let thread_count = 1;
let factor = tm.compute_time_factor(
falling_eval,
time_reduction,
tot_best_move_changes,
thread_count,
);
let instability = 0.9929 + 1.8519 * tot_best_move_changes / thread_count as f64;
let reduction = (1.4540 + 0.85) / (2.1593 * time_reduction);
let expected_factor = falling_eval * reduction * instability;
assert!((factor - expected_factor).abs() < 1e-9);
assert_eq!(tm.optimum(), 1000);
assert_eq!(tm.maximum(), 2000);
}
#[test]
fn test_previous_time_reduction_roundtrip() {
let mut tm = create_time_manager();
tm.set_previous_time_reduction(0.42);
assert!((tm.previous_time_reduction() - 0.42).abs() < 1e-9);
}
#[test]
fn test_previous_time_reduction_is_preserved_through_init() {
let mut tm = create_time_manager();
tm.set_previous_time_reduction(0.42);
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60_000;
limits.set_start_time();
tm.init(&limits, Color::Black, 10, DEFAULT_MAX_MOVES_TO_DRAW);
assert!(
(tm.previous_time_reduction() - 0.42).abs() < 1e-9,
"init() で previous_time_reduction がリセットされないことを保証する"
);
}
#[test]
fn test_apply_iteration_timing_sets_search_end_on_effort() {
let mut tm = create_time_manager();
tm.optimum_time = 1000;
tm.maximum_time = 2000;
tm.remain_time = 5000;
tm.minimum_time = 500;
tm.search_end = 0;
tm.apply_iteration_timing(1200, 1000.0, 98000.0, 12);
assert!(tm.search_end() > 0, "search_end should be set when nodes_effort threshold hit");
}
#[test]
fn test_time_manager_init_no_time_management() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.depth = 10;
tm.init(&limits, Color::Black, 0, 256);
assert!(tm.optimum() > 1_000_000_000);
assert!(tm.maximum() > 1_000_000_000);
}
#[test]
fn test_time_manager_init_with_time() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60000; limits.set_start_time();
tm.init(&limits, Color::Black, 0, 256);
assert!(tm.optimum() > 0);
assert!(tm.maximum() >= tm.optimum());
assert!(tm.maximum() <= 60000);
}
#[test]
fn test_time_manager_init_byoyomi() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.byoyomi[Color::Black.index()] = 30000; limits.set_start_time();
tm.init(&limits, Color::Black, 0, 256);
assert!(tm.optimum() > 0);
assert!(tm.optimum() < 30000);
}
#[test]
fn test_time_manager_elapsed() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60000;
limits.set_start_time();
tm.init(&limits, Color::Black, 0, 256);
std::thread::sleep(std::time::Duration::from_millis(10));
let elapsed = tm.elapsed();
assert!(elapsed >= 10);
assert!(elapsed < 1000);
}
#[test]
fn test_time_manager_should_stop() {
let stop = Arc::new(AtomicBool::new(false));
let mut tm = TimeManagement::new(Arc::clone(&stop), Arc::new(AtomicBool::new(false)));
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 100; limits.set_start_time();
tm.init(&limits, Color::Black, 0, 256);
assert!(!stop.load(Ordering::Relaxed));
stop.store(true, Ordering::Relaxed);
assert!(tm.should_stop(5));
}
#[test]
fn test_stop_on_ponderhit_sets_search_end_when_checked() {
let stop = Arc::new(AtomicBool::new(false));
let mut tm = TimeManagement::new(Arc::clone(&stop), Arc::new(AtomicBool::new(false)));
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 5000;
limits.set_start_time();
tm.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
tm.stop_on_ponderhit = true;
tm.start_time = Instant::now() - std::time::Duration::from_millis(1200);
let before = tm.search_end();
let should_stop = tm.should_stop(5);
assert!(
tm.search_end() > before,
"search_end should be set when stop_on_ponderhit is set"
);
assert!(!should_stop || tm.elapsed() >= tm.search_end());
}
#[test]
fn test_last_stop_threshold_is_used() {
let mut tm = create_time_manager();
tm.maximum_time = 5000;
tm.optimum_time = 1000;
tm.last_stop_threshold = Some(1500);
tm.start_time = Instant::now() - Duration::from_millis(1600);
tm.ponderhit_time = tm.start_time;
assert!(tm.should_stop(5), "elapsed beyond last_stop_threshold should stop");
tm.search_end = 0;
tm.last_stop_threshold = Some(2000);
tm.start_time = Instant::now() - Duration::from_millis(500);
tm.ponderhit_time = tm.start_time;
assert!(!tm.should_stop(5), "elapsed below threshold should continue");
}
#[test]
fn test_time_manager_round_up() {
let tm = create_time_manager();
let result = tm.round_up(1);
assert_eq!(result, 1880);
let result = tm.round_up(500);
assert_eq!(result, 1880);
let result = tm.round_up(1001);
assert_eq!(result, 1880);
}
#[test]
fn test_round_up_respects_minimum_lower_bound() {
let mut tm = create_time_manager();
tm.set_options(&TimeOptions {
network_delay: 120,
network_delay2: 1120,
minimum_thinking_time: 1000,
slow_mover: 100,
..TimeOptions::default()
});
assert_eq!(tm.round_up(1), 880);
}
#[test]
fn test_time_manager_on_ponderhit_switches_off_ponder_without_forcing_stop() {
let stop = Arc::new(AtomicBool::new(false));
let mut tm = TimeManagement::new(Arc::clone(&stop), Arc::new(AtomicBool::new(false)));
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 5000; limits.ponder = true;
limits.set_start_time();
tm.init(&limits, Color::Black, 0, 256);
tm.last_stop_threshold = Some(1);
tm.on_ponderhit();
assert!(!tm.is_pondering(), "ponderhit後は通常探索へ移行するべき");
assert_eq!(tm.search_end(), 0, "stop_on_ponderhit が無ければ search_end は確定しない");
assert!(tm.last_stop_threshold.is_none(), "ponderhitで停止閾値をリセットする");
}
#[test]
fn test_ponderhit_does_not_consume_budget_from_long_ponder() {
let stop = Arc::new(AtomicBool::new(false));
let mut tm = TimeManagement::new(Arc::clone(&stop), Arc::new(AtomicBool::new(false)));
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60000; limits.ponder = true;
limits.start_time = Some(Instant::now() - Duration::from_millis(20_000));
tm.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
tm.on_ponderhit();
assert!(!tm.is_pondering(), "ponderhit後は通常探索に切り替わる");
let raw_elapsed = tm.elapsed();
tm.apply_iteration_timing(raw_elapsed, 5000.0, 0.0, 12);
assert_eq!(tm.search_end(), 0, "stop_on_ponderhitが無ければ search_end は確定しない");
assert!(
!tm.should_stop_immediately(),
"ponderhit後は ponder 前の経過時間に引きずられず継続できるべき"
);
}
#[test]
fn test_on_ponderhit_ignored_when_not_pondering() {
let stop = Arc::new(AtomicBool::new(false));
let mut tm = TimeManagement::new(Arc::clone(&stop), Arc::new(AtomicBool::new(false)));
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60000;
limits.ponder = false;
limits.start_time = Some(Instant::now() - Duration::from_millis(1500));
tm.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
let before_elapsed = tm.elapsed_from_ponderhit();
tm.on_ponderhit();
let after_elapsed = tm.elapsed_from_ponderhit();
assert!(after_elapsed >= before_elapsed, "非ponder時は時間基準をリセットしない");
assert_eq!(tm.search_end(), 0, "search_endを確定させない");
assert!(!tm.stop_on_ponderhit(), "stop_on_ponderhitも変更しない");
assert!(!tm.is_pondering(), "状態は通常探索のまま");
}
#[test]
fn test_on_ponderhit_final_push_respects_minimum() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.byoyomi[Color::Black.index()] = 4000;
limits.ponder = true;
limits.set_start_time();
tm.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
tm.stop_on_ponderhit = true;
tm.on_ponderhit();
assert!(
tm.search_end() >= tm.round_up(tm.minimum()),
"search_end {} should be >= rounded minimum {}",
tm.search_end(),
tm.round_up(tm.minimum())
);
}
#[test]
fn test_apply_iteration_timing_depth_gate() {
let mut tm = create_time_manager();
tm.optimum_time = 1000;
tm.maximum_time = 2000;
tm.remain_time = 5000;
tm.minimum_time = 500;
tm.search_end = 0;
tm.apply_iteration_timing(900, 1000.0, 98000.0, 8);
assert_eq!(tm.search_end(), 0);
tm.apply_iteration_timing(1200, 1000.0, 98000.0, 12);
assert!(tm.search_end() >= tm.round_up(tm.minimum()));
}
#[test]
fn test_round_up_uses_remain_time_and_delay() {
let stop = Arc::new(AtomicBool::new(false));
let mut tm = TimeManagement::new(Arc::clone(&stop), Arc::new(AtomicBool::new(false)));
tm.set_options(&TimeOptions {
network_delay: 120,
network_delay2: 1120,
minimum_thinking_time: 2000,
slow_mover: 100,
usi_ponder: false,
stochastic_ponder: false,
});
let mut limits = LimitsType::new();
limits.byoyomi[Color::Black.index()] = 5000;
limits.set_start_time();
tm.init(&limits, Color::Black, 0, 256);
assert_eq!(tm.optimum(), 4880);
assert_eq!(tm.maximum(), 4880);
assert_eq!(tm.minimum(), 4880);
}
#[test]
fn test_network_delay2_reduces_time_budget() {
let mut tm_base = create_time_manager();
tm_base.set_options(&TimeOptions {
network_delay: 0,
network_delay2: 0,
minimum_thinking_time: 2000,
slow_mover: 100,
usi_ponder: false,
stochastic_ponder: false,
});
let mut tm_delay = create_time_manager();
tm_delay.set_options(&TimeOptions {
network_delay: 0,
network_delay2: 2000,
minimum_thinking_time: 2000,
slow_mover: 100,
usi_ponder: false,
stochastic_ponder: false,
});
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 10_000;
limits.set_start_time();
tm_base.init(&limits, Color::Black, 0, 256);
tm_delay.init(&limits, Color::Black, 0, 256);
assert!(
tm_delay.optimum() <= tm_base.optimum(),
"network_delay2 should not increase optimum: base={}, delay={}",
tm_base.optimum(),
tm_delay.optimum()
);
}
#[test]
fn test_slow_mover_scales_time() {
let mut tm_base = create_time_manager();
let mut tm_slow = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60_000;
limits.set_start_time();
tm_base.init(&limits, Color::Black, 0, 256);
tm_slow.set_options(&TimeOptions {
network_delay: 120,
network_delay2: 1120,
minimum_thinking_time: 2000,
slow_mover: 200, usi_ponder: false,
stochastic_ponder: false,
});
tm_slow.init(&limits, Color::Black, 0, 256);
assert!(
tm_slow.optimum() > tm_base.optimum(),
"slow mover should increase optimum: base={}, slow={}",
tm_base.optimum(),
tm_slow.optimum()
);
}
}