pub struct MCTSEngine { /* private fields */ }Implementations§
Source§impl MCTSEngine
impl MCTSEngine
Sourcepub fn new(config: MCTSConfig) -> Self
pub fn new(config: MCTSConfig) -> Self
Examples found in repository?
71fn 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 // No legal moves: the side to move loses (see Global
83 // Constraints — this is a decisive result, never a draw).
84 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 // use_transposition_table MUST be false here: with it on (the
95 // engine's actual default), root moves that canonicalize to the
96 // same child are merged onto one shared node and reported under a
97 // single arbitrary move, silently dropping every other legal move
98 // that led there — worst exactly at shallow plies (the empty
99 // board's 64 legal moves collapse to 3), which every self-play
100 // game passes through. Verified in mcts.rs's
101 // root_move_visits_default_config_collapses_symmetric_root_moves
102 // test — this is not a hypothetical concern, it was caught by
103 // Opus review of the PR that added root_move_visits and is
104 // exactly what this exporter must avoid to produce a faithful
105 // per-legal-move policy target.
106 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}Sourcepub fn search(&mut self, bb: &Bitboard) -> Option<(Move, f64)>
pub fn search(&mut self, bb: &Bitboard) -> Option<(Move, f64)>
Run MCTS from the given bitboard and return
(best_move, win_probability_for_the_root_mover).
Examples found in repository?
71fn 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 // No legal moves: the side to move loses (see Global
83 // Constraints — this is a decisive result, never a draw).
84 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 // use_transposition_table MUST be false here: with it on (the
95 // engine's actual default), root moves that canonicalize to the
96 // same child are merged onto one shared node and reported under a
97 // single arbitrary move, silently dropping every other legal move
98 // that led there — worst exactly at shallow plies (the empty
99 // board's 64 legal moves collapse to 3), which every self-play
100 // game passes through. Verified in mcts.rs's
101 // root_move_visits_default_config_collapses_symmetric_root_moves
102 // test — this is not a hypothetical concern, it was caught by
103 // Opus review of the PR that added root_move_visits and is
104 // exactly what this exporter must avoid to produce a faithful
105 // per-legal-move policy target.
106 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}Sourcepub fn root_move_visits(&self) -> Vec<(Move, u32)>
pub fn root_move_visits(&self) -> Vec<(Move, u32)>
Visit-count distribution over the root’s legal moves from the most
recent search() call — the raw material for an AlphaZero-style
soft policy target (visits / total_visits per move), as opposed
to the single argmax move search() returns. Empty if search()
returned None (no legal moves) or hasn’t been called yet.
With use_transposition_table enabled (the default), this is NOT
one entry per legal move. Root moves that canonicalize to the same
child state are merged onto one shared node, reported under a single
arbitrary “first discovered” move — every other legal move that led
there is silently absent, not just uncounted. This is worst exactly
where self-play data collection needs it most: the empty board’s 64
legal first moves collapse to 3 entries (see
root_move_visits_default_config_collapses_symmetric_root_moves
below, and docs/benchmarks/quantik-game-tree-census-2026-07-13.md
for how orbit size — and therefore collapse severity — shrinks with
depth but is large at the shallow plies every game starts from). For
a faithful per-legal-move policy target, run search() with
use_transposition_table: false.
Examples found in repository?
71fn 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 // No legal moves: the side to move loses (see Global
83 // Constraints — this is a decisive result, never a draw).
84 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 // use_transposition_table MUST be false here: with it on (the
95 // engine's actual default), root moves that canonicalize to the
96 // same child are merged onto one shared node and reported under a
97 // single arbitrary move, silently dropping every other legal move
98 // that led there — worst exactly at shallow plies (the empty
99 // board's 64 legal moves collapse to 3), which every self-play
100 // game passes through. Verified in mcts.rs's
101 // root_move_visits_default_config_collapses_symmetric_root_moves
102 // test — this is not a hypothetical concern, it was caught by
103 // Opus review of the PR that added root_move_visits and is
104 // exactly what this exporter must avoid to produce a faithful
105 // per-legal-move policy target.
106 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}pub fn iterations_performed(&self) -> u32
pub fn nodes_created(&self) -> usize
Auto Trait Implementations§
impl Freeze for MCTSEngine
impl RefUnwindSafe for MCTSEngine
impl Send for MCTSEngine
impl Sync for MCTSEngine
impl Unpin for MCTSEngine
impl UnsafeUnpin for MCTSEngine
impl UnwindSafe for MCTSEngine
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read more