use crate::search::{
DEFAULT_MAX_MOVES_TO_DRAW, LimitsType, SearchTuneParams, TimeManagement, TimeOptions,
};
use crate::time::Instant;
use crate::types::Color;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::time::Duration;
fn create_time_manager() -> TimeManagement {
TimeManagement::new(Arc::new(AtomicBool::new(false)), Arc::new(AtomicBool::new(false)))
}
#[test]
fn test_calculate_falling_eval_clamp() {
use super::super::time_manager::calculate_falling_eval;
let high = calculate_falling_eval(10000, -10000, 0);
assert!((0.5786..=1.6752).contains(&high), "falling_eval should be clamped, got {high}");
}
#[test]
fn test_calculate_time_reduction_positive() {
use super::super::time_manager::calculate_time_reduction;
let tr = calculate_time_reduction(10, 5);
assert!(tr > 0.0, "time_reduction should be positive, got {tr}");
}
#[test]
fn test_best_move_instability_factor_increases_when_unstable() {
let mut tm = create_time_manager();
let factor = tm.compute_time_factor(1.0, 1.0, 1.0, 1);
assert!(factor > 1.0, "不安定な場合は factor > 1.0 となるべき: {factor}");
}
#[test]
fn test_best_move_instability_factor_bounded_when_stable() {
let mut tm = create_time_manager();
let factor = tm.compute_time_factor(1.0, 1.0, 0.0, 1);
assert!(
factor > 0.0 && factor < 3.0,
"安定時は factor が適度な範囲に収まるべき: {factor}"
);
}
#[test]
fn test_best_move_instability_does_not_mutate_budget() {
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);
let original_optimum = tm.optimum();
let original_max = tm.maximum();
let _ = tm.compute_time_factor(1.0, 1.0, 1.0, 1);
assert_eq!(tm.optimum(), original_optimum);
assert_eq!(tm.maximum(), original_max);
}
#[test]
fn test_nodes_effort_normalization() {
use super::super::time_manager::normalize_nodes_effort;
let effort = 500.0;
let nodes_total = 1000u64;
let nodes_effort = normalize_nodes_effort(effort, nodes_total);
assert_eq!(nodes_effort as i32, 50000);
}
#[test]
fn test_apply_iteration_timing_sets_stop_on_ponderhit() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 5000;
limits.ponder = true;
limits.set_start_time();
tm.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
tm.reset_search_end();
tm.apply_iteration_timing(1600, 1200.0, 0.0, 12);
assert!(tm.stop_on_ponderhit(), "ponder中は stop_on_ponderhit が立つべき");
assert_eq!(tm.search_end(), 0, "ponder中は search_end を設定しない");
tm.reset_search_end();
tm.apply_iteration_timing(1200, 1000.0, 98000.0, 12);
assert_eq!(tm.search_end(), 0);
}
#[test]
fn test_single_root_move_caps_stop_threshold() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60000; limits.start_time = Some(Instant::now() - Duration::from_millis(600));
tm.init_with_root_moves_count(&limits, Color::Black, 0, 256, 1);
tm.apply_iteration_timing(600, 2000.0, 0.0, 12);
assert!(tm.should_stop_immediately(), "500ms閾値を超えているので停止すべき");
}
#[test]
fn test_movetime_sets_search_end_and_stop() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.movetime = 50;
limits.start_time = Some(Instant::now() - Duration::from_millis(60));
tm.init(&limits, Color::Black, 0, DEFAULT_MAX_MOVES_TO_DRAW);
assert_eq!(tm.search_end(), 50, "movetime指定時はsearch_endが設定される");
assert!(tm.should_stop(1), "movetime超過で停止する");
}
#[test]
fn test_time_options_deep_defaults() {
let deep = TimeOptions::deep_defaults();
assert_eq!(deep.network_delay, 400);
assert_eq!(deep.network_delay2, 1400);
}
#[test]
fn test_worker_best_move_changes_initial_value() {
const STACK_SIZE: usize = 64 * 1024 * 1024; std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
use crate::eval::EvalHash;
use crate::search::alpha_beta::SearchWorker;
use crate::tt::TranspositionTable;
use std::sync::Arc;
let tt = Arc::new(TranspositionTable::new(16));
let eval_hash = Arc::new(EvalHash::new(1));
let worker = SearchWorker::new(
tt,
eval_hash,
DEFAULT_MAX_MOVES_TO_DRAW,
0,
SearchTuneParams::default(),
);
assert_eq!(worker.state.best_move_changes, 0.0, "初期値は0.0であるべき");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_worker_best_move_changes_decay() {
const STACK_SIZE: usize = 64 * 1024 * 1024;
std::thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(|| {
use crate::eval::EvalHash;
use crate::search::alpha_beta::SearchWorker;
use crate::tt::TranspositionTable;
use std::sync::Arc;
let tt = Arc::new(TranspositionTable::new(16));
let eval_hash = Arc::new(EvalHash::new(1));
let mut worker = SearchWorker::new(
tt,
eval_hash,
DEFAULT_MAX_MOVES_TO_DRAW,
0,
SearchTuneParams::default(),
);
worker.state.best_move_changes = 4.0;
worker.decay_best_move_changes();
assert_eq!(worker.state.best_move_changes, 2.0, "decay後は半減(4.0 → 2.0)すべき");
})
.unwrap()
.join()
.unwrap();
}
#[test]
fn test_move_horizon_time_forfeit_early_game() {
use super::super::time_manager::calculate_move_horizon;
let time_forfeit = true;
let ply = 10;
let result = calculate_move_horizon(time_forfeit, ply);
assert_eq!(result, 190, "切れ負け序盤(ply=10): 160+40-10=190");
}
#[test]
fn test_move_horizon_time_forfeit_mid_game() {
use super::super::time_manager::calculate_move_horizon;
let time_forfeit = true;
let ply = 50;
let result = calculate_move_horizon(time_forfeit, ply);
assert_eq!(result, 160, "切れ負け中盤(ply=50): 160+40-40=160");
}
#[test]
fn test_move_horizon_fischer_early_game() {
use super::super::time_manager::calculate_move_horizon;
let time_forfeit = false;
let ply = 10;
let result = calculate_move_horizon(time_forfeit, ply);
assert_eq!(result, 170, "フィッシャー序盤(ply=10): 160+20-10=170");
}
#[test]
fn test_move_horizon_fischer_late_game() {
use super::super::time_manager::calculate_move_horizon;
let time_forfeit = false;
let ply = 100;
let result = calculate_move_horizon(time_forfeit, ply);
assert_eq!(result, 100, "フィッシャー終盤(ply=100): 160+20-80=100");
}
#[test]
fn test_round_up_basic() {
let mut tm = create_time_manager();
tm.set_options(&TimeOptions {
minimum_thinking_time: 2000,
network_delay: 120,
network_delay2: 1120,
slow_mover: 100,
usi_ponder: false,
stochastic_ponder: false,
});
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 100000;
limits.set_start_time();
tm.init(&limits, Color::Black, 1, 512);
let result = tm.round_up(5500);
assert_eq!(result, 5880, "round_up(5500) = 5880");
}
#[test]
fn test_round_up_below_minimum() {
let mut tm = create_time_manager();
tm.set_options(&TimeOptions {
minimum_thinking_time: 2000,
network_delay: 120,
network_delay2: 1120,
slow_mover: 100,
usi_ponder: false,
stochastic_ponder: false,
});
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 100000;
limits.set_start_time();
tm.init(&limits, Color::Black, 1, 512);
let result = tm.round_up(1500);
assert_eq!(result, 1880, "round_up(1500) = 1880");
}
#[test]
fn test_round_up_add_extra_second() {
let mut tm = create_time_manager();
tm.set_options(&TimeOptions {
minimum_thinking_time: 2000,
network_delay: 500, network_delay2: 1500,
slow_mover: 100,
usi_ponder: false,
stochastic_ponder: false,
});
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 100000;
limits.set_start_time();
tm.init(&limits, Color::Black, 1, 512);
let result = tm.round_up(2600);
assert_eq!(result, 3500, "round_up(2600) with network_delay=500 → 3500");
}
#[test]
fn test_round_up_exceeds_remain_time() {
let mut tm = create_time_manager();
tm.set_options(&TimeOptions {
minimum_thinking_time: 2000,
network_delay: 120,
network_delay2: 1120,
slow_mover: 100,
usi_ponder: false,
stochastic_ponder: false,
});
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 5000; limits.set_start_time();
tm.init(&limits, Color::Black, 1, 512);
let result = tm.round_up(10000);
assert!(result <= tm.remain_time(), "round_up(10000) should be clamped by remain_time");
}
#[test]
fn test_final_push_byoyomi_entry() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 5000; limits.byoyomi[Color::Black.index()] = 10000; limits.inc[Color::Black.index()] = 0;
limits.set_start_time();
tm.init(&limits, Color::Black, 1, 512);
assert!(tm.is_final_push(), "持ち時間5秒 < 秒読み10秒×1.2 → isFinalPush");
assert_eq!(tm.minimum(), 14880);
assert_eq!(tm.optimum(), 14880);
assert_eq!(tm.maximum(), 14880);
}
#[test]
fn test_not_final_push_enough_time() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 30000; limits.byoyomi[Color::Black.index()] = 10000; limits.set_start_time();
tm.init(&limits, Color::Black, 1, 512);
assert!(!tm.is_final_push(), "持ち時間30秒 >= 秒読み10秒×1.2 → not finalPush");
assert!(tm.minimum() < 30000);
assert!(tm.optimum() < 30000);
}
#[test]
fn test_maximum_time_30_percent_cap() {
let mut tm = create_time_manager();
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 300000; limits.inc[Color::Black.index()] = 5000; limits.byoyomi[Color::Black.index()] = 0;
limits.set_start_time();
tm.init(&limits, Color::Black, 10, 512);
assert!(tm.maximum() > 0, "maximum_time should be positive");
}
#[test]
fn test_ponder_optimum_time_increase() {
let mut tm_no_ponder = create_time_manager();
let opts_no_ponder = TimeOptions {
usi_ponder: false,
stochastic_ponder: false,
..Default::default()
};
tm_no_ponder.set_options(&opts_no_ponder);
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60000;
limits.inc[Color::Black.index()] = 0;
limits.byoyomi[Color::Black.index()] = 0;
limits.set_start_time();
tm_no_ponder.init(&limits, Color::Black, 1, 512);
let base_optimum = tm_no_ponder.optimum();
let mut tm_ponder = create_time_manager();
let opts_ponder = TimeOptions {
usi_ponder: true,
stochastic_ponder: false,
..Default::default()
};
tm_ponder.set_options(&opts_ponder);
limits.set_start_time(); tm_ponder.init(&limits, Color::Black, 1, 512);
let expected = base_optimum + base_optimum / 4;
assert_eq!(tm_ponder.optimum(), expected, "Ponder有効時はoptimum = base + base/4");
}
#[test]
fn test_stochastic_ponder_no_increase() {
let mut tm_normal = create_time_manager();
let opts_normal = TimeOptions {
usi_ponder: true,
stochastic_ponder: false,
..Default::default()
};
tm_normal.set_options(&opts_normal);
let mut limits = LimitsType::new();
limits.time[Color::Black.index()] = 60000;
limits.set_start_time();
tm_normal.init(&limits, Color::Black, 1, 512);
let normal_optimum = tm_normal.optimum();
let mut tm_stochastic = create_time_manager();
let opts_stochastic = TimeOptions {
usi_ponder: true,
stochastic_ponder: true,
..Default::default()
};
tm_stochastic.set_options(&opts_stochastic);
limits.set_start_time();
tm_stochastic.init(&limits, Color::Black, 1, 512);
assert!(
tm_stochastic.optimum() < normal_optimum,
"Stochastic_Ponder有効時は25%増加しない"
);
}
#[test]
fn test_best_move_instability_yaneuraou_coefficients() {
use super::super::time_manager::calculate_best_move_instability;
let result = calculate_best_move_instability(0.0, 1);
assert!((result - 0.9929).abs() < 0.0001, "YaneuraOu BASE: 0.9929, got {result}");
let result = calculate_best_move_instability(1.0, 1);
let expected = 0.9929 + 1.8519;
assert!(
(result - expected).abs() < 0.0001,
"YaneuraOu FACTOR: 1.8519, expected {expected}, got {result}"
);
let result = calculate_best_move_instability(4.0, 2);
let expected = 0.9929 + 1.8519 * 2.0;
assert!(
(result - expected).abs() < 0.0001,
"YaneuraOu with threads, expected {expected}, got {result}"
);
}