From 6be9f950c25f0fe0f8b7ca93299107e92eae417d Mon Sep 17 00:00:00 2001 From: autocommit Date: Thu, 16 Apr 2026 15:22:11 -0700 Subject: [PATCH] =?UTF-8?q?test(simulator):=20=E2=9C=85=20Add=20comprehens?= =?UTF-8?q?ive=20test=20cases=20for=20AI=20models=20and=20edge=20cases=20i?= =?UTF-8?q?n=20the=20simulator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .../crates/mc-ai/tests/mcts_basic.rs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/simulator/crates/mc-ai/tests/mcts_basic.rs diff --git a/src/simulator/crates/mc-ai/tests/mcts_basic.rs b/src/simulator/crates/mc-ai/tests/mcts_basic.rs new file mode 100644 index 00000000..08b84bd7 --- /dev/null +++ b/src/simulator/crates/mc-ai/tests/mcts_basic.rs @@ -0,0 +1,78 @@ +//! Basic tree-growth tests for the MCTS scaffold. + +use mc_ai::mcts::XorShift64; +use mc_ai::mcts_tree::{Tree, TreeState}; + +/// Toy state: a non-negative counter with configurable branching and depth. +/// Legal actions = integers `0..branching` while `depth > 0`. Each action decrements depth. +#[derive(Clone, Debug)] +struct ToyState { + depth: u32, + branching: u32, +} + +impl TreeState for ToyState { + type Action = u32; + + fn legal_actions(&self) -> Vec { + if self.depth == 0 { + Vec::new() + } else { + (0..self.branching).collect() + } + } + + fn apply(&self, _action: &u32) -> Self { + Self { depth: self.depth - 1, branching: self.branching } + } +} + +#[test] +fn root_starts_with_all_actions_untried() { + let tree = Tree::new(ToyState { depth: 2, branching: 3 }); + assert_eq!(tree.root().untried.len(), 3); + assert_eq!(tree.root().children.len(), 0); + assert_eq!(tree.root().visits, 0); +} + +#[test] +fn expand_creates_child_and_shrinks_untried() { + let mut tree = Tree::new(ToyState { depth: 2, branching: 3 }); + let child = tree.expand(0).expect("root has untried actions"); + assert_eq!(tree.root().untried.len(), 2); + assert_eq!(tree.root().children, vec![child]); + assert_eq!(tree.nodes[child].parent, Some(0)); +} + +#[test] +fn backpropagate_updates_ancestors() { + let mut tree = Tree::new(ToyState { depth: 2, branching: 2 }); + let c1 = tree.expand(0).unwrap(); + let c2 = tree.expand(c1).unwrap(); + tree.backpropagate(c2, 1.0); + assert_eq!(tree.nodes[c2].visits, 1); + assert_eq!(tree.nodes[c1].visits, 1); + assert_eq!(tree.root().visits, 1); + assert!((tree.root().wins - 1.0).abs() < 1e-6); +} + +#[test] +fn iterate_grows_tree_and_accumulates_visits() { + let mut tree = Tree::new(ToyState { depth: 3, branching: 2 }); + let mut rng = XorShift64::new(7); + for _ in 0..32 { + tree.iterate(&mut rng); + } + assert_eq!(tree.root().visits, 32); + // Tree must have grown past the root. + assert!(tree.nodes.len() > 1); + // Stubbed simulate returns 0.5 → root total wins == 0.5 * visits. + assert!((tree.root().wins - 0.5 * 32.0).abs() < 1e-4); +} + +#[test] +fn terminal_state_has_no_legal_actions() { + let state = ToyState { depth: 0, branching: 4 }; + assert!(state.is_terminal()); + assert!(state.legal_actions().is_empty()); +}