use crate::search::engine::compute_aspiration_window;
use crate::search::types::{RootMove, RootMoves};
use crate::types::{Move, Value};
use std::thread;
const STACK_SIZE: usize = 64 * 1024 * 1024;
fn run_with_large_stack<F, R>(f: F) -> R
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
thread::Builder::new()
.stack_size(STACK_SIZE)
.spawn(f)
.expect("failed to spawn test thread with large stack")
.join()
.expect("test thread panicked")
}
#[test]
fn test_multi_pv_clamped_by_legal_moves() {
let root_moves_count = 3; let requested_multi_pv = 5;
let effective_multi_pv = requested_multi_pv.min(root_moves_count);
assert_eq!(effective_multi_pv, 3, "MultiPV=5だが合法手が3つなので3にクランプ");
}
#[test]
fn test_skill_level_forces_multi_pv_4() {
let skill_enabled = true;
let user_multi_pv = 1;
let effective_multi_pv = if skill_enabled {
user_multi_pv.max(4)
} else {
user_multi_pv
};
assert_eq!(effective_multi_pv, 4, "SkillLevel有効時は最低4");
}
#[test]
fn test_no_skill_level_uses_normal_multi_pv() {
let skill_enabled = false;
let user_multi_pv = 1;
let effective_multi_pv = if skill_enabled {
user_multi_pv.max(4)
} else {
user_multi_pv
};
assert_eq!(effective_multi_pv, 1, "SkillLevel無効時は指定通り");
}
#[test]
fn test_skill_level_with_high_multi_pv() {
let skill_enabled = true;
let user_multi_pv = 5;
let effective_multi_pv = if skill_enabled {
user_multi_pv.max(4)
} else {
user_multi_pv
};
assert_eq!(effective_multi_pv, 5, "既に4以上なら変更なし");
}
#[test]
fn test_multi_pv_stable_sort_per_pv() {
let mut root_moves = [
RootMove::new(Move::from_usi("7g7f").unwrap()),
RootMove::new(Move::from_usi("2g2f").unwrap()),
RootMove::new(Move::from_usi("5g5f").unwrap()),
];
root_moves[0].score = Value::new(100);
root_moves[1].score = Value::new(200);
root_moves[2].score = Value::new(150);
root_moves[0..1].sort_by_key(|rm| std::cmp::Reverse(rm.score));
root_moves[0..2].sort_by_key(|rm| std::cmp::Reverse(rm.score));
assert_eq!(root_moves[0].pv[0], Move::from_usi("2g2f").unwrap());
assert_eq!(root_moves[1].pv[0], Move::from_usi("7g7f").unwrap());
root_moves[0..3].sort_by_key(|rm| std::cmp::Reverse(rm.score));
assert_eq!(root_moves[0].pv[0], Move::from_usi("2g2f").unwrap());
assert_eq!(root_moves[1].pv[0], Move::from_usi("5g5f").unwrap());
assert_eq!(root_moves[2].pv[0], Move::from_usi("7g7f").unwrap());
}
#[test]
fn test_stable_sort_range() {
let mut rm1 = RootMove::new(Move::from_usi("7g7f").unwrap());
rm1.score = Value::new(100);
let mut rm2 = RootMove::new(Move::from_usi("2g2f").unwrap());
rm2.score = Value::new(200);
let mut rm3 = RootMove::new(Move::from_usi("5g5f").unwrap());
rm3.score = Value::new(150);
let mut rm4 = RootMove::new(Move::from_usi("8h7g").unwrap());
rm4.score = Value::new(200);
let mut root_moves =
RootMoves::from_vec(vec![rm1.clone(), rm2.clone(), rm3.clone(), rm4.clone()]);
root_moves.stable_sort_range(0, 4);
assert_eq!(root_moves[0].score.raw(), 200);
assert_eq!(
root_moves[0].pv[0],
Move::from_usi("2g2f").unwrap(),
"同点の場合、元の順序を保持(rm2が先)"
);
assert_eq!(root_moves[1].score.raw(), 200);
assert_eq!(
root_moves[1].pv[0],
Move::from_usi("8h7g").unwrap(),
"同点の場合、元の順序を保持(rm4が後)"
);
assert_eq!(root_moves[2].score.raw(), 150);
assert_eq!(root_moves[2].pv[0], Move::from_usi("5g5f").unwrap());
assert_eq!(root_moves[3].score.raw(), 100);
assert_eq!(root_moves[3].pv[0], Move::from_usi("7g7f").unwrap());
}
#[test]
fn test_stable_sort_range_partial() {
let mut rm1 = RootMove::new(Move::from_usi("7g7f").unwrap());
rm1.score = Value::new(100);
let mut rm2 = RootMove::new(Move::from_usi("2g2f").unwrap());
rm2.score = Value::new(50);
let mut rm3 = RootMove::new(Move::from_usi("5g5f").unwrap());
rm3.score = Value::new(150);
let mut rm4 = RootMove::new(Move::from_usi("8h7g").unwrap());
rm4.score = Value::new(75);
let mut root_moves =
RootMoves::from_vec(vec![rm1.clone(), rm2.clone(), rm3.clone(), rm4.clone()]);
root_moves.stable_sort_range(1, 4);
assert_eq!(root_moves[0].score.raw(), 100);
assert_eq!(root_moves[0].pv[0], Move::from_usi("7g7f").unwrap(), "範囲外は変更されない");
assert_eq!(root_moves[1].score.raw(), 150);
assert_eq!(root_moves[1].pv[0], Move::from_usi("5g5f").unwrap());
assert_eq!(root_moves[2].score.raw(), 75);
assert_eq!(root_moves[2].pv[0], Move::from_usi("8h7g").unwrap());
assert_eq!(root_moves[3].score.raw(), 50);
assert_eq!(root_moves[3].pv[0], Move::from_usi("2g2f").unwrap());
}
#[test]
fn test_mate_early_exit_when_multi_pv_1() {
let multi_pv = 1;
let best_value = Value::mate_in(10); let depth = 30;
let mate_ply = best_value.mate_ply();
let should_exit = multi_pv == 1 && best_value.is_win() && (mate_ply + 2) * 5 / 2 < depth;
assert!(!should_exit, "30 < 30 は false なので早期終了しない");
let depth = 31;
let should_exit = multi_pv == 1 && best_value.is_win() && (mate_ply + 2) * 5 / 2 < depth;
assert!(should_exit, "30 < 31 は true なので早期終了");
}
#[test]
fn test_mate_no_early_exit_when_multi_pv_gt_1() {
let multi_pv = 3;
let best_value = Value::mate_in(5);
let depth = 30;
let mate_ply = best_value.mate_ply();
let should_exit = multi_pv == 1 && best_value.is_win() && (mate_ply + 2) * 5 / 2 < depth;
assert!(!should_exit, "MultiPV>1では詰みでも早期終了しない");
}
#[test]
fn test_no_early_exit_when_mated() {
let multi_pv = 1;
let best_value = Value::mated_in(10); let depth = 30;
let should_exit =
multi_pv == 1 && best_value.is_win() && (best_value.mate_ply() + 2) * 5 / 2 < depth;
assert!(!should_exit, "詰まされる側は早期終了しない");
}
#[test]
fn test_multi_pv_3_integration() {
use crate::position::Position;
use crate::search::LimitsType;
use crate::search::engine::{Search, SearchInfo};
run_with_large_stack(|| {
let mut search = Search::new(16); let mut pos = Position::new();
pos.set_hirate();
let limits = LimitsType {
depth: 1,
multi_pv: 3,
..Default::default()
};
let mut infos = Vec::new();
search.go(
&mut pos,
limits,
Some(|info: &SearchInfo| {
infos.push(info.clone());
}),
);
let depth1_infos: Vec<_> = infos.iter().filter(|info| info.depth == 1).collect();
assert!(
depth1_infos.len() >= 3,
"MultiPV=3なので最低3つのPVライン。実際: {}",
depth1_infos.len()
);
let multipv_values: Vec<usize> = depth1_infos.iter().map(|info| info.multi_pv).collect();
assert!(multipv_values.contains(&1), "multipv 1が含まれる。実際: {multipv_values:?}");
assert!(multipv_values.contains(&2), "multipv 2が含まれる。実際: {multipv_values:?}");
assert!(multipv_values.contains(&3), "multipv 3が含まれる。実際: {multipv_values:?}");
let mut first_moves = std::collections::HashSet::new();
for info in &depth1_infos {
if !info.pv.is_empty() {
first_moves.insert(info.pv[0].to_u32());
}
}
assert!(
first_moves.len() >= 2,
"MultiPV=3なので少なくとも2つ以上の異なる候補手があるはず。実際: {}",
first_moves.len()
);
});
}
#[test]
fn test_multi_pv_1_outputs_multipv_field() {
use crate::position::Position;
use crate::search::LimitsType;
use crate::search::engine::{Search, SearchInfo};
run_with_large_stack(|| {
let mut search = Search::new(16);
let mut pos = Position::new();
pos.set_hirate();
let limits = LimitsType {
depth: 1,
multi_pv: 1,
..Default::default()
};
let mut last_info = None;
search.go(
&mut pos,
limits,
Some(|info: &SearchInfo| {
if info.depth == 1 {
last_info = Some(info.clone());
}
}),
);
let info = last_info.expect("depth=1のinfo出力があるはず");
assert_eq!(info.multi_pv, 1, "MultiPV=1でも multipv 1 を出力");
let usi_string = info.to_usi_string();
assert!(
usi_string.contains("multipv 1"),
"USI出力に 'multipv 1' が含まれる。実際: {usi_string}"
);
});
}
#[test]
fn test_multi_pv_clamped_to_legal_moves_integration() {
use crate::position::Position;
use crate::search::LimitsType;
use crate::search::engine::{Search, SearchInfo};
run_with_large_stack(|| {
let mut search = Search::new(16);
let mut pos = Position::new();
pos.set_hirate();
let limits = LimitsType {
depth: 1,
multi_pv: 100, ..Default::default()
};
let mut infos = Vec::new();
search.go(
&mut pos,
limits,
Some(|info: &SearchInfo| {
if info.depth == 1 {
infos.push(info.clone());
}
}),
);
let max_multipv = infos.iter().map(|info| info.multi_pv).max().unwrap_or(0);
assert!(max_multipv < 100, "合法手数でクランプされる。最大MultiPV: {max_multipv}");
assert!(
max_multipv >= 10,
"平手初期局面なので少なくとも10手以上の合法手がある。実際: {max_multipv}"
);
let mut multipv_values: Vec<usize> = infos.iter().map(|info| info.multi_pv).collect();
multipv_values.sort();
multipv_values.dedup();
for (i, &value) in multipv_values.iter().enumerate() {
assert_eq!(value, i + 1, "multipv値が1から連続している");
}
});
}
#[test]
fn test_multi_pv_scores_sorted_desc() {
use crate::position::Position;
use crate::search::LimitsType;
use crate::search::engine::{Search, SearchInfo};
run_with_large_stack(|| {
let mut search = Search::new(16);
let mut pos = Position::new();
pos.set_hirate();
let limits = LimitsType {
depth: 1,
multi_pv: 3,
..Default::default()
};
let mut infos: Vec<SearchInfo> = Vec::new();
search.go(
&mut pos,
limits,
Some(|info: &SearchInfo| {
if info.depth == 1 {
infos.push(info.clone());
}
}),
);
infos.sort_by_key(|i| i.multi_pv);
assert!(
infos.len() >= 2,
"MultiPV=3なので2本以上のinfo出力があるはず。実際: {}",
infos.len()
);
for window in infos.windows(2) {
let first = &window[0];
let second = &window[1];
assert!(
first.score.raw() >= second.score.raw(),
"multipv {} のスコア {} が multipv {} のスコア {} より小さい",
first.multi_pv,
first.score.raw(),
second.multi_pv,
second.score.raw()
);
}
});
}
#[test]
fn test_aspiration_window_uses_average_and_mean_squared() {
let mut rm = RootMove::new(Move::from_usi("7g7f").unwrap());
rm.average_score = Value::new(120);
rm.mean_squared_score = Some(11131 * 10);
let (alpha, beta, delta) = compute_aspiration_window(&rm, 0);
assert_eq!(delta.raw(), 17);
assert_eq!(alpha.raw(), 103);
assert_eq!(beta.raw(), 137);
}
#[test]
fn test_aspiration_window_defaults_to_full_window_when_unseeded() {
let rm = RootMove::new(Move::from_usi("7g7f").unwrap());
let (alpha, beta, _) = compute_aspiration_window(&rm, 0);
assert_eq!(alpha.raw(), -Value::INFINITE.raw());
assert_eq!(beta, Value::INFINITE);
}
#[test]
fn test_multi_pv_outputs_once_per_depth() {
use crate::position::Position;
use crate::search::LimitsType;
use crate::search::engine::{Search, SearchInfo};
run_with_large_stack(|| {
let mut search = Search::new(16);
let mut pos = Position::new();
pos.set_hirate();
let limits = LimitsType {
depth: 1,
multi_pv: 2,
..Default::default()
};
let expected_multipv = limits.multi_pv;
let mut depth1_infos = Vec::new();
search.go(
&mut pos,
limits,
Some(|info: &SearchInfo| {
if info.depth == 1 {
depth1_infos.push(info.clone());
}
}),
);
assert_eq!(
depth1_infos.len(),
expected_multipv,
"depthごとにMultiPV本数分だけinfoを出力する"
);
let mut multipv: Vec<_> = depth1_infos.iter().map(|info| info.multi_pv).collect();
multipv.sort();
multipv.dedup();
assert_eq!(multipv, vec![1, 2], "multipv値が1,2のみに収まる");
});
}
#[test]
fn test_previous_score_seeding() {
use crate::position::Position;
use crate::search::LimitsType;
use crate::search::engine::Search;
run_with_large_stack(|| {
let mut search = Search::new(16);
let mut pos = Position::new();
pos.set_hirate();
let limits = LimitsType {
depth: 1, multi_pv: 1,
..Default::default()
};
let result = search.go(&mut pos, limits, None::<fn(&_)>);
assert_ne!(result.best_move, Move::NONE);
});
}
#[test]
fn test_final_sort_orders_pvs_correctly() {
use crate::search::types::{RootMove, RootMoves};
use crate::types::{Move, Value};
let mut rm1 = RootMove::new(Move::from_usi("7g7f").unwrap());
rm1.score = Value::new(100);
let mut rm2 = RootMove::new(Move::from_usi("2g2f").unwrap());
rm2.score = Value::new(50);
let mut rm3 = RootMove::new(Move::from_usi("5g5f").unwrap());
rm3.score = Value::new(150);
let mut root_moves = RootMoves::from_vec(vec![rm1, rm2, rm3]);
root_moves.stable_sort_range(0, 3);
assert_eq!(root_moves[0].score.raw(), 150);
assert_eq!(root_moves[1].score.raw(), 100);
assert_eq!(root_moves[2].score.raw(), 50);
}