verify_perft/
verify_perft.rs1use atomic_movegen::board::Board;
16use atomic_movegen::perft;
17use std::env;
18use std::fs;
19use std::process;
20use std::time::Instant;
21
22struct TestCase {
24 number: usize,
25 fen: String,
26 depths: Vec<(u32, u64)>,
28}
29
30fn parse_perft_values(path: &str) -> Vec<TestCase> {
40 let content = fs::read_to_string(path).unwrap_or_else(|e| {
41 eprintln!("Error: could not read `{}`: {}", path, e);
42 process::exit(1);
43 });
44
45 let mut cases: Vec<TestCase> = Vec::new();
46
47 for line in content.lines() {
48 let trimmed = line.trim();
49
50 if trimmed.is_empty()
52 || trimmed.starts_with("| #")
53 || trimmed.starts_with("| ---")
54 || !trimmed.starts_with('|')
55 {
56 continue;
57 }
58
59 let parts: Vec<&str> = trimmed.split('|').collect();
61 if parts.len() < 9 {
62 continue;
63 }
64
65 let number: usize = match parts[1].trim().parse() {
67 Ok(n) => n,
68 Err(_) => continue,
69 };
70
71 let depth_counts: Vec<u64> = parts[2..8]
73 .iter()
74 .map(|s| s.trim().replace([',', '_'], "").parse().unwrap_or(0))
75 .collect();
76
77 let raw_fen = parts[8].trim();
79 let fen = if let Some(start) = raw_fen.find('`') {
80 if let Some(end) = raw_fen.rfind('`') {
81 raw_fen[start + 1..end].trim()
82 } else {
83 &raw_fen[start + 1..]
84 }
85 } else {
86 raw_fen
87 };
88 let fen = fen.trim().to_string();
89
90 let depths: Vec<(u32, u64)> = depth_counts
91 .into_iter()
92 .enumerate()
93 .map(|(i, count)| (i as u32 + 1, count))
94 .collect();
95
96 cases.push(TestCase {
97 number,
98 fen,
99 depths,
100 });
101 }
102
103 cases
104}
105
106fn main() {
107 atomic_movegen::attacks::init();
108 let args: Vec<String> = env::args().collect();
109
110 let max_depth: u32 = if args.len() > 1 {
112 args[1].parse().unwrap_or(6)
113 } else {
114 6
115 };
116
117 let path = if args.len() > 2 {
119 &args[2]
120 } else {
121 "perft_values.md"
122 };
123
124 let max_depth = if max_depth == 0 { u32::MAX } else { max_depth };
126
127 let cases = parse_perft_values(path);
128
129 eprintln!(
130 "=== Perft verification ===\n Source: {path}\n Max depth: {}\n Test cases: {}\n",
131 if max_depth == u32::MAX {
132 "unlimited".to_string()
133 } else {
134 max_depth.to_string()
135 },
136 cases.len()
137 );
138
139 let mut passed = 0u64;
140 let mut failed = 0u64;
141 let mut detail_lines: Vec<String> = Vec::new();
142
143 struct CaseTiming {
145 number: usize,
146 elapsed: std::time::Duration,
147 }
148 let mut timings: Vec<CaseTiming> = Vec::with_capacity(cases.len());
149 let total_timer = Instant::now();
150
151 for case in &cases {
152 let mut board = match Board::from_fen(&case.fen) {
153 Ok(b) => b,
154 Err(e) => {
155 detail_lines.push(format!(
156 " Test #{}: INVALID FEN — {} ({})",
157 case.number, case.fen, e
158 ));
159 failed += 1;
160 continue;
161 }
162 };
163
164 let depths_to_test: Vec<(u32, u64)> = case
166 .depths
167 .iter()
168 .copied()
169 .filter(|(d, _)| *d <= max_depth)
170 .collect();
171
172 if depths_to_test.is_empty() {
173 continue;
174 }
175
176 let mut ok = true;
177 let case_timer = Instant::now();
178
179 for &(depth, expected) in &depths_to_test {
181 let result = perft(&mut board, depth);
182 if result != expected {
183 detail_lines.push(format!(
184 " Test #{} FAIL depth={}: expected {}, got {}",
185 case.number, depth, expected, result
186 ));
187 ok = false;
188 }
189 }
190
191 let elapsed = case_timer.elapsed();
192 timings.push(CaseTiming {
193 number: case.number,
194 elapsed,
195 });
196
197 if ok {
198 passed += 1;
199 println!(
201 " Test #{:<4} PASS ({} depth{}) [{:.3} s]",
202 case.number,
203 depths_to_test.len(),
204 if depths_to_test.len() == 1 { "" } else { "s" },
205 elapsed.as_secs_f64(),
206 );
207 } else {
208 failed += 1;
209 }
210 }
211
212 let total_elapsed = total_timer.elapsed();
213
214 if !detail_lines.is_empty() {
216 eprintln!("\n--- Failures ---");
217 for line in &detail_lines {
218 eprintln!("{line}");
219 }
220 }
221
222 let total_tests = passed + failed;
224 let fast = timings.iter().min_by(|a, b| a.elapsed.cmp(&b.elapsed));
225 let slow = timings.iter().max_by(|a, b| a.elapsed.cmp(&b.elapsed));
226 let total_time: std::time::Duration = timings.iter().map(|t| t.elapsed).sum();
227 let avg_time = if !timings.is_empty() {
228 total_time / timings.len() as u32
229 } else {
230 std::time::Duration::ZERO
231 };
232
233 eprintln!();
235 eprintln!("--- Summary ---");
236 if let Some(f) = fast {
237 eprintln!(
238 " Fastest: Test #{} ({:.3} s)",
239 f.number,
240 f.elapsed.as_secs_f64()
241 );
242 }
243 if let Some(s) = slow {
244 eprintln!(
245 " Slowest: Test #{} ({:.3} s)",
246 s.number,
247 s.elapsed.as_secs_f64()
248 );
249 }
250 eprintln!(" Average: {:.3} s per test", avg_time.as_secs_f64());
251 eprintln!(" Total time: {:.3} s", total_elapsed.as_secs_f64());
252 eprintln!(" Result: {passed}/{total_tests} passed, {failed}/{total_tests} failed");
253
254 if failed > 0 {
255 process::exit(1);
256 }
257}