selfplay_export/
selfplay_export.rs1use quantik_core::bench::canonical::canonical_json;
24use quantik_core::bitboard::Bitboard;
25use quantik_core::game::{check_winner, current_player, has_winning_line, WinStatus};
26use quantik_core::mcts::{MCTSConfig, MCTSEngine};
27use quantik_core::moves::{apply_move, generate_legal_moves};
28use quantik_core::state::State;
29use serde_json::json;
30use std::io::Write;
31
32struct Args {
33 games: u32,
34 iterations: u32,
35 seed: u64,
36 out: String,
37}
38
39fn parse_args() -> Args {
40 let mut games = 100u32;
41 let mut iterations = 2000u32;
42 let mut seed = 20260713u64;
43 let mut out = "benchmarks/results/selfplay.jsonl".to_string();
44 let mut it = std::env::args().skip(1);
45 while let Some(flag) = it.next() {
46 match flag.as_str() {
47 "--games" => games = it.next().unwrap().parse().unwrap(),
48 "--iterations" => iterations = it.next().unwrap().parse().unwrap(),
49 "--seed" => seed = it.next().unwrap().parse().unwrap(),
50 "--out" => out = it.next().unwrap(),
51 other => panic!("unknown flag {other}"),
52 }
53 }
54 Args {
55 games,
56 iterations,
57 seed,
58 out,
59 }
60}
61
62struct PendingRow {
63 ply: u32,
64 qfen: String,
65 side_to_move: u8,
66 policy: Vec<(u8, u8, u32)>, }
68
69fn play_game(seed: u64, iterations: u32) -> (Vec<PendingRow>, WinStatus) {
72 let mut bb = Bitboard::EMPTY;
73 let mut rows = Vec::new();
74 let mut ply = 0u32;
75
76 loop {
77 if has_winning_line(&bb) {
78 return (rows, check_winner(&bb));
79 }
80 let legal = generate_legal_moves(&bb);
81 if legal.is_empty() {
82 let loser = current_player(&bb).unwrap();
85 let winner = if loser == 0 {
86 WinStatus::Player1Wins
87 } else {
88 WinStatus::Player0Wins
89 };
90 return (rows, winner);
91 }
92
93 let side_to_move = current_player(&bb).unwrap();
94 let mut engine = MCTSEngine::new(MCTSConfig {
107 max_iterations: iterations,
108 seed: Some(seed.wrapping_add(ply as u64)),
109 use_transposition_table: false,
110 ..Default::default()
111 });
112 let (best_move, _) = engine.search(&bb).expect("legal moves exist");
113 let policy: Vec<(u8, u8, u32)> = engine
114 .root_move_visits()
115 .into_iter()
116 .map(|(mv, visits)| (mv.shape, mv.position, visits))
117 .collect();
118
119 rows.push(PendingRow {
120 ply,
121 qfen: State::new(bb).to_qfen(),
122 side_to_move,
123 policy,
124 });
125
126 bb = apply_move(&bb, &best_move);
127 ply += 1;
128 }
129}
130
131fn main() {
132 let args = parse_args();
133 if let Some(parent) = std::path::Path::new(&args.out).parent() {
134 std::fs::create_dir_all(parent).expect("mkdir output dir");
135 }
136 let mut file = std::fs::File::create(&args.out).expect("create output file");
137
138 for game_id in 0..args.games {
139 let (rows, winner) = play_game(
140 args.seed.wrapping_add(game_id as u64 * 1000),
141 args.iterations,
142 );
143 assert_ne!(
144 winner,
145 WinStatus::NoWin,
146 "game must resolve to a decisive winner"
147 );
148
149 for row in rows {
150 let value = match (winner, row.side_to_move) {
151 (WinStatus::Player0Wins, 0) => 1.0,
152 (WinStatus::Player0Wins, 1) => -1.0,
153 (WinStatus::Player1Wins, 0) => -1.0,
154 (WinStatus::Player1Wins, 1) => 1.0,
155 _ => unreachable!("side_to_move is always 0 or 1, winner is always decisive"),
160 };
161 let policy_json: Vec<_> = row
162 .policy
163 .iter()
164 .map(|(shape, position, visits)| {
165 json!({"shape": shape, "position": position, "visits": visits})
166 })
167 .collect();
168 let record = json!({
169 "game_id": game_id,
170 "ply": row.ply,
171 "qfen": row.qfen,
172 "side_to_move": row.side_to_move,
173 "policy": policy_json,
174 "value": value,
175 });
176 writeln!(file, "{}", canonical_json(&record)).expect("write row");
177 }
178
179 if (game_id + 1) % 10 == 0 || game_id + 1 == args.games {
180 println!(
181 "{}/{} games exported -> {}",
182 game_id + 1,
183 args.games,
184 args.out
185 );
186 }
187 }
188}