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}