use atomic_movegen::board::Board;
use atomic_movegen::perft;
use std::env;
use std::fs;
use std::process;
use std::time::Instant;
struct TestCase {
number: usize,
fen: String,
depths: Vec<(u32, u64)>,
}
fn parse_perft_values(path: &str) -> Vec<TestCase> {
let content = fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("Error: could not read `{}`: {}", path, e);
process::exit(1);
});
let mut cases: Vec<TestCase> = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty()
|| trimmed.starts_with("| #")
|| trimmed.starts_with("| ---")
|| !trimmed.starts_with('|')
{
continue;
}
let parts: Vec<&str> = trimmed.split('|').collect();
if parts.len() < 9 {
continue;
}
let number: usize = match parts[1].trim().parse() {
Ok(n) => n,
Err(_) => continue,
};
let depth_counts: Vec<u64> = parts[2..8]
.iter()
.map(|s| s.trim().replace([',', '_'], "").parse().unwrap_or(0))
.collect();
let raw_fen = parts[8].trim();
let fen = if let Some(start) = raw_fen.find('`') {
if let Some(end) = raw_fen.rfind('`') {
raw_fen[start + 1..end].trim()
} else {
&raw_fen[start + 1..]
}
} else {
raw_fen
};
let fen = fen.trim().to_string();
let depths: Vec<(u32, u64)> = depth_counts
.into_iter()
.enumerate()
.map(|(i, count)| (i as u32 + 1, count))
.collect();
cases.push(TestCase {
number,
fen,
depths,
});
}
cases
}
fn main() {
atomic_movegen::attacks::init();
let args: Vec<String> = env::args().collect();
let max_depth: u32 = if args.len() > 1 {
args[1].parse().unwrap_or(6)
} else {
6
};
let path = if args.len() > 2 {
&args[2]
} else {
"perft_values.md"
};
let max_depth = if max_depth == 0 { u32::MAX } else { max_depth };
let cases = parse_perft_values(path);
eprintln!(
"=== Perft verification ===\n Source: {path}\n Max depth: {}\n Test cases: {}\n",
if max_depth == u32::MAX {
"unlimited".to_string()
} else {
max_depth.to_string()
},
cases.len()
);
let mut passed = 0u64;
let mut failed = 0u64;
let mut detail_lines: Vec<String> = Vec::new();
struct CaseTiming {
number: usize,
elapsed: std::time::Duration,
}
let mut timings: Vec<CaseTiming> = Vec::with_capacity(cases.len());
let total_timer = Instant::now();
for case in &cases {
let mut board = match Board::from_fen(&case.fen) {
Ok(b) => b,
Err(e) => {
detail_lines.push(format!(
" Test #{}: INVALID FEN — {} ({})",
case.number, case.fen, e
));
failed += 1;
continue;
}
};
let depths_to_test: Vec<(u32, u64)> = case
.depths
.iter()
.copied()
.filter(|(d, _)| *d <= max_depth)
.collect();
if depths_to_test.is_empty() {
continue;
}
let mut ok = true;
let case_timer = Instant::now();
for &(depth, expected) in &depths_to_test {
let result = perft(&mut board, depth);
if result != expected {
detail_lines.push(format!(
" Test #{} FAIL depth={}: expected {}, got {}",
case.number, depth, expected, result
));
ok = false;
}
}
let elapsed = case_timer.elapsed();
timings.push(CaseTiming {
number: case.number,
elapsed,
});
if ok {
passed += 1;
println!(
" Test #{:<4} PASS ({} depth{}) [{:.3} s]",
case.number,
depths_to_test.len(),
if depths_to_test.len() == 1 { "" } else { "s" },
elapsed.as_secs_f64(),
);
} else {
failed += 1;
}
}
let total_elapsed = total_timer.elapsed();
if !detail_lines.is_empty() {
eprintln!("\n--- Failures ---");
for line in &detail_lines {
eprintln!("{line}");
}
}
let total_tests = passed + failed;
let fast = timings.iter().min_by(|a, b| a.elapsed.cmp(&b.elapsed));
let slow = timings.iter().max_by(|a, b| a.elapsed.cmp(&b.elapsed));
let total_time: std::time::Duration = timings.iter().map(|t| t.elapsed).sum();
let avg_time = if !timings.is_empty() {
total_time / timings.len() as u32
} else {
std::time::Duration::ZERO
};
eprintln!();
eprintln!("--- Summary ---");
if let Some(f) = fast {
eprintln!(
" Fastest: Test #{} ({:.3} s)",
f.number,
f.elapsed.as_secs_f64()
);
}
if let Some(s) = slow {
eprintln!(
" Slowest: Test #{} ({:.3} s)",
s.number,
s.elapsed.as_secs_f64()
);
}
eprintln!(" Average: {:.3} s per test", avg_time.as_secs_f64());
eprintln!(" Total time: {:.3} s", total_elapsed.as_secs_f64());
eprintln!(" Result: {passed}/{total_tests} passed, {failed}/{total_tests} failed");
if failed > 0 {
process::exit(1);
}
}