depth4_survey/
depth4_survey.rs1use quantik_core::bench::reference::solve_position_with_book;
21use quantik_core::bitboard::Bitboard;
22use quantik_core::game::has_winning_line;
23use quantik_core::moves::{apply_move, generate_legal_moves};
24use quantik_core::opening_book::{OpeningBookConfig, OpeningBookDatabase};
25use quantik_core::state::State;
26use quantik_core::symmetry::SymmetryHandler;
27use std::collections::HashMap;
28use std::time::Instant;
29
30struct Args {
31 budget_s: f64,
32 limit: usize,
33 sample: usize,
34 seed: u64,
35 db: String,
36 out: String,
37}
38
39fn parse_args() -> Args {
40 let mut budget_s = 5.0;
41 let mut limit = 0usize;
42 let mut sample = 0usize;
43 let mut seed = 20260712u64;
44 let mut db = "benchmarks/results/depth4.db".to_string();
45 let mut out = "benchmarks/results/depth4-survey.json".to_string();
46 let mut it = std::env::args().skip(1);
47 while let Some(flag) = it.next() {
48 match flag.as_str() {
49 "--budget-s" => budget_s = it.next().unwrap().parse().unwrap(),
50 "--limit" => limit = it.next().unwrap().parse().unwrap(),
51 "--sample" => sample = it.next().unwrap().parse().unwrap(),
52 "--seed" => seed = it.next().unwrap().parse().unwrap(),
53 "--db" => db = it.next().unwrap(),
54 "--out" => out = it.next().unwrap(),
55 other => panic!("unknown flag {other}"),
56 }
57 }
58 Args {
59 budget_s,
60 limit,
61 sample,
62 seed,
63 db,
64 out,
65 }
66}
67
68fn enumerate_depth4() -> Vec<(Bitboard, u64)> {
72 let mut frontier: HashMap<[u8; 16], (Bitboard, u64)> = HashMap::new();
73 frontier.insert(Bitboard::EMPTY.to_le_bytes(), (Bitboard::EMPTY, 1));
74
75 for _ply in 0..4 {
76 let mut next: HashMap<[u8; 16], (Bitboard, u64)> = HashMap::new();
77 for (bb, mult) in frontier.values() {
78 for mv in generate_legal_moves(bb) {
79 let child = apply_move(bb, &mv);
80 if has_winning_line(&child) {
81 continue;
82 }
83 let canon = SymmetryHandler::find_canonical(&child);
84 let key = canon.to_le_bytes();
85 let entry = next.entry(key).or_insert((canon, 0));
86 entry.1 += mult;
87 }
88 }
89 frontier = next;
90 }
91
92 let mut states: Vec<(Bitboard, u64)> = frontier
93 .into_values()
94 .filter(|(bb, _)| !generate_legal_moves(bb).is_empty())
95 .collect();
96 states.sort_by_key(|(bb, _)| bb.to_le_bytes());
97 states
98}
99
100fn main() {
101 let args = parse_args();
102
103 let started = Instant::now();
104 let mut states = enumerate_depth4();
105 println!(
106 "enumerated {} canonical nonterminal depth-4 states in {:.2}s",
107 states.len(),
108 started.elapsed().as_secs_f64()
109 );
110 if args.sample > 0 && args.sample < states.len() {
111 use rand::prelude::*;
112 let mut rng = StdRng::seed_from_u64(args.seed);
113 states.shuffle(&mut rng);
114 states.truncate(args.sample);
115 states.sort_by_key(|(bb, _)| bb.to_le_bytes());
116 println!(
117 "sampled {} of {} states (seed={})",
118 states.len(),
119 10946,
120 args.seed
121 );
122 } else if args.limit > 0 {
123 states.truncate(args.limit);
124 println!("limited to {} states for this run", states.len());
125 }
126
127 let book = OpeningBookDatabase::open(&OpeningBookConfig {
128 database_path: args.db.clone(),
129 ..Default::default()
130 })
131 .expect("open book");
132
133 let mut rows: Vec<serde_json::Value> = Vec::with_capacity(states.len());
134 let mut solved = 0usize;
135 let mut cutoff = 0usize;
136 let solve_started = Instant::now();
137
138 for (i, (bb, mult)) in states.iter().enumerate() {
139 let t0 = Instant::now();
140 let reference = solve_position_with_book(bb, args.budget_s, Some(&book));
141 let elapsed = t0.elapsed().as_secs_f64();
142 println!(
143 " [{}/{}] {} solved={} elapsed={:.3}s",
144 i + 1,
145 states.len(),
146 State::new(*bb).to_qfen(),
147 reference.is_some(),
148 elapsed
149 );
150
151 match &reference {
152 Some(r) => {
153 solved += 1;
154 rows.push(serde_json::json!({
155 "qfen": State::new(*bb).to_qfen(),
156 "multiplicity": mult,
157 "value": r["value"],
158 "nodes": r["nodes"],
159 "solve_time_s": elapsed,
160 "solver": r["solver"],
161 }));
162 }
163 None => {
164 cutoff += 1;
165 rows.push(serde_json::json!({
166 "qfen": State::new(*bb).to_qfen(),
167 "multiplicity": mult,
168 "value": null,
169 "nodes": null,
170 "solve_time_s": elapsed,
171 "solver": null,
172 }));
173 }
174 }
175
176 if (i + 1) % 25 == 0 || i + 1 == states.len() {
177 println!(
178 "progress: {}/{} solved={} cutoff={} elapsed_total={:.1}s",
179 i + 1,
180 states.len(),
181 solved,
182 cutoff,
183 solve_started.elapsed().as_secs_f64()
184 );
185 let partial = serde_json::json!({
189 "budget_s": args.budget_s,
190 "total_canonical_depth4_states": states.len(),
191 "processed": i + 1,
192 "solved": solved,
193 "cutoff": cutoff,
194 "positions": rows,
195 });
196 std::fs::write(&args.out, serde_json::to_string_pretty(&partial).unwrap())
197 .expect("write partial output");
198 }
199 }
200
201 println!(
202 "done: {} solved, {} cutoff, total solve wall time {:.1}s -> {}",
203 solved,
204 cutoff,
205 solve_started.elapsed().as_secs_f64(),
206 args.out
207 );
208}