simple_mcts/
mcts.rs

1//! Implementation of Monte Carlo Tree Search (MCTS) algorithm.
2//!
3//! This module provides the core MCTS logic, allowing for tree traversal,
4//! node expansion, simulation, and backpropagation. It is designed to be
5//! generic over game types and evaluation strategies, making it suitable
6//! for various board games.
7
8use core::f64;
9use std::sync::{atomic::{AtomicU8, Ordering}, Arc, Mutex};
10
11use crate::{Game, GameEvaluator, Node, NodeRef};
12
13/// A very large floating-point number used to represent infinity in score calculations.
14///
15/// This constant is used, for instance, to ensure unvisited nodes are prioritized
16/// during selection. Using `1e300` instead of `f64::MAX` can sometimes prevent
17/// potential overflow or precision issues when `INFINITY` is involved in
18/// arithmetic operations.
19const INFINITY : f64 = 1e300;
20
21/// Data stored in each node of the MCTS tree.
22///
23/// This struct holds the essential statistics and game-specific information
24/// for a node within the Monte Carlo Search Tree.
25///
26/// # Type Parameters
27/// - `N`: The number of possible actions in the game, fixed at compile time.
28struct MctsNodeData<const N: usize>{
29    /// Policy probabilities for each action from this node's state.
30    /// Typically obtained from a `GameEvaluator`.
31    policy: [f64; N],
32    /// A boolean mask indicating which actions are valid from this node's state.
33    /// `true` at index `i` means action `i` is valid.
34    mask: [bool; N],
35    /// The cumulative sum of scores obtained from simulations that have passed through this node.
36    /// This is used to calculate the average value of the node.
37    score: f64,
38    /// The number of times this node has been visited during simulations.
39    /// Incremented during backpropagation.
40    n: usize,
41    /// A flag indicating if the game state represented by this node is a terminal (finished) state.
42    /// If `true`, no further actions can be taken from this node.
43    finish: bool
44}
45
46impl<const N: usize> MctsNodeData<N>{
47    /// Creates a new `MctsNodeData` with default values.
48    ///
49    /// Policy is initialized to uniform probabilities, mask to false, score and visits to zero,
50    /// and finish flag to false.
51    ///
52    /// # Returns
53    /// A new `MctsNodeData` instance.
54    pub fn new() -> Self{
55        MctsNodeData { 
56            policy: [1./N as f64; N], 
57            mask: [false; N], 
58            score: 0.0, 
59            n: 0, 
60            finish: false 
61        }
62    }
63
64    /// Calculates the average value (score per visit) of this node.
65    ///
66    /// # Returns
67    /// The average score (`score / n`). Returns `0.0` if `n` (visit count) is zero
68    /// to prevent division by zero.
69    #[inline]
70    pub fn get_value(&self) -> f64{
71        if self.n != 0 { self.score / self.n as f64 } else { 0.0 }
72    }
73
74    /// Returns the visit count (`n`) for this node.
75    ///
76    /// # Returns
77    /// The number of times this node has been visited.
78    #[inline]
79    pub fn get_n(&self) -> usize{
80        self.n
81    }
82    
83    /// Gets the policy value (action probability) for a specific action index.
84    ///
85    /// # Parameters
86    /// - `index`: The action index (0 to N-1).
87    ///
88    /// # Returns
89    /// The policy probability for the given action.
90    #[inline]
91    pub fn get_policy(&self, index: usize) -> f64{
92        self.policy[index]
93    }
94
95    /// Checks if an action is valid from this node's state using the mask.
96    ///
97    /// # Parameters
98    /// - `index`: The action index to check.
99    ///
100    /// # Returns
101    /// `true` if the action is valid, `false` otherwise.
102    #[inline]
103    pub fn get_mask(&self, index: usize) -> bool{
104        self.mask[index]
105    }
106
107    /// Returns whether this node represents a finished game state.
108    ///
109    /// # Returns
110    /// `true` if the game is over at this node, `false` otherwise.
111    #[inline]
112    pub fn is_finish(&self) -> bool{
113        self.finish
114    }
115
116    /// Updates the node's statistics by incorporating a new simulation result.
117    ///
118    /// This method adds the `score` to the total `score` and increments the visit count `n`.
119    ///
120    /// # Parameters
121    /// - `score`: The result of a simulation to incorporate into this node's statistics.
122    #[inline]
123    pub fn add_score(&mut self, score: f64){
124        self.score += score;
125        self.n += 1;
126    }
127}
128
129/// Type alias for a `Node` containing `MctsNodeData`.
130type MctsNode<const N: usize> = Node<MctsNodeData<N>, N>;
131/// Type alias for a strong reference (`Rc<RefCell<...>>`) to an `MctsNode`.
132type MctsNodeRef<const N: usize> = NodeRef<MctsNodeData<N>, N>;
133
134/// Represents the current state of an MCTS (Monte Carlo Tree Search) instance,
135/// controlling the flow of operations and preventing invalid sequential calls.
136#[derive(Debug)]
137pub struct MctsState(pub AtomicU8);
138impl MctsState{
139    /// The MCTS instance is in a normal, ready-to-use state.
140    /// All normal operations can be performed.
141    pub const USABLE: u8 = 0;
142    /// The MCTS instance is awaiting the result of an external simulation.
143    /// Only `apply_simulation` can be called in this state.
144    pub const AWAITING_SIMULATION: u8 = 1;
145    /// The MCTS instance is temporarily locked during an internal operation
146    /// (e.g., selection, expansion, backpropagation).
147    /// No public methods should be called while in this state.
148    pub const LOCKED: u8 = 2;
149}
150
151/// Represents possible errors that can occur during MCTS (Monte Carlo Tree Search) operations.
152#[derive(Debug)]
153pub enum MctsError{
154    /// Indicates that an MCTS operation was attempted when the instance was not in the
155    /// required state (e.g., calling `apply_simulation` without `start_iteration`,
156    /// or calling any method while in `Locked` state).
157    InvalidState(u8),
158    /// Occurs when the number of provided evaluations does not match the expected count
159    /// (e.g., in `MctsBatch::apply_simulation`).
160    /// Contains (expected_count, received_count).
161    InvalidEvaluationCount(usize, usize),
162    //// Indicates that an MCTS search cannot proceed because the root node
163    /// already represents a finished game state. Further iterations or plays are
164    /// not possible.
165    SearchAlreadyOver,
166    /// Occurs when an action index provided is outside the valid range [0, N-1] for the game.
167    /// Contains the (attempted_action_index, max_action_index_N).
168    ActionOutOfRange(usize, usize),
169    /// Indicates that a chosen action is invalid according to the game's mask.
170    /// This means the game state does not allow this action.
171    /// Contains the invalid action index.
172    InvalidAction(usize),
173    /// Occurs when an action cannot be checked or played because the root
174    /// node is none, it has not yet been explored and added to the MCTS tree.
175    UnexploredAction
176}
177
178/// Type alias for a function pointer used to determine a node's selection score during MCTS.
179///
180/// This function takes the following parameters:
181/// - `value`: The current mean value of the node.
182/// - `policy`: The initial policy probability for the action leading to this node (from the parent's perspective).
183/// - `n_visits`: The number of times the node has been visited.
184/// - `parent_n_visits`: The number of times the parent node has been visited.
185/// - `exploration_coef`: The exploration coefficient from `MctsConfig`.
186///
187/// It returns an `f64` score used to rank nodes for selection.
188pub type SelectionFunction<const N: usize> = fn(value: f64, policy: f64, n_visits: f64, parent_n_visits: f64, exploration_coef: f64) -> f64;
189
190/// Configuration parameters for a single Monte Carlo Tree Search (MCTS) instance.
191///
192/// This struct allows customization of MCTS behavior, including the exploration-exploitation
193/// balance and the specific function used to calculate node selection scores.
194///
195/// # Type Parameters
196/// - `N`: The number of possible actions in the game.
197pub struct MctsConfig<const N: usize>{
198    /// The exploration coefficient (often denoted as C_p or C_u) used in the selection phase.
199    ///
200    /// A higher value encourages more exploration of less-visited nodes, while a lower value
201    /// prioritizes exploitation of known good paths.
202    pub exploration_coef: f64,
203    /// The function used to calculate the selection score for a child node during MCTS traversal.
204    ///
205    /// This function typically balances exploitation (based on value) and exploration (based on visits).
206    /// You can use provided functions like `ucb1` or `default_selection_score`, or define your own.
207    pub selection_function: SelectionFunction<N>
208}
209
210impl<const N: usize> MctsConfig<N>{
211    /// The default MCTS configuration.
212    ///
213    /// - `exploration_coef`: `std::f64::consts::SQRT_2` (approximately 1.414), a common choice for UCB1.
214    /// - `selection_function`: `default_selection_score`, the default formula.
215    pub const DEFAULT: MctsConfig<N> = MctsConfig{
216        exploration_coef: std::f64::consts::SQRT_2,
217        selection_function: default_selection_score::<N>
218    };
219}
220
221/// The Monte Carlo Tree Search algorithm implementation.
222///
223/// This struct manages the MCTS tree for a single game instance, allowing
224/// for iterative search, game progression, and result retrieval.
225///
226/// # Type Parameters
227/// - `T`: The game type that implements the `Game` trait.
228/// - `N`: The number of possible actions in the game, a constant generic.
229pub struct Mcts<T: Game<N>, const N: usize>{
230    game: T,
231    root: Option<MctsNodeRef<N>>,
232    coef: f64,
233    state: MctsState,
234    /// Stores intermediate state between start_iteration and apply_simulation
235    /// Contains: (game_state, node_to_simulate)
236    latent: Option<(T, MctsNodeRef<N>)>,
237    selection_function: SelectionFunction<N>
238}
239
240/// A selection function that combines value, visit count, and initial policy.
241///
242/// This function prioritizes unvisited nodes. For visited nodes, it balances exploitation
243/// (node's value) with an exploration term that incorporates the initial policy
244/// probability.
245///
246/// # Parameters
247/// - `value`: The mean value of the node.
248/// - `policy`: The initial policy probability for the action leading to this node.
249/// - `n_visits`: Number of visits to the current node.
250/// - `parent_n_visits`: Number of visits to the parent node.
251/// - `exploration_coef`: The exploration coefficient.
252///
253/// # Returns
254/// The calculated selection score for the node.
255pub fn default_selection_score<const N: usize>(value: f64, policy: f64, n_visits: f64, parent_n_visits: f64, exploration_coef: f64) -> f64{
256    value + exploration_coef * policy * parent_n_visits.sqrt() / (1.+n_visits)
257}
258
259/// The standard Upper Confidence Bound 1 (UCB1) selection function.
260///
261/// This function balances exploitation (current value) and exploration (unvisited nodes or less-visited nodes).
262///
263/// # Parameters
264/// - `value`: The mean value (exploitation term) of the node.
265/// - `policy`: This parameter is ignored in the standard UCB1 formula, but is present to match `SelectionFunction` signature.
266/// - `n_visits`: Number of visits to the current node.
267/// - `parent_n_visits`: Number of visits to the parent node.
268/// - `exploration_coef`: The exploration coefficient.
269///
270/// # Returns
271/// The UCB1 score for the node.
272pub fn ucb1<const N: usize>(value: f64, policy: f64, n_visits: f64, parent_n_visits: f64, exploration_coef: f64) -> f64{
273    value + exploration_coef * policy * (parent_n_visits.ln() / n_visits).sqrt()
274}
275
276impl<T: Game<N>, const N: usize> Mcts<T, N>{
277    /// Standard score representing a victory in the game (e.g., for the current player).
278    pub const VICTORY_SCORE: f64 = 1.0;
279    /// Standard score representing a defeat in the game (e.g., for the current player).
280    pub const DEFEAT_SCORE: f64 = -1.0;
281    /// Standard score representing a draw or tie in the game.
282    pub const EQUALITY_SCORE: f64 = 0.0;
283
284    /// Creates a new MCTS instance with the default configuration.
285    ///
286    /// The default configuration uses `MctsConfig::DEFAULT`, which includes a
287    /// standard `exploration_coef` and the `default_selection_score` selection function.
288    ///
289    /// # Returns
290    /// A new MCTS instance ready to start searching from a new game.
291    #[inline]
292    pub fn new() -> Self{
293        Self::from_config(&MctsConfig::DEFAULT)
294    }
295
296    /// Creates a new MCTS instance from a specified configuration.
297    ///
298    /// This allows users to customize the exploration coefficient and the selection
299    /// function used during the MCTS process.
300    ///
301    /// # Parameters
302    /// - `config`: The `MctsConfig` to use for this instance.
303    ///
304    /// # Returns
305    /// A new MCTS instance initialized with the given configuration, ready to start
306    /// searching from a new game.
307    #[inline]
308    pub fn from_config(config: &MctsConfig<N>) -> Self{
309        Self::from_game_with_config(T::new(), config)
310    }
311
312    /// Creates a new MCTS instance starting from an existing game state with the default configuration.
313    ///
314    /// This is useful when you want to continue a search from a specific point in a game
315    /// without custom MCTS parameters.
316    ///
317    /// # Parameters
318    /// - `game`: The initial game instance.
319    ///
320    /// # Returns
321    /// A new MCTS instance rooted at the given game state, using `MctsConfig::DEFAULT`.
322    #[inline]
323    pub fn from_game(game: T) -> Self{
324        Mcts::from_game_with_config(game, &MctsConfig::DEFAULT)
325    }
326
327    /// Creates a new MCTS instance starting from an existing game state with a custom configuration.
328    ///
329    /// This allows resuming a search from a specific game point with fine-tuned MCTS parameters.
330    ///
331    /// # Parameters
332    /// - `game`: The initial game instance.
333    /// - `config`: The `MctsConfig` to use for this instance.
334    ///
335    /// # Returns
336    /// A new MCTS instance rooted at the given game state, initialized with the provided configuration.
337    #[inline]
338    pub fn from_game_with_config(game: T, config: &MctsConfig<N>) -> Self{
339        Mcts { 
340            game: game, 
341            root: None, 
342            coef: config.exploration_coef, 
343            state: MctsState(AtomicU8::new(MctsState::USABLE)), 
344            latent: None, 
345            selection_function: config.selection_function
346        }
347    }
348
349    /// Gets an immutable reference to the underlying game instance.
350    ///
351    /// This allows inspection of the game state without modifying the MCTS tree.
352    ///
353    /// # Returns
354    /// A reference to the internal `Game` instance.
355    pub fn get_game(&self) -> &T{
356        &self.game
357    }
358
359    /// Returns the current operational state of the MCTS instance.
360    ///
361    /// This indicates whether the MCTS is ready for a new iteration, awaiting
362    /// simulation results, or temporarily locked.
363    ///
364    /// # Returns
365    /// A clone of the current `MctsState`.
366    #[inline]
367    pub fn get_state(&self) -> u8{
368        self.state.0.load(Ordering::SeqCst)
369    }
370
371    /// Calculates the UCB1 score for a child node during the selection phase.
372    ///
373    /// This private helper function computes the Upper Confidence Bound 1 (UCB1)
374    /// value for a specific child of a given parent node. It's used to balance
375    /// exploration and exploitation in MCTS tree traversal.
376    ///
377    /// # Parameters
378    /// - `node`: The parent `MctsNode` from which the child originates.
379    /// - `index`: The index of the child (representing an action) for which to calculate the score.
380    ///
381    /// # Returns
382    /// The calculated UCB1 score (`f64`). Returns `-INFINITY` if the child node
383    /// represents a finished game state, to avoid selecting it for further expansion.
384    /// Returns INFINITY ponderate by policy for unexplorated node for keep order.
385    #[inline]
386    fn get_selection_score(&self, node: &MctsNode<N>, index: usize) -> f64{
387        if let Some(child) = node.get_child(index){
388            let node_child = &*child.lock().unwrap();
389
390            if node_child.get().is_finish() { 
391                -INFINITY 
392            }
393            else{
394                (self.selection_function) (
395                    node_child.get().get_value(), 
396                    node.get().get_policy(index), 
397                    node_child.get().get_n() as f64, 
398                    node.get().get_n() as f64, 
399                    self.coef
400                )
401            }
402        }
403        else{
404            if node.get().get_mask(index) && !node.get().is_finish() { INFINITY * (1. + node.get().get_policy(index))} else { -INFINITY }
405        }
406    }
407
408    /// Performs the selection phase of MCTS
409    ///
410    /// # Returns
411    /// Tuple containing:
412    /// - The selected node
413    /// - The action taken to reach it
414    /// - The game state at that node
415    #[inline]
416    fn selection(&self) -> (Option<MctsNodeRef<N>>, usize, T){
417        let mut game: T = self.game.clone();
418
419        let mut node = match &self.root {
420            Some(root) => Arc::clone(root),
421            None => return (None, 0, game)
422        };
423
424        loop {
425            let scores : [f64; N] = std::array::from_fn(|index| self.get_selection_score(&node.lock().unwrap(), index));
426
427            let index = scores.iter().enumerate().max_by(|a, b| (a.1).total_cmp(b.1)).unwrap().0;
428            game.play(index);
429
430            if node.lock().unwrap().get_child(index).is_none() {
431                return (Some(node), index, game);
432            }
433
434            let next = node.lock().unwrap().get_child(index).unwrap();
435            node = next;
436        }
437    }
438
439    /// Performs the expansion phase of MCTS
440    ///
441    /// # Parameters
442    /// - `node`: The node to expand from
443    /// - `index`: The action to expand
444    ///
445    /// # Returns
446    /// The newly created child node
447    #[inline]
448    fn expansion(&mut self, node: &Option<MctsNodeRef<N>>, index: usize) -> MctsNodeRef<N>{
449        if let Some(node) = node{
450            Node::add_child(&node, index, MctsNodeData::new())
451        }
452        else{
453            let node_ref = Arc::new(Mutex::new(
454                MctsNode::new(None, MctsNodeData::new())
455            ));
456
457            self.root = Some(Arc::clone(&node_ref));
458            node_ref
459        }
460    }
461
462    /// Performs the simulation phase of MCTS
463    ///
464    /// # Parameters
465    /// - `node`: The node to simulate from
466    /// - `game`: The game state at that node
467    /// - `evaluator`: The policy/value evaluator
468    #[inline]
469    fn simulation(&mut self, node: &mut MctsNode<N>, game: &T, evaluator: &dyn GameEvaluator<T, N>){
470        let data = node.get_mut();
471
472        if let Some(score) = game.get_result(){
473            data.add_score(-score);
474            data.finish = true;
475        }
476        else{
477            let (score, policy) = evaluator.evaluate(game.get_state());
478
479            data.add_score(-score);
480            data.policy=policy;
481            data.mask=game.get_actions();
482        }
483    }
484
485    /// Performs simulation using precomputed evaluation data
486    ///
487    /// # Panics
488    /// If called when not in `AwaitingSimulation` state
489    ///
490    /// # Parameters
491    /// - `node`: The node to simulate from
492    /// - `game`: The game state at that node
493    /// - `evaluation`: The policy/value
494    #[inline]
495    fn simulation_from_data(&mut self, node: &mut MctsNode<N>, game: &T, evaluation: (f64, [f64; N])){
496        let data = node.get_mut();
497
498        if let Some(score) = game.get_result(){
499            data.add_score(-score);
500            data.finish = true;
501        }
502        else{
503            let (score, policy) = evaluation;
504
505            data.add_score(-score);
506            data.policy=policy;
507            data.mask=game.get_actions();
508        }
509    }
510
511    /// Performs the backpropagation phase of MCTS
512    ///
513    /// # Parameters
514    /// - `node_ref`: The node to start backpropagation from
515    #[inline]
516    fn backpropagation(&mut self, node_ref: &MctsNodeRef<N>){
517        let mut current_ref_opt: Option<Arc<Mutex<Node<MctsNodeData<N>, N>>>>;
518
519        let mut score: f64;
520        let mut finish: bool;
521
522        {
523            let node = &*node_ref.lock().unwrap();
524            score = node.get().get_value();
525            finish = node.get().is_finish();
526            current_ref_opt = node.get_parent();
527        }
528
529        while let Some(current_ref) = current_ref_opt {
530            score = -score;
531            
532            let current = &mut *current_ref.lock().unwrap();
533
534            if !finish{
535                current.get_mut().add_score(score);
536            }
537            else {
538                //the score is already inverse Defeat => Victory and Victory => Defeat
539                if score == -Self::VICTORY_SCORE {
540                    current.get_mut().score = current.get().n as f64 * score;
541                    current.get_mut().finish = true;
542                }
543                else{
544                    let mut max_score = f64::MIN;
545                    let current_is_finish = (0..N).all(|index|{
546                        !current.get().get_mask(index) || {
547                            if let Some(child_ref) = current.get_child(index){
548                                let child = &*child_ref.lock().unwrap();
549
550                                let value = child.get().get_value();
551                                if value > max_score {
552                                    max_score = value;
553                                }
554
555                                child.get().is_finish()
556                            }
557                            else{ false }
558                        }
559                    });
560
561                    if current_is_finish {
562                        current.get_mut().score = current.get().n as f64 * -max_score;
563                        current.get_mut().finish = true;
564                    }
565                    else{
566                        current.get_mut().add_score(score);
567                        finish = false;
568                    }
569                }
570            }
571            
572            current_ref_opt = current.get_parent();
573        }
574    }
575
576    /// Performs one full iteration of MCTS (selection, expansion, simulation, backpropagation).
577    ///
578    /// This method requires the MCTS instance to be in a `Usable` state.
579    ///
580    /// # Parameters
581    /// - `evaluator`: The policy/value evaluator to use.
582    ///
583    /// # Returns
584    /// `Ok(())` if the iteration completes successfully.
585    /// `Err(MctsError::InvalidState(_))` if the MCTS instance is not in the `Usable` state.
586    #[inline]
587    pub fn iterate(&mut self, evaluator: &dyn GameEvaluator<T, N>) -> Result<(), MctsError> {
588        match self.state.0.compare_exchange(MctsState::USABLE, MctsState::LOCKED, Ordering::SeqCst, Ordering::SeqCst){
589            Ok(_) => {}
590            Err(current_state) => { return Err(MctsError::InvalidState(current_state)); }
591        }
592
593        if let Some(root) = self.root.as_ref(){
594            if root.lock().unwrap().get().is_finish(){
595                self.state.0.store(MctsState::USABLE, Ordering::Relaxed);
596                return Ok(());
597            }
598        }
599
600        let (node, index, game) = self.selection();
601        let child_ref = self.expansion(&node, index);
602        self.simulation(&mut *child_ref.lock().unwrap(), &game, evaluator);
603        self.backpropagation(&child_ref);
604
605        self.state.0.store(MctsState::USABLE, Ordering::Relaxed);
606        Ok(())
607    }
608
609    /// Performs the first partial iteration of MCTS (selection and expansion),
610    /// returning the game state for external simulation.
611    ///
612    /// This method transitions the MCTS instance from `Usable` to `AwaitingSimulation` state.
613    ///
614    /// # Returns
615    /// `Ok(game_state)` containing the game state requiring evaluation.
616    /// `Err(MctsError::InvalidState(_))` if the MCTS instance is not in the `Usable` state.
617    /// `Err(MctsError::SearchAlreadyOver)` if the root node already represents a finished game.
618    #[inline]
619    pub fn start_iteration(&mut self) -> Result<T::State, MctsError>{
620        match self.state.0.compare_exchange(MctsState::USABLE, MctsState::LOCKED, Ordering::SeqCst, Ordering::SeqCst){
621            Ok(_) => {}
622            Err(current_state) => { return Err(MctsError::InvalidState(current_state)); }
623        }
624
625        if let Some(root) = self.root.as_ref(){
626            if root.lock().unwrap().get().is_finish(){
627                self.state.0.store(MctsState::USABLE, Ordering::Relaxed);
628                return Err(MctsError::SearchAlreadyOver)
629            }
630        }
631
632        let (node, index, game) = self.selection();
633        let child_ref = self.expansion(&node, index);
634
635        let game_state = game.get_state();
636
637        self.latent = Some((game, child_ref));
638        self.state.0.store(MctsState::AWAITING_SIMULATION, Ordering::Relaxed);
639
640        Ok(game_state)
641    }
642
643    /// Completes a partial MCTS iteration by applying an external simulation's evaluation
644    /// and performing backpropagation.
645    ///
646    /// This method transitions the MCTS instance from `AwaitingSimulation` back to `Usable` state.
647    ///
648    /// # Parameters
649    /// - `evaluation`: A tuple containing the estimated value (f64) and action probabilities ([f64; N])
650    ///                 from the external simulation.
651    ///
652    /// # Returns
653    /// `Ok(())` if the simulation is successfully applied and backpropagation completes.
654    /// `Err(MctsError::InvalidState(_))` if the MCTS instance is not in the `AwaitingSimulation` state.
655    #[inline]
656    pub fn apply_simulation(&mut self, evaluation : (f64, [f64; N])) -> Result<(), MctsError>{
657        match self.state.0.compare_exchange(MctsState::AWAITING_SIMULATION, MctsState::LOCKED, Ordering::SeqCst, Ordering::SeqCst){
658            Ok(_) => {}
659            Err(current_state) => { return Err(MctsError::InvalidState(current_state)); }
660        }
661
662        let (game, child_ref) = self.latent.take().unwrap();
663
664        self.simulation_from_data(&mut *child_ref.lock().unwrap(), &game, evaluation);
665        self.backpropagation(&child_ref);
666
667        self.state.0.store(MctsState::USABLE, Ordering::Relaxed);
668        Ok(())
669    }
670
671    /// Determines if the MCTS search has concluded, either because the game is finished
672    /// or due to other stopping criteria (though currently only checks for game finish).
673    ///
674    /// # Returns
675    /// `true` if the MCTS search is considered finished (e.g., game over at root), `false` otherwise.
676    #[inline]
677    pub fn is_finish(&self) -> bool{
678        if let Some(root) = &self.root{
679            root.lock().unwrap().get().is_finish()
680        }
681        else{ false }
682    }
683
684    /// Gets the current value estimate for the root node
685    #[inline]
686    pub fn get_score(&self) -> f64{
687        if let Some(root) = &self.root {
688            -root.lock().unwrap().get().get_value()
689        }
690        else{ Self::EQUALITY_SCORE }
691    }
692
693    /// Calculates action probabilities from the root node's statistics
694    ///
695    /// # Parameters
696    /// - `root`: The root node to calculate from
697    ///
698    /// # Returns
699    /// Array of action probabilities
700    #[inline]
701    fn statistics_from_root(root: &MctsNode<N>) -> [f64; N]{
702        /*
703            We project the values of the nodes from the interval ]-1; 1[ to ]-inf; +inf[
704            using the function ln((1+x)/(1-x)). To calculate probabilities, we use softmax,
705            so the logarithm simplifies and the function becomes (1+x)/(1-x).
706            The function ln((1+x)/(1-x)) is the reciprocal of tanh(x/2).
707        */
708
709        // 0 -> score = -inf
710        let scores: [f64; N] = std::array::from_fn(|index|{
711            if let Some(child_ref) = root.get_child(index){
712                let score = child_ref.lock().unwrap().get().get_value();
713
714                if score != 1.{ (score + 1.) / (1. - score) + f64::MIN_POSITIVE } else{ f64::MAX / N as f64 }
715            }
716            else if root.get().get_mask(index){ f64::MIN_POSITIVE }
717            else { 0.0 }
718        });
719
720        let total: f64 = scores.iter().sum();
721        let total: f64 = if total == 0.0 { f64::MIN_POSITIVE } else{ total };
722
723        let scores = scores.map(|x| x/total);
724
725        scores
726    }
727
728    /// Gets the current action probabilities from the root node
729    #[inline]
730    pub fn get_statistics(&self) -> [f64; N]{
731        if let Some(root_ref) = &self.root {
732            Self::statistics_from_root(&*root_ref.lock().unwrap())
733        }
734        else{
735            [1./N as f64; N]
736        }
737    }
738
739    /// Returns the final result of the MCTS search (best score and policy).
740    ///
741    /// This method is typically called when the search is considered complete
742    /// or when a decision needs to be made based on the current tree.
743    ///
744    /// # Returns
745    /// A tuple containing:
746    /// - The average value of the root node (`f64`).
747    /// - An array of action probabilities ([f64; N]), which is usually the policy
748    ///   from the root node adjusted by visit counts for robust decision making.
749    #[inline]
750    pub fn get_result(&self) -> (f64, [f64; N]){
751        if let Some(root_ref) = &self.root {
752            let root = &*root_ref.lock().unwrap();
753            (-root.get().get_value(), Self::statistics_from_root(root))
754        }
755        else{
756            (0.0, [1./N as f64; N])
757        }
758    }
759
760    /// Calculates the total number of visits across all nodes in the MCTS tree.
761    ///
762    /// This can be used as a metric for the extent of the search performed.
763    ///
764    /// # Returns
765    /// The sum of visit counts (`n`) of all nodes in the tree.
766    #[inline]
767    pub fn count_visit(&self) -> usize{
768        if let Some(root_ref) = &self.root{
769            let root = &*root_ref.lock().unwrap();
770            root.get().get_n()
771        }
772        else { 0 }
773    }
774
775    /// Moves the MCTS root to the specified child, effectively playing an action.
776    ///
777    /// This method prunes the tree, discarding all branches not descending from the chosen child.
778    /// The MCTS instance must be in a `Usable` state.
779    ///
780    /// # Parameters
781    /// - `action`: The index of the child (action) to play.
782    ///
783    /// # Returns
784    /// `Ok(())` if the root is successfully moved to the child corresponding to the action.
785    /// `Err(MctsError::InvalidState(_))` if the MCTS instance is not in the `Usable` state.
786    /// `Err(MctsError::ActionOutOfRange(action, N))` if the `action` index is out of bounds (>= N).
787    /// `Err(MctsError::InvalidAction(_))` if the action is invalid according to the game mask.
788    /// `Err(MctsError::UnexploredAction)` if the action cannot be check because root is null.
789    #[inline]
790    pub fn play(&mut self, action: usize) -> Result<(), MctsError>{
791        match self.state.0.compare_exchange(MctsState::USABLE, MctsState::LOCKED, Ordering::SeqCst, Ordering::SeqCst){
792            Ok(_) => {}
793            Err(current_state) => { return Err(MctsError::InvalidState(current_state)); }
794        }
795
796        if action >= N {
797            self.state.0.store(MctsState::USABLE, Ordering::Relaxed);
798            return Err(MctsError::ActionOutOfRange(action, N));
799        }
800
801        let new_root;
802
803        if let Some(root_ref) = &self.root{
804            let root = &*root_ref.lock().unwrap();
805
806            if let Some(child_ref) = root.get_child(action){
807                {
808                    let child = &mut *child_ref.lock().unwrap();
809                    child.detach();
810                }
811
812                new_root = Some(child_ref);
813            }
814            else if root.get().get_mask(action){
815                new_root = None;
816            }
817            else{
818                self.state.0.store(MctsState::USABLE, Ordering::Relaxed);
819                return Err(MctsError::InvalidAction(action));
820            }
821        }
822        else{
823            self.state.0.store(MctsState::USABLE, Ordering::Relaxed);
824            return Err(MctsError::UnexploredAction);
825        }
826
827        self.game.play(action);
828        self.root = new_root;
829
830        self.state.0.store(MctsState::USABLE, Ordering::Relaxed);
831        Ok(())
832    }
833}
834
835#[cfg(test)]
836mod tests {
837    use crate::{test_utils::{compare_array, GameEvaluatorTest, GameEvaluatorTest2, GameTest}, Game, GameEvaluator, Mcts, MctsError};
838
839    #[test]
840    fn test_selection_empty(){
841        let mcts = Mcts::<GameTest, 4>::new();
842        assert!(mcts.selection().0.is_none())
843    }
844
845    #[test]
846    fn test_expansion_empty(){
847        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
848        mcts.expansion(&None, 0);
849
850        assert!(mcts.root.is_some());
851    }
852
853    #[test]
854    fn test_selection_root(){
855        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
856        mcts.expansion(&None, 0);
857
858        let (node, _index, _game)  = mcts.selection();
859        let node = &*node.as_ref().unwrap().lock().unwrap();
860
861        assert!(node.is_root());
862
863        assert!(node.get_child(0).is_none());
864        assert!(node.get_child(1).is_none());
865        assert!(node.get_child(2).is_none());
866        assert!(node.get_child(3).is_none());
867    }
868
869    #[test] 
870    fn test_simulation_root(){
871        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
872        let evaluator: GameEvaluatorTest = GameEvaluatorTest::new();
873
874        let (node, index, game) = mcts.selection();
875        let child = mcts.expansion(&node, index);
876        let child = &mut *child.lock().unwrap();
877
878        mcts.simulation(child, &game, &evaluator);
879
880        assert!(child.is_root());
881        assert!(child.get_child(0).is_none());
882        assert!(child.get_child(1).is_none());
883        assert!(child.get_child(2).is_none());
884        assert!(child.get_child(3).is_none());
885
886        assert!(!child.get().is_finish());
887        assert_eq!(child.get().get_policy(0), 0.2);
888        assert_eq!(child.get().get_policy(1), 0.7);
889        assert_eq!(child.get().get_policy(2), 0.06);
890        assert_eq!(child.get().get_policy(3), 0.04);
891    }
892
893    #[test]
894    fn test_iteration_empty() -> Result<(), MctsError>{
895        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
896        let evaluator: GameEvaluatorTest = GameEvaluatorTest::new();
897
898        mcts.iterate(&evaluator)?;
899
900        {
901            let node_ref = mcts.root.as_ref().unwrap();
902            let node = &*node_ref.lock().unwrap();
903
904            assert!(node.is_root());
905            assert!(!node.get().is_finish());
906            assert_eq!(node.get().mask, [true, true, true, true]);
907            assert_eq!(node.get().policy, [0.2, 0.7, 0.06, 0.04]);
908            assert_eq!(node.get().score, 0.0);
909            assert_eq!(node.get().n, 1);
910        }
911
912        assert!(!mcts.is_finish());
913        Ok(())
914    }
915
916    #[test]
917    fn test_iteration_root_with_no_child() -> Result<(), MctsError>{
918        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
919        let evaluator: GameEvaluatorTest = GameEvaluatorTest::new();
920
921        mcts.iterate(&evaluator)?;
922        mcts.iterate(&evaluator)?;
923
924        {
925            let root_ref = mcts.root.as_ref().unwrap();
926            let root = &*root_ref.lock().unwrap();
927
928            assert_eq!(root.get().score, 0.2);
929            assert_eq!(root.get().n, 2);
930            assert!(root.get_child(0).is_none());
931            assert!(root.get_child(1).is_some());
932            assert!(root.get_child(2).is_none());
933            assert!(root.get_child(3).is_none());
934        }
935
936        mcts.iterate(&evaluator)?;
937        mcts.iterate(&evaluator)?;
938        mcts.iterate(&evaluator)?;
939
940        {
941            let root_ref = mcts.root.as_ref().unwrap();
942            let root = &*root_ref.lock().unwrap();
943
944            assert_eq!(root.get().score, 0.0);
945            assert_eq!(root.get().n, 5);
946            assert!(root.get_child(0).is_some());
947            assert!(root.get_child(1).is_some());
948            assert!(root.get_child(2).is_some());
949            assert!(root.get_child(3).is_some());
950        }
951
952        assert!(!mcts.is_finish());
953        Ok(())
954    }
955
956    #[test]
957    fn test_iteration_root_with_child() -> Result<(), MctsError>{
958        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
959        let evaluator: GameEvaluatorTest = GameEvaluatorTest::new();
960
961        for _ in 0..7{
962            mcts.iterate(&evaluator)?;
963        }
964
965        {
966            let root_ref = mcts.root.as_ref().unwrap();
967            let root = &*root_ref.lock().unwrap();
968
969            assert_eq!(root.get().score, 0.0);
970            assert_eq!(root.get().n, 7);
971        }
972
973        assert!(!mcts.is_finish());
974        Ok(())
975    }
976
977    #[test]
978    fn test_iteration_victory_1() -> Result<(), MctsError>{
979        let mut game: GameTest = GameTest::new();
980        game.play(3);
981        game.play(1);
982        game.play(2);
983
984        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::from_game(game);
985        let evaluator: GameEvaluatorTest = GameEvaluatorTest::new();
986
987        mcts.iterate(&evaluator)?;
988        mcts.iterate(&evaluator)?;
989
990        {
991            let root_ref = mcts.root.as_ref().unwrap();
992            let root = &*root_ref.lock().unwrap();
993
994            assert!(root.get().is_finish());
995            assert_eq!(root.get().get_value(), 1.0);
996        }
997
998        assert!(mcts.is_finish());
999        assert_eq!(mcts.get_result(), (-1.0, [1.0, 0., 0., 0.]));
1000        Ok(())
1001    }
1002
1003    #[test]
1004    fn test_iteration_victory_2() -> Result<(), MctsError>{
1005        let mut game: GameTest = GameTest::new();
1006        game.play(0);
1007        game.play(3);
1008        game.play(1);
1009
1010        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::from_game(game);
1011        let evaluator: GameEvaluatorTest = GameEvaluatorTest::new();
1012
1013        mcts.iterate(&evaluator)?;
1014        mcts.iterate(&evaluator)?;
1015
1016        {
1017            let root_ref = mcts.root.as_ref().unwrap();
1018            let root = &*root_ref.lock().unwrap();
1019
1020            assert!(root.get().is_finish());
1021            assert_eq!(root.get().get_value(), -1.0);
1022        }
1023
1024        assert!(mcts.is_finish());
1025        assert_eq!(mcts.get_result(), (1.0, [0., 0., 1., 0.]));
1026        Ok(())
1027    }
1028
1029    #[test]
1030    fn test_iteration_end_1() -> Result<(), MctsError>{
1031        let mut game: GameTest = GameTest::new();
1032        game.play(3);
1033        game.play(1);
1034
1035        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::from_game(game);
1036        let evaluator: GameEvaluatorTest2 = GameEvaluatorTest2::new();
1037
1038        for _ in 0..4{
1039            mcts.iterate(&evaluator)?;
1040        }
1041
1042        {
1043            let root_ref = mcts.root.as_ref().unwrap();
1044            let root = &*root_ref.lock().unwrap();
1045
1046            assert!(root.get().is_finish());
1047            assert_eq!(root.get().get_value(), -1.0);
1048        }
1049
1050        assert!(mcts.is_finish());
1051
1052        let result = mcts.get_result();
1053        assert_eq!(result.0, 1.0);
1054        assert!(compare_array(&result.1, &[0., 0., 1., 0.]));
1055        Ok(())
1056    }
1057
1058    #[test]
1059    fn test_iteration_total() -> Result<(), MctsError>{
1060        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
1061        let evaluator: GameEvaluatorTest2 = GameEvaluatorTest2::new();
1062
1063        for _ in 0..18{
1064            mcts.iterate(&evaluator)?;
1065        }
1066
1067        {
1068            let root_ref = mcts.root.as_ref().unwrap();
1069            let root = &*root_ref.lock().unwrap();
1070
1071            assert!(root.get().is_finish());
1072            assert_eq!(root.get().get_value(), -1.0);
1073        }
1074
1075        assert!(mcts.is_finish());
1076
1077        let result = mcts.get_result();
1078        assert_eq!(result.0, 1.0);
1079        assert!(compare_array(&result.1, &[0., 0., 0., 1.]));
1080        Ok(())
1081    }
1082
1083    #[test]
1084    fn test_iteration_total_2() -> Result<(), MctsError>{
1085        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
1086        let evaluator: GameEvaluatorTest2 = GameEvaluatorTest2::new();
1087
1088        for _ in 0..18{
1089            let game = mcts.start_iteration()?;
1090            mcts.apply_simulation(evaluator.evaluate(game))?;
1091        }
1092
1093        {
1094            let root_ref = mcts.root.as_ref().unwrap();
1095            let root = &*root_ref.lock().unwrap();
1096
1097            assert!(root.get().is_finish());
1098            assert_eq!(root.get().get_value(), -1.0);
1099        }
1100
1101        assert!(mcts.is_finish());
1102
1103        let result = mcts.get_result();
1104        assert_eq!(result.0, 1.0);
1105        assert!(compare_array(&result.1, &[0., 0., 0., 1.]));
1106        Ok(())
1107    }
1108
1109    #[test]
1110    fn test_play_and_count_visit() -> Result<(), MctsError>{
1111        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
1112        let evaluator: GameEvaluatorTest2 = GameEvaluatorTest2::new();
1113
1114        assert_eq!(mcts.count_visit(), 0);
1115
1116        for _ in 0..6{
1117            mcts.iterate(&evaluator)?;
1118        }
1119
1120        assert_eq!(mcts.count_visit(), 6);
1121        mcts.play(3)?;
1122        assert_eq!(mcts.count_visit(), 2);
1123
1124        assert!(mcts.root.unwrap().lock().unwrap().is_root());
1125        Ok(())
1126    }
1127
1128    #[test]
1129    fn test_play_empty() -> Result<(), MctsError>{
1130        let mut mcts: Mcts<GameTest, 4> = Mcts::<GameTest, 4>::new();
1131        let evaluator: GameEvaluatorTest2 = GameEvaluatorTest2::new();
1132
1133        mcts.iterate(&evaluator)?;
1134        mcts.play(3)?;
1135        Ok(())
1136    }
1137}