Skip to main content

MCTSEngine

Struct MCTSEngine 

Source
pub struct MCTSEngine { /* private fields */ }

Implementations§

Source§

impl MCTSEngine

Source

pub fn new(config: MCTSConfig) -> Self

Examples found in repository?
examples/selfplay_export.rs (lines 106-111)
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}
Source

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?
examples/selfplay_export.rs (line 112)
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}
Source

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?
examples/selfplay_export.rs (line 114)
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}
Source

pub fn iterations_performed(&self) -> u32

Source

pub fn nodes_created(&self) -> usize

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V