From 7e192e78a2d921d69ddcd0f92fc3169dfff3d4fb Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Sat, 16 May 2026 21:46:37 +0200 Subject: [PATCH 01/13] Dodano dodatkowe rodzaje mcts (RAVE i Heavy Playouts) --- src/compute/src/agents.rs | 80 ++++++++++- src/compute/src/agents/mcts.rs | 126 +++++++++++++++--- src/compute/src/agents/mcts/metrics.rs | 11 ++ src/compute/src/agents/mcts/node.rs | 3 + src/compute/src/agents/metrics.rs | 11 ++ src/compute/src/bin/tournament.rs | 79 +++++++++++ src/compute/src/gui/views/gameplay.rs | 78 ++++++++++- .../configs/without_human/mcts_vs_mcts.toml | 15 +++ 8 files changed, 376 insertions(+), 27 deletions(-) create mode 100644 src/compute/src/resources/configs/without_human/mcts_vs_mcts.toml diff --git a/src/compute/src/agents.rs b/src/compute/src/agents.rs index 581e4cf..40ccab0 100644 --- a/src/compute/src/agents.rs +++ b/src/compute/src/agents.rs @@ -5,7 +5,9 @@ mod stats; pub use mcts::{MctsAgent, MctsStats, MctsStatsAccumulator}; pub use metrics::*; -pub use minimax::{HeuristicEvaluator, MinimaxAgent, MinimaxStats, MinimaxStatsAccumulator}; +pub use minimax::{ + HeuristicEvaluator, MinimaxAgent, MinimaxStats, MinimaxStatsAccumulator, PositionEvaluator, +}; pub use stats::{AgentRuntimeStats, AgentStatsAccumulator}; use serde::{Deserialize, Serialize}; @@ -38,6 +40,18 @@ fn default_max_time_ms() -> Option { fn default_exploration_constant() -> f64 { 1.41 } +fn default_use_heavy_playouts() -> bool { + false +} +fn default_heavy_playouts_epsilon() -> f64 { + 0.1 +} +fn default_use_rave() -> bool { + false +} +fn default_rave_k() -> f64 { + 1000.0 +} #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(tag = "type")] @@ -61,10 +75,36 @@ pub enum AgentConfig { Mcts { #[serde(default = "default_max_iterations")] max_iterations: u32, + #[serde(default = "default_max_time_ms")] max_time_ms: Option, + #[serde(default = "default_exploration_constant")] exploration_constant: f64, + + #[serde(default = "default_use_rave")] + use_rave: bool, + + #[serde(default = "default_rave_k")] + rave_k: f64, + + #[serde(default = "default_use_heavy_playouts")] + use_heavy_playouts: bool, + + #[serde(default = "default_heavy_playouts_epsilon")] + heavy_playouts_epsilon: f64, + + #[serde(default = "default_material")] + material_weight: i32, + + #[serde(default = "default_advancement")] + advancement_weight: i32, + + #[serde(default = "default_defended")] + defended_weight: i32, + + #[serde(default = "default_edge_penalty")] + edge_penalty_weight: i32, }, Human, } @@ -101,12 +141,38 @@ impl AgentConfig { max_iterations, max_time_ms, exploration_constant, - } => BreakthroughAgent::Mcts(MctsAgent::new( - *max_iterations, - *max_time_ms, - *exploration_constant, - seed, - )), + use_rave, + rave_k, + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, + } => { + let heuristic: Option> = if *use_heavy_playouts { + Some(Box::new(HeuristicEvaluator::new( + *material_weight, + *advancement_weight, + *defended_weight, + *edge_penalty_weight, + ))) + } else { + None + }; + + BreakthroughAgent::Mcts(MctsAgent::new( + *max_iterations, + *max_time_ms, + *exploration_constant, + *use_rave, + *rave_k, + *use_heavy_playouts, + heuristic, + *heavy_playouts_epsilon, + seed, + )) + } Self::Human => BreakthroughAgent::Human, } } diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index f497523..edc863e 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -11,6 +11,7 @@ use rand::rngs::SmallRng; use rand::{RngExt, SeedableRng, seq::SliceRandom}; use std::time::Instant; +use super::minimax::PositionEvaluator; use super::{Agent, AgentStats}; use crate::core::{Board, BoardConfig, Ply, Status}; @@ -19,6 +20,14 @@ pub struct MctsAgent { pub max_iterations: u32, pub max_time_ms: Option, pub exploration_constant: f64, + + pub use_rave: bool, + pub rave_k: f64, + + pub use_heavy_playouts: bool, + pub heavy_playouts_heuristic: Option>, + pub heavy_playouts_epsilon: f64, + pub rng: SmallRng, pub current_stats: MctsStats, } @@ -28,12 +37,22 @@ impl MctsAgent { max_iterations: u32, max_time_ms: Option, exploration_constant: f64, + use_rave: bool, + rave_k: f64, + use_heavy_playouts: bool, + heavy_playouts_heuristic: Option>, + heavy_playouts_epsilon: f64, seed: u64, ) -> Self { Self { max_iterations, max_time_ms, exploration_constant, + use_rave, + rave_k, + use_heavy_playouts, + heavy_playouts_heuristic, + heavy_playouts_epsilon, rng: SmallRng::seed_from_u64(seed), current_stats: MctsStats::default(), } @@ -89,25 +108,29 @@ impl Agent for MctsAgent { let c = self.exploration_constant; let mut best_child_idx = 0; let mut best_score = f64::NEG_INFINITY; + let rave_k = self.rave_k; for &child_idx in &tree[current_idx].children { let child = &tree[child_idx]; - let score = if child.visits == 0 { - f64::INFINITY - } else { - let exploitation = child.wins / (child.visits as f64); - let exploration = - c * ((parent_visits as f64).ln() / (child.visits as f64)).sqrt(); - exploitation + exploration - }; - if score == f64::INFINITY { + if child.visits == 0 { best_child_idx = child_idx; break; } - if score > best_score { - best_score = score; + let uct_exploitation = child.wins / (child.visits as f64); + let uct_exploration = c * ((parent_visits as f64).ln() / (child.visits as f64)).sqrt(); + + let final_score = if self.use_rave && child.amaf_visits > 0 { + let amaf_exploitation = child.amaf_wins / (child.amaf_visits as f64); + let beta = rave_k / (rave_k + child.visits as f64); + (1.0 - beta) * uct_exploitation + beta * amaf_exploitation + uct_exploration + } else { + uct_exploitation + uct_exploration + }; + + if final_score > best_score { + best_score = final_score; best_child_idx = child_idx; } } @@ -145,6 +168,10 @@ impl Agent for MctsAgent { // Phase 3: Playout let mut playout_steps = 0; + + let mut white_played_moves = [[false; 128]; 128]; + let mut black_played_moves = [[false; 128]; 128]; + while status == Status::Ongoing { let plies = sim_board.get_legal_plies(config); if plies.is_empty() { @@ -156,8 +183,43 @@ impl Agent for MctsAgent { }; break; } - let random_idx = self.rng.random_range(0..plies.len()); - sim_board.apply_ply(plies[random_idx]); + + let chosen_ply = if let Some(ref heuristic) = self.heavy_playouts_heuristic { + if self.rng.random_range(0.0..1.0) < self.heavy_playouts_epsilon { + plies[self.rng.random_range(0..plies.len())] + } else { + let mut best_ply = plies[0]; + let mut best_score = if sim_board.turn.is_white() { + i32::MIN + } else { + i32::MAX + }; + + for &ply in &plies { + let mut next_board = sim_board; + next_board.apply_ply(ply); + let score = heuristic.evaluate(&next_board, config); + + if (sim_board.turn.is_white() && score > best_score) + || (!sim_board.turn.is_white() && score < best_score) + { + best_score = score; + best_ply = ply; + } + } + best_ply + } + } else { + plies[self.rng.random_range(0..plies.len())] + }; + + if sim_board.turn.is_white() { + white_played_moves[chosen_ply.from as usize][chosen_ply.to as usize] = true; + } else { + black_played_moves[chosen_ply.from as usize][chosen_ply.to as usize] = true; + } + + sim_board.apply_ply(chosen_ply); status = sim_board.get_status(config); playout_steps += 1; } @@ -167,11 +229,13 @@ impl Agent for MctsAgent { let mut node_ptr = Some(current_idx); while let Some(idx) = node_ptr { - let node = &mut tree[idx]; - node.visits += 1; + let node_turn = tree[idx].turn; + let parent_idx = tree[idx].parent; + let children_indices = tree[idx].children.clone(); - let player_who_just_moved = node.turn.opponent(); + tree[idx].visits += 1; + let player_who_just_moved = node_turn.opponent(); let is_win = match status { Status::WhiteWon => player_who_just_moved.is_white(), Status::BlackWon => !player_who_just_moved.is_white(), @@ -179,10 +243,36 @@ impl Agent for MctsAgent { }; if is_win { - node.wins += 1.0; + tree[idx].wins += 1.0; + } + + if self.use_rave { + let played_moved = if node_turn.is_white() { + &white_played_moves + } else { + &black_played_moves + }; + + let turn_player_won = match status { + Status::WhiteWon => node_turn.is_white(), + Status::BlackWon => !node_turn.is_white(), + Status::Ongoing => false, + }; + + for &child_idx in &children_indices { + let child = &mut tree[child_idx]; + if let Some(ply) = child.ply_to_reach { + if played_moved[ply.from as usize][ply.to as usize] { + child.amaf_visits += 1; + if turn_player_won { + child.amaf_wins += 1.0; + } + } + } + } } - node_ptr = node.parent; + node_ptr = parent_idx; } self.current_stats.iterations += 1; diff --git a/src/compute/src/agents/mcts/metrics.rs b/src/compute/src/agents/mcts/metrics.rs index b747200..a382c6f 100644 --- a/src/compute/src/agents/mcts/metrics.rs +++ b/src/compute/src/agents/mcts/metrics.rs @@ -9,6 +9,17 @@ pub struct MctsMetrics { pub max_iterations: u32, pub max_time_ms: Option, pub exploration_constant: f64, + + pub use_rave: bool, + pub use_heavy_playouts: bool, + + pub rave_k: Option, + pub heavy_playouts_epsilon: Option, + pub material_weight: Option, + pub advancement_weight: Option, + pub defended_weight: Option, + pub edge_penalty_weight: Option, + pub total_iterations: u64, pub total_nodes_created: u64, } diff --git a/src/compute/src/agents/mcts/node.rs b/src/compute/src/agents/mcts/node.rs index 02bfdaf..7135a90 100644 --- a/src/compute/src/agents/mcts/node.rs +++ b/src/compute/src/agents/mcts/node.rs @@ -11,6 +11,9 @@ pub struct MctsNode { pub wins: f64, pub turn: Player, + + pub amaf_visits: u32, + pub amaf_wins: f64, } impl MctsNode { diff --git a/src/compute/src/agents/metrics.rs b/src/compute/src/agents/metrics.rs index 32a42ab..e403bde 100644 --- a/src/compute/src/agents/metrics.rs +++ b/src/compute/src/agents/metrics.rs @@ -94,6 +94,17 @@ pub struct MctsMetrics { pub max_iterations: u32, pub max_time_ms: Option, pub exploration_constant: f64, + + pub use_rave: bool, + pub use_heavy_playouts: bool, + + pub rave_k: Option, + pub heavy_playouts_epsilon: Option, + pub material_weight: Option, + pub advancement_weight: Option, + pub defended_weight: Option, + pub edge_penalty_weight: Option, + pub total_iterations: u64, pub total_nodes_created: u64, } diff --git a/src/compute/src/bin/tournament.rs b/src/compute/src/bin/tournament.rs index 0345d88..edb1cea 100644 --- a/src/compute/src/bin/tournament.rs +++ b/src/compute/src/bin/tournament.rs @@ -146,6 +146,14 @@ fn main() -> Result<()> { max_iterations, max_time_ms, exploration_constant, + use_rave, + rave_k, + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, } = config.white_player.clone() else { unreachable!(); @@ -155,6 +163,37 @@ fn main() -> Result<()> { max_iterations, max_time_ms, exploration_constant, + use_rave, + use_heavy_playouts, + + rave_k: if use_rave { Some(rave_k) } else { None }, + + heavy_playouts_epsilon: if use_heavy_playouts { + Some(heavy_playouts_epsilon) + } else { + None + }, + material_weight: if use_heavy_playouts { + Some(material_weight) + } else { + None + }, + advancement_weight: if use_heavy_playouts { + Some(advancement_weight) + } else { + None + }, + defended_weight: if use_heavy_playouts { + Some(defended_weight) + } else { + None + }, + edge_penalty_weight: if use_heavy_playouts { + Some(edge_penalty_weight) + } else { + None + }, + total_iterations: acc.total_iterations, total_nodes_created: acc.total_nodes_created, }; @@ -212,6 +251,14 @@ fn main() -> Result<()> { max_iterations, max_time_ms, exploration_constant, + use_rave, + rave_k, + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, } = config.black_player.clone() else { unreachable!(); @@ -221,6 +268,38 @@ fn main() -> Result<()> { max_iterations, max_time_ms, exploration_constant, + + use_rave, + use_heavy_playouts, + + rave_k: if use_rave { Some(rave_k) } else { None }, + + heavy_playouts_epsilon: if use_heavy_playouts { + Some(heavy_playouts_epsilon) + } else { + None + }, + material_weight: if use_heavy_playouts { + Some(material_weight) + } else { + None + }, + advancement_weight: if use_heavy_playouts { + Some(advancement_weight) + } else { + None + }, + defended_weight: if use_heavy_playouts { + Some(defended_weight) + } else { + None + }, + edge_penalty_weight: if use_heavy_playouts { + Some(edge_penalty_weight) + } else { + None + }, + total_iterations: acc.total_iterations, total_nodes_created: acc.total_nodes_created, }; diff --git a/src/compute/src/gui/views/gameplay.rs b/src/compute/src/gui/views/gameplay.rs index 63f22cf..ad7a9c6 100644 --- a/src/compute/src/gui/views/gameplay.rs +++ b/src/compute/src/gui/views/gameplay.rs @@ -567,7 +567,14 @@ impl GameplayView { max_iterations, max_time_ms, exploration_constant, - .. + use_rave, + rave_k, + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, } = self.config.white_player else { unreachable!(); @@ -577,6 +584,36 @@ impl GameplayView { max_iterations, max_time_ms, exploration_constant, + use_rave, + use_heavy_playouts, + + rave_k: if use_rave { Some(rave_k) } else { None }, + + heavy_playouts_epsilon: if use_heavy_playouts { + Some(heavy_playouts_epsilon) + } else { + None + }, + material_weight: if use_heavy_playouts { + Some(material_weight) + } else { + None + }, + advancement_weight: if use_heavy_playouts { + Some(advancement_weight) + } else { + None + }, + defended_weight: if use_heavy_playouts { + Some(defended_weight) + } else { + None + }, + edge_penalty_weight: if use_heavy_playouts { + Some(edge_penalty_weight) + } else { + None + }, total_iterations: acc.total_iterations, total_nodes_created: acc.total_nodes_created, }; @@ -634,7 +671,14 @@ impl GameplayView { max_iterations, max_time_ms, exploration_constant, - .. + use_rave, + rave_k, + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, } = self.config.black_player else { unreachable!(); @@ -644,6 +688,36 @@ impl GameplayView { max_iterations, max_time_ms, exploration_constant, + use_rave, + use_heavy_playouts, + + rave_k: if use_rave { Some(rave_k) } else { None }, + + heavy_playouts_epsilon: if use_heavy_playouts { + Some(heavy_playouts_epsilon) + } else { + None + }, + material_weight: if use_heavy_playouts { + Some(material_weight) + } else { + None + }, + advancement_weight: if use_heavy_playouts { + Some(advancement_weight) + } else { + None + }, + defended_weight: if use_heavy_playouts { + Some(defended_weight) + } else { + None + }, + edge_penalty_weight: if use_heavy_playouts { + Some(edge_penalty_weight) + } else { + None + }, total_iterations: acc.total_iterations, total_nodes_created: acc.total_nodes_created, }; diff --git a/src/compute/src/resources/configs/without_human/mcts_vs_mcts.toml b/src/compute/src/resources/configs/without_human/mcts_vs_mcts.toml new file mode 100644 index 0000000..0236a96 --- /dev/null +++ b/src/compute/src/resources/configs/without_human/mcts_vs_mcts.toml @@ -0,0 +1,15 @@ +board_width = 8 +board_height = 8 +seed = 42 + +[black_player] +type = "Mcts" +max_iterations = 50000 +exploration_constant = 1.41 +use_rave = true + +[white_player] +type = "Mcts" +max_iterations = 50000 +exploration_constant = 1.41 +use_heavy_playouts = true From 51eb4248543861c27e67ef6f0a8008d2bdc1deac Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Sat, 16 May 2026 22:22:19 +0200 Subject: [PATCH 02/13] =?UTF-8?q?Porozdzielane=20fazy=20algorytmu=20na=20r?= =?UTF-8?q?=C3=B3=C5=BCne=20funkcj=C4=99=20w=20celu=20lepszej=20czytelno?= =?UTF-8?q?=C5=9Bci.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/compute/src/agents/mcts.rs | 353 +++++++++++++++++++++------------ 1 file changed, 228 insertions(+), 125 deletions(-) diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index edc863e..e65285a 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -30,6 +30,10 @@ pub struct MctsAgent { pub rng: SmallRng, pub current_stats: MctsStats, + + // Configuration constants + check_interval: u32, + tree_capacity_time_mode: usize, } impl MctsAgent { @@ -55,13 +59,15 @@ impl MctsAgent { heavy_playouts_epsilon, rng: SmallRng::seed_from_u64(seed), current_stats: MctsStats::default(), + check_interval: 511, + tree_capacity_time_mode: 100_000, } } fn check_limits(&self, iterations: u32, start_time: Instant) -> bool { match self.max_time_ms { Some(max_time) => { - if (iterations & 511) == 0 { + if (iterations & self.check_interval) == 0 { (start_time.elapsed().as_millis() as u64) < max_time } else { true @@ -79,7 +85,7 @@ impl Agent for MctsAgent { let use_time = self.max_time_ms.is_some(); let capacity = if use_time { - 100_000 + self.tree_capacity_time_mode } else { self.max_iterations as usize + 1 }; @@ -96,98 +102,92 @@ impl Agent for MctsAgent { tree.push(MctsNode::new(None, None, root_plies, board.turn)); self.current_stats.nodes_created += 1; + // Main MCTS loop while self.check_limits(self.current_stats.iterations, start_time) { let mut current_idx = 0; let mut sim_board = *board; // Phase 1: Selection - while tree[current_idx].unexpanded_plies.is_empty() - && !tree[current_idx].children.is_empty() - { - let parent_visits = tree[current_idx].visits; - let c = self.exploration_constant; - let mut best_child_idx = 0; - let mut best_score = f64::NEG_INFINITY; - let rave_k = self.rave_k; - - for &child_idx in &tree[current_idx].children { - let child = &tree[child_idx]; - - if child.visits == 0 { - best_child_idx = child_idx; - break; - } + current_idx = self.select_node(&tree, &mut sim_board, config, current_idx); - let uct_exploitation = child.wins / (child.visits as f64); - let uct_exploration = c * ((parent_visits as f64).ln() / (child.visits as f64)).sqrt(); + // Phase 2: Expansion + let status = self.expand_node(&mut tree, current_idx, &mut sim_board, config); - let final_score = if self.use_rave && child.amaf_visits > 0 { - let amaf_exploitation = child.amaf_wins / (child.amaf_visits as f64); - let beta = rave_k / (rave_k + child.visits as f64); - (1.0 - beta) * uct_exploitation + beta * amaf_exploitation + uct_exploration - } else { - uct_exploitation + uct_exploration - }; + if status != Status::Ongoing { + self.backpropagate(&mut tree, current_idx, status, None); + self.current_stats.iterations += 1; + continue; + } - if final_score > best_score { - best_score = final_score; - best_child_idx = child_idx; - } - } + // Phase 3 & 4: Playout and Backpropagation + // Note: We create the strategy here to avoid borrow checker issues + let (_final_status, playout_steps, _move_tracking) = + self.do_playout_and_backprop(&mut tree, current_idx, &mut sim_board, config); - current_idx = best_child_idx; - if let Some(ply) = tree[current_idx].ply_to_reach { - sim_board.apply_ply(ply); - } - } + self.current_stats.playout_steps += playout_steps as u64; + self.current_stats.iterations += 1; + } - // Phase 2: Expansion - let mut status = sim_board.get_status(config); - - if status == Status::Ongoing && !tree[current_idx].unexpanded_plies.is_empty() { - let ply_to_expand = tree[current_idx].unexpanded_plies.pop().unwrap(); - sim_board.apply_ply(ply_to_expand); - - let mut next_plies = sim_board.get_legal_plies(config); - next_plies.shuffle(&mut self.rng); - let new_node = MctsNode::new( - Some(current_idx), - Some(ply_to_expand), - next_plies, - sim_board.turn, - ); - - let new_child_idx = tree.len(); - tree.push(new_node); - tree[current_idx].children.push(new_child_idx); - - current_idx = new_child_idx; - self.current_stats.nodes_created += 1; - status = sim_board.get_status(config); + // Select best move from root + let root = &tree[0]; + let mut best_ply = None; + let mut max_visits = 0; + + for &child_idx in &root.children { + let child = &tree[child_idx]; + if child.visits > max_visits { + max_visits = child.visits; + best_ply = child.ply_to_reach; } + } - // Phase 3: Playout - let mut playout_steps = 0; + best_ply + } - let mut white_played_moves = [[false; 128]; 128]; - let mut black_played_moves = [[false; 128]; 128]; + fn take_stats(&mut self) -> AgentStats { + let stats = std::mem::take(&mut self.current_stats); + AgentStats::Mcts(stats) + } +} - while status == Status::Ongoing { - let plies = sim_board.get_legal_plies(config); - if plies.is_empty() { - // Should not happen, but just in case - status = if sim_board.turn.is_white() { - Status::BlackWon - } else { - Status::WhiteWon - }; - break; - } +impl MctsAgent { + /// Combined phase 3 (Playout) and 4 (Backpropagation) + fn do_playout_and_backprop( + &mut self, + tree: &mut Vec, + current_idx: usize, + sim_board: &mut Board, + config: &BoardConfig, + ) -> (Status, u32, Option) { + // Phase 3: Playout - Play out a random/heuristic game + let mut playout_steps = 0; + let mut status = sim_board.get_status(config); + + let mut move_tracking = if self.use_rave { + Some(MoveTracking::new()) + } else { + None + }; - let chosen_ply = if let Some(ref heuristic) = self.heavy_playouts_heuristic { - if self.rng.random_range(0.0..1.0) < self.heavy_playouts_epsilon { + while status == Status::Ongoing { + let plies = sim_board.get_legal_plies(config); + if plies.is_empty() { + status = if sim_board.turn.is_white() { + Status::BlackWon + } else { + Status::WhiteWon + }; + break; + } + + let chosen_ply = if self.use_heavy_playouts { + if let Some(ref heuristic) = self.heavy_playouts_heuristic { + // Epsilon-greedy selection + let epsilon = self.heavy_playouts_epsilon; + if self.rng.random_range(0.0..1.0) < epsilon { plies[self.rng.random_range(0..plies.len())] } else { + // Greedy: select best move according to heuristic let mut best_ply = plies[0]; let mut best_score = if sim_board.turn.is_white() { i32::MIN @@ -196,7 +196,7 @@ impl Agent for MctsAgent { }; for &ply in &plies { - let mut next_board = sim_board; + let mut next_board = *sim_board; next_board.apply_ply(ply); let score = heuristic.evaluate(&next_board, config); @@ -211,48 +211,140 @@ impl Agent for MctsAgent { } } else { plies[self.rng.random_range(0..plies.len())] - }; - - if sim_board.turn.is_white() { - white_played_moves[chosen_ply.from as usize][chosen_ply.to as usize] = true; - } else { - black_played_moves[chosen_ply.from as usize][chosen_ply.to as usize] = true; } + } else { + plies[self.rng.random_range(0..plies.len())] + }; - sim_board.apply_ply(chosen_ply); - status = sim_board.get_status(config); - playout_steps += 1; + if let Some(ref mut tracking) = move_tracking { + tracking.record_move(sim_board.turn, chosen_ply); } - self.current_stats.playout_steps += playout_steps; - // Phase 4: Backpropagation - let mut node_ptr = Some(current_idx); + sim_board.apply_ply(chosen_ply); + status = sim_board.get_status(config); + playout_steps += 1; + } + + // Phase 4: Backpropagation - Update tree statistics + self.backpropagate(tree, current_idx, status, move_tracking.clone()); + + (status, playout_steps, move_tracking) + } - while let Some(idx) = node_ptr { - let node_turn = tree[idx].turn; - let parent_idx = tree[idx].parent; - let children_indices = tree[idx].children.clone(); + /// Phase 1: Selection + fn select_node( + &self, + tree: &[MctsNode], + sim_board: &mut Board, + _config: &BoardConfig, + mut current_idx: usize, + ) -> usize { + while tree[current_idx].unexpanded_plies.is_empty() + && !tree[current_idx].children.is_empty() + { + let parent_visits = tree[current_idx].visits; + let c = self.exploration_constant; + let mut best_child_idx = 0; + let mut best_score = f64::NEG_INFINITY; + let rave_k = self.rave_k; + + for &child_idx in &tree[current_idx].children { + let child = &tree[child_idx]; + + if child.visits == 0 { + best_child_idx = child_idx; + break; + } - tree[idx].visits += 1; + let uct_exploitation = child.wins / (child.visits as f64); + let uct_exploration = + c * ((parent_visits as f64).ln() / (child.visits as f64)).sqrt(); - let player_who_just_moved = node_turn.opponent(); - let is_win = match status { - Status::WhiteWon => player_who_just_moved.is_white(), - Status::BlackWon => !player_who_just_moved.is_white(), - Status::Ongoing => false, + let final_score = if self.use_rave && child.amaf_visits > 0 { + let amaf_exploitation = child.amaf_wins / (child.amaf_visits as f64); + let beta = rave_k / (rave_k + child.visits as f64); + (1.0 - beta) * uct_exploitation + beta * amaf_exploitation + uct_exploration + } else { + uct_exploitation + uct_exploration }; - if is_win { - tree[idx].wins += 1.0; + if final_score > best_score { + best_score = final_score; + best_child_idx = child_idx; } + } - if self.use_rave { - let played_moved = if node_turn.is_white() { - &white_played_moves - } else { - &black_played_moves - }; + current_idx = best_child_idx; + if let Some(ply) = tree[current_idx].ply_to_reach { + sim_board.apply_ply(ply); + } + } + + current_idx + } + /// Phase 2: Expansion + fn expand_node( + &mut self, + tree: &mut Vec, + current_idx: usize, + sim_board: &mut Board, + config: &BoardConfig, + ) -> Status { + let mut status = sim_board.get_status(config); + + if status == Status::Ongoing && !tree[current_idx].unexpanded_plies.is_empty() { + let ply_to_expand = tree[current_idx].unexpanded_plies.pop().unwrap(); + sim_board.apply_ply(ply_to_expand); + + let mut next_plies = sim_board.get_legal_plies(config); + next_plies.shuffle(&mut self.rng); + let new_node = MctsNode::new( + Some(current_idx), + Some(ply_to_expand), + next_plies, + sim_board.turn, + ); + + let new_child_idx = tree.len(); + tree.push(new_node); + tree[current_idx].children.push(new_child_idx); + + self.current_stats.nodes_created += 1; + status = sim_board.get_status(config); + } + + status + } + + /// Phase 4: Backpropagation + fn backpropagate( + &mut self, + tree: &mut [MctsNode], + mut leaf_idx: usize, + status: Status, + move_tracking: Option, + ) { + while leaf_idx != usize::MAX { + let node_turn = tree[leaf_idx].turn; + let parent_idx = tree[leaf_idx].parent; + let children_indices = tree[leaf_idx].children.clone(); + + tree[leaf_idx].visits += 1; + + let player_who_just_moved = node_turn.opponent(); + let is_win = match status { + Status::WhiteWon => player_who_just_moved.is_white(), + Status::BlackWon => !player_who_just_moved.is_white(), + Status::Ongoing => false, + }; + + if is_win { + tree[leaf_idx].wins += 1.0; + } + + if self.use_rave { + if let Some(ref tracking) = move_tracking { let turn_player_won = match status { Status::WhiteWon => node_turn.is_white(), Status::BlackWon => !node_turn.is_white(), @@ -262,7 +354,7 @@ impl Agent for MctsAgent { for &child_idx in &children_indices { let child = &mut tree[child_idx]; if let Some(ply) = child.ply_to_reach { - if played_moved[ply.from as usize][ply.to as usize] { + if tracking.contains_move(node_turn, ply) { child.amaf_visits += 1; if turn_player_won { child.amaf_wins += 1.0; @@ -271,30 +363,41 @@ impl Agent for MctsAgent { } } } - - node_ptr = parent_idx; } - self.current_stats.iterations += 1; + leaf_idx = parent_idx.unwrap_or(usize::MAX); } + } +} - let root = &tree[0]; - let mut best_ply = None; - let mut max_visits = 0; +/// Helper struct for tracking moves during playout (used for RAVE) +#[derive(Clone)] +struct MoveTracking { + white_moves: [[bool; 128]; 128], + black_moves: [[bool; 128]; 128], +} - for &child_idx in &root.children { - let child = &tree[child_idx]; - if child.visits > max_visits { - max_visits = child.visits; - best_ply = child.ply_to_reach; - } +impl MoveTracking { + fn new() -> Self { + Self { + white_moves: [[false; 128]; 128], + black_moves: [[false; 128]; 128], } + } - best_ply + fn record_move(&mut self, player: crate::core::Player, ply: Ply) { + if player.is_white() { + self.white_moves[ply.from as usize][ply.to as usize] = true; + } else { + self.black_moves[ply.from as usize][ply.to as usize] = true; + } } - fn take_stats(&mut self) -> AgentStats { - let stats = std::mem::take(&mut self.current_stats); - AgentStats::Mcts(stats) + fn contains_move(&self, player: crate::core::Player, ply: Ply) -> bool { + if player.is_white() { + self.white_moves[ply.from as usize][ply.to as usize] + } else { + self.black_moves[ply.from as usize][ply.to as usize] + } } } From f09ef4e466bac6ddf7cf7c8365299ce08632f3b9 Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Sat, 16 May 2026 22:43:44 +0200 Subject: [PATCH 03/13] Dodany wzorzec strategii --- src/compute/src/agents.rs | 27 ++++-- src/compute/src/agents/mcts.rs | 95 +++++-------------- .../src/agents/mcts/playout_strategy.rs | 61 ++++++++++++ .../src/agents/mcts/selection_strategy.rs | 54 +++++++++++ 4 files changed, 157 insertions(+), 80 deletions(-) create mode 100644 src/compute/src/agents/mcts/playout_strategy.rs create mode 100644 src/compute/src/agents/mcts/selection_strategy.rs diff --git a/src/compute/src/agents.rs b/src/compute/src/agents.rs index 40ccab0..6311877 100644 --- a/src/compute/src/agents.rs +++ b/src/compute/src/agents.rs @@ -3,7 +3,7 @@ mod metrics; mod minimax; mod stats; -pub use mcts::{MctsAgent, MctsStats, MctsStatsAccumulator}; +pub use mcts::{MctsAgent, MctsStats, MctsStatsAccumulator, SelectionStrategy, PlayoutStrategy}; pub use metrics::*; pub use minimax::{ HeuristicEvaluator, MinimaxAgent, MinimaxStats, MinimaxStatsAccumulator, PositionEvaluator, @@ -150,26 +150,33 @@ impl AgentConfig { defended_weight, edge_penalty_weight, } => { - let heuristic: Option> = if *use_heavy_playouts { - Some(Box::new(HeuristicEvaluator::new( + let selection_strategy = if *use_rave { + SelectionStrategy::Rave { k: *rave_k } + } else { + SelectionStrategy::Ucb1 + }; + + let playout_strategy = if *use_heavy_playouts { + let evaluator = Box::new(HeuristicEvaluator::new( *material_weight, *advancement_weight, *defended_weight, *edge_penalty_weight, - ))) + )); + PlayoutStrategy::Heavy { + evaluator, + epsilon: *heavy_playouts_epsilon, + } } else { - None + PlayoutStrategy::Random }; BreakthroughAgent::Mcts(MctsAgent::new( *max_iterations, *max_time_ms, *exploration_constant, - *use_rave, - *rave_k, - *use_heavy_playouts, - heuristic, - *heavy_playouts_epsilon, + selection_strategy, + playout_strategy, seed, )) } diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index e65285a..ed44716 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -3,15 +3,18 @@ mod metrics; pub mod node; mod stats; +mod selection_strategy; +mod playout_strategy; use node::MctsNode; pub use stats::{MctsStats, MctsStatsAccumulator}; +pub use selection_strategy::SelectionStrategy; +pub use playout_strategy::PlayoutStrategy; use rand::rngs::SmallRng; -use rand::{RngExt, SeedableRng, seq::SliceRandom}; +use rand::{SeedableRng, seq::SliceRandom}; use std::time::Instant; -use super::minimax::PositionEvaluator; use super::{Agent, AgentStats}; use crate::core::{Board, BoardConfig, Ply, Status}; @@ -21,12 +24,8 @@ pub struct MctsAgent { pub max_time_ms: Option, pub exploration_constant: f64, - pub use_rave: bool, - pub rave_k: f64, - - pub use_heavy_playouts: bool, - pub heavy_playouts_heuristic: Option>, - pub heavy_playouts_epsilon: f64, + pub selection_strategy: SelectionStrategy, + pub playout_strategy: PlayoutStrategy, pub rng: SmallRng, pub current_stats: MctsStats, @@ -41,22 +40,16 @@ impl MctsAgent { max_iterations: u32, max_time_ms: Option, exploration_constant: f64, - use_rave: bool, - rave_k: f64, - use_heavy_playouts: bool, - heavy_playouts_heuristic: Option>, - heavy_playouts_epsilon: f64, + selection_strategy: SelectionStrategy, + playout_strategy: PlayoutStrategy, seed: u64, ) -> Self { Self { max_iterations, max_time_ms, exploration_constant, - use_rave, - rave_k, - use_heavy_playouts, - heavy_playouts_heuristic, - heavy_playouts_epsilon, + selection_strategy, + playout_strategy, rng: SmallRng::seed_from_u64(seed), current_stats: MctsStats::default(), check_interval: 511, @@ -163,7 +156,7 @@ impl MctsAgent { let mut playout_steps = 0; let mut status = sim_board.get_status(config); - let mut move_tracking = if self.use_rave { + let mut move_tracking = if self.selection_strategy.uses_amaf() { Some(MoveTracking::new()) } else { None @@ -180,41 +173,7 @@ impl MctsAgent { break; } - let chosen_ply = if self.use_heavy_playouts { - if let Some(ref heuristic) = self.heavy_playouts_heuristic { - // Epsilon-greedy selection - let epsilon = self.heavy_playouts_epsilon; - if self.rng.random_range(0.0..1.0) < epsilon { - plies[self.rng.random_range(0..plies.len())] - } else { - // Greedy: select best move according to heuristic - let mut best_ply = plies[0]; - let mut best_score = if sim_board.turn.is_white() { - i32::MIN - } else { - i32::MAX - }; - - for &ply in &plies { - let mut next_board = *sim_board; - next_board.apply_ply(ply); - let score = heuristic.evaluate(&next_board, config); - - if (sim_board.turn.is_white() && score > best_score) - || (!sim_board.turn.is_white() && score < best_score) - { - best_score = score; - best_ply = ply; - } - } - best_ply - } - } else { - plies[self.rng.random_range(0..plies.len())] - } - } else { - plies[self.rng.random_range(0..plies.len())] - }; + let chosen_ply = self.playout_strategy.select_move(&plies, sim_board, config, &mut self.rng); if let Some(ref mut tracking) = move_tracking { tracking.record_move(sim_board.turn, chosen_ply); @@ -246,7 +205,6 @@ impl MctsAgent { let c = self.exploration_constant; let mut best_child_idx = 0; let mut best_score = f64::NEG_INFINITY; - let rave_k = self.rave_k; for &child_idx in &tree[current_idx].children { let child = &tree[child_idx]; @@ -256,20 +214,17 @@ impl MctsAgent { break; } - let uct_exploitation = child.wins / (child.visits as f64); - let uct_exploration = - c * ((parent_visits as f64).ln() / (child.visits as f64)).sqrt(); - - let final_score = if self.use_rave && child.amaf_visits > 0 { - let amaf_exploitation = child.amaf_wins / (child.amaf_visits as f64); - let beta = rave_k / (rave_k + child.visits as f64); - (1.0 - beta) * uct_exploitation + beta * amaf_exploitation + uct_exploration - } else { - uct_exploitation + uct_exploration - }; - - if final_score > best_score { - best_score = final_score; + let score = self.selection_strategy.compute_uct_score( + child.visits, + child.wins, + child.amaf_visits, + child.amaf_wins, + parent_visits, + c, + ); + + if score > best_score { + best_score = score; best_child_idx = child_idx; } } @@ -343,7 +298,7 @@ impl MctsAgent { tree[leaf_idx].wins += 1.0; } - if self.use_rave { + if self.selection_strategy.uses_amaf() { if let Some(ref tracking) = move_tracking { let turn_player_won = match status { Status::WhiteWon => node_turn.is_white(), diff --git a/src/compute/src/agents/mcts/playout_strategy.rs b/src/compute/src/agents/mcts/playout_strategy.rs new file mode 100644 index 0000000..b26d8aa --- /dev/null +++ b/src/compute/src/agents/mcts/playout_strategy.rs @@ -0,0 +1,61 @@ +use rand::rngs::SmallRng; +use rand::RngExt; + +use crate::core::{Board, BoardConfig, Ply}; +use super::super::minimax::PositionEvaluator; + +/// Strategy for selecting moves during the playout phase +#[derive(Debug)] +pub enum PlayoutStrategy { + /// Uniformly random move selection + Random, + /// Heuristic-guided move selection with epsilon-greedy exploration + Heavy { + evaluator: Box, + epsilon: f64, + }, +} + +impl PlayoutStrategy { + /// Select a move according to this strategy + pub fn select_move( + &self, + plies: &[Ply], + board: &Board, + config: &BoardConfig, + rng: &mut SmallRng, + ) -> Ply { + match self { + PlayoutStrategy::Random => plies[rng.random_range(0..plies.len())], + PlayoutStrategy::Heavy { evaluator, epsilon } => { + // Epsilon-greedy: random with probability epsilon, greedy otherwise + if rng.random_range(0.0..1.0) < *epsilon { + return plies[rng.random_range(0..plies.len())]; + } + + // Greedy: select best move according to heuristic + let mut best_ply = plies[0]; + let mut best_score = if board.turn.is_white() { + i32::MIN + } else { + i32::MAX + }; + + for &ply in plies { + let mut next_board = *board; + next_board.apply_ply(ply); + let score = evaluator.evaluate(&next_board, config); + + if (board.turn.is_white() && score > best_score) + || (!board.turn.is_white() && score < best_score) + { + best_score = score; + best_ply = ply; + } + } + + best_ply + } + } + } +} diff --git a/src/compute/src/agents/mcts/selection_strategy.rs b/src/compute/src/agents/mcts/selection_strategy.rs new file mode 100644 index 0000000..e3a11ba --- /dev/null +++ b/src/compute/src/agents/mcts/selection_strategy.rs @@ -0,0 +1,54 @@ +/// Selection strategy for the tree search phase +#[derive(Debug, Clone)] +pub enum SelectionStrategy { + /// UCB1 (Upper Confidence Bound) - vanilla MCTS selection + Ucb1, + /// RAVE (Rapid Action Value Estimation) - uses AMAF (all-moves-as-first heuristic) + Rave { k: f64 }, +} + +impl SelectionStrategy { + /// Computes the UCT score for a child node + /// + /// # Arguments + /// * `child_visits` - Number of times this child has been visited + /// * `child_wins` - Total wins accumulated at this child + /// * `child_amaf_visits` - AMAF visits (only used for RAVE) + /// * `child_amaf_wins` - AMAF wins (only used for RAVE) + /// * `parent_visits` - Number of times the parent has been visited + /// * `exploration_constant` - UCB1 exploration parameter (typically ~1.41) + pub fn compute_uct_score( + &self, + child_visits: u32, + child_wins: f64, + child_amaf_visits: u32, + child_amaf_wins: f64, + parent_visits: u32, + exploration_constant: f64, + ) -> f64 { + if child_visits == 0 { + return f64::INFINITY; // Unvisited nodes have highest priority + } + + let exploitation = child_wins / (child_visits as f64); + let exploration = exploration_constant * ((parent_visits as f64).ln() / (child_visits as f64)).sqrt(); + + match self { + SelectionStrategy::Ucb1 => exploitation + exploration, + SelectionStrategy::Rave { k } => { + if child_amaf_visits > 0 { + let amaf_exploitation = child_amaf_wins / (child_amaf_visits as f64); + let beta = k / (k + child_visits as f64); + (1.0 - beta) * exploitation + beta * amaf_exploitation + exploration + } else { + exploitation + exploration + } + } + } + } + + /// Returns true if this strategy uses AMAF tracking + pub fn uses_amaf(&self) -> bool { + matches!(self, SelectionStrategy::Rave { .. }) + } +} From dec21dc1311a31416c00a53de63f4d01fa442332 Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Sun, 17 May 2026 19:25:46 +0200 Subject: [PATCH 04/13] =?UTF-8?q?Usuni=C4=99ty=20niepotrzebny=20komentarz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/compute/src/agents/mcts.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index ed44716..765aa82 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -113,7 +113,6 @@ impl Agent for MctsAgent { } // Phase 3 & 4: Playout and Backpropagation - // Note: We create the strategy here to avoid borrow checker issues let (_final_status, playout_steps, _move_tracking) = self.do_playout_and_backprop(&mut tree, current_idx, &mut sim_board, config); From e15aa9edc39d839cd889646940a193d12e85ba40 Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Sun, 17 May 2026 23:25:11 +0200 Subject: [PATCH 05/13] Poprawiona ekspansja oraz ulepszone aktualizowanie statystyk amaf --- src/compute/src/agents/mcts.rs | 113 ++++++++++++++++++++++++++++----- 1 file changed, 97 insertions(+), 16 deletions(-) diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index 765aa82..b2b96fe 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -2,14 +2,14 @@ mod metrics; pub mod node; -mod stats; -mod selection_strategy; mod playout_strategy; +mod selection_strategy; +mod stats; use node::MctsNode; -pub use stats::{MctsStats, MctsStatsAccumulator}; -pub use selection_strategy::SelectionStrategy; pub use playout_strategy::PlayoutStrategy; +pub use selection_strategy::SelectionStrategy; +pub use stats::{MctsStats, MctsStatsAccumulator}; use rand::rngs::SmallRng; use rand::{SeedableRng, seq::SliceRandom}; @@ -104,17 +104,19 @@ impl Agent for MctsAgent { current_idx = self.select_node(&tree, &mut sim_board, config, current_idx); // Phase 2: Expansion - let status = self.expand_node(&mut tree, current_idx, &mut sim_board, config); + let (leaf_idx, status) = + self.expand_node(&mut tree, current_idx, &mut sim_board, config); if status != Status::Ongoing { - self.backpropagate(&mut tree, current_idx, status, None); + let move_tracking = self.initial_move_tracking(); + self.backpropagate(&mut tree, leaf_idx, status, move_tracking); self.current_stats.iterations += 1; continue; } // Phase 3 & 4: Playout and Backpropagation let (_final_status, playout_steps, _move_tracking) = - self.do_playout_and_backprop(&mut tree, current_idx, &mut sim_board, config); + self.do_playout_and_backprop(&mut tree, leaf_idx, &mut sim_board, config); self.current_stats.playout_steps += playout_steps as u64; self.current_stats.iterations += 1; @@ -155,11 +157,7 @@ impl MctsAgent { let mut playout_steps = 0; let mut status = sim_board.get_status(config); - let mut move_tracking = if self.selection_strategy.uses_amaf() { - Some(MoveTracking::new()) - } else { - None - }; + let mut move_tracking = self.initial_move_tracking(); while status == Status::Ongoing { let plies = sim_board.get_legal_plies(config); @@ -172,7 +170,9 @@ impl MctsAgent { break; } - let chosen_ply = self.playout_strategy.select_move(&plies, sim_board, config, &mut self.rng); + let chosen_ply = + self.playout_strategy + .select_move(&plies, sim_board, config, &mut self.rng); if let Some(ref mut tracking) = move_tracking { tracking.record_move(sim_board.turn, chosen_ply); @@ -244,8 +244,9 @@ impl MctsAgent { current_idx: usize, sim_board: &mut Board, config: &BoardConfig, - ) -> Status { + ) -> (usize, Status) { let mut status = sim_board.get_status(config); + let mut leaf_idx = current_idx; if status == Status::Ongoing && !tree[current_idx].unexpanded_plies.is_empty() { let ply_to_expand = tree[current_idx].unexpanded_plies.pop().unwrap(); @@ -265,10 +266,11 @@ impl MctsAgent { tree[current_idx].children.push(new_child_idx); self.current_stats.nodes_created += 1; + leaf_idx = new_child_idx; status = sim_board.get_status(config); } - status + (leaf_idx, status) } /// Phase 4: Backpropagation @@ -277,11 +279,12 @@ impl MctsAgent { tree: &mut [MctsNode], mut leaf_idx: usize, status: Status, - move_tracking: Option, + mut move_tracking: Option, ) { while leaf_idx != usize::MAX { let node_turn = tree[leaf_idx].turn; let parent_idx = tree[leaf_idx].parent; + let ply_to_reach = tree[leaf_idx].ply_to_reach; let children_indices = tree[leaf_idx].children.clone(); tree[leaf_idx].visits += 1; @@ -319,9 +322,23 @@ impl MctsAgent { } } + if let Some(ref mut tracking) = move_tracking { + if let Some(ply) = ply_to_reach { + tracking.record_move(node_turn.opponent(), ply); + } + } + leaf_idx = parent_idx.unwrap_or(usize::MAX); } } + + fn initial_move_tracking(&self) -> Option { + if self.selection_strategy.uses_amaf() { + Some(MoveTracking::new()) + } else { + None + } + } } /// Helper struct for tracking moves during playout (used for RAVE) @@ -355,3 +372,67 @@ impl MoveTracking { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::Player; + + fn test_agent(selection_strategy: SelectionStrategy) -> MctsAgent { + MctsAgent::new( + 1, + None, + 1.41, + selection_strategy, + PlayoutStrategy::Random, + 0, + ) + } + + #[test] + fn expansion_backpropagates_from_new_child() { + let config = BoardConfig::new(3, 3); + let mut board = Board { + white_board: 1 << 3, + black_board: 1 << 8, + turn: Player::White, + }; + let winning_ply = Ply { from: 3, to: 6 }; + + let mut agent = test_agent(SelectionStrategy::Ucb1); + let mut tree = vec![MctsNode::new(None, None, vec![winning_ply], board.turn)]; + + let (leaf_idx, status) = agent.expand_node(&mut tree, 0, &mut board, &config); + assert_eq!(leaf_idx, 1); + assert_eq!(status, Status::WhiteWon); + + agent.backpropagate(&mut tree, leaf_idx, status, None); + + assert_eq!(tree[0].visits, 1); + assert_eq!(tree[0].wins, 0.0); + assert_eq!(tree[1].visits, 1); + assert_eq!(tree[1].wins, 1.0); + } + + #[test] + fn terminal_expansion_records_path_move_for_rave() { + let config = BoardConfig::new(3, 3); + let mut board = Board { + white_board: 1 << 3, + black_board: 1 << 8, + turn: Player::White, + }; + let winning_ply = Ply { from: 3, to: 6 }; + + let mut agent = test_agent(SelectionStrategy::Rave { k: 1000.0 }); + let mut tree = vec![MctsNode::new(None, None, vec![winning_ply], board.turn)]; + + let (leaf_idx, status) = agent.expand_node(&mut tree, 0, &mut board, &config); + let move_tracking = agent.initial_move_tracking(); + + agent.backpropagate(&mut tree, leaf_idx, status, move_tracking); + + assert_eq!(tree[1].amaf_visits, 1); + assert_eq!(tree[1].amaf_wins, 1.0); + } +} From 80a9aac55d161bd286d6c7a6624060d7c185100c Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Mon, 18 May 2026 21:07:44 +0200 Subject: [PATCH 06/13] Rozdzielona bardziej modyfikacja rave od podstawy i przeniesione move_tracking --- src/compute/src/agents.rs | 2 +- src/compute/src/agents/mcts.rs | 172 ++++++------------ .../src/agents/mcts/playout_strategy.rs | 4 +- src/compute/src/agents/mcts/rave.rs | 88 +++++++++ .../src/agents/mcts/selection_strategy.rs | 5 +- 5 files changed, 151 insertions(+), 120 deletions(-) create mode 100644 src/compute/src/agents/mcts/rave.rs diff --git a/src/compute/src/agents.rs b/src/compute/src/agents.rs index 6311877..132abb6 100644 --- a/src/compute/src/agents.rs +++ b/src/compute/src/agents.rs @@ -3,7 +3,7 @@ mod metrics; mod minimax; mod stats; -pub use mcts::{MctsAgent, MctsStats, MctsStatsAccumulator, SelectionStrategy, PlayoutStrategy}; +pub use mcts::{MctsAgent, MctsStats, MctsStatsAccumulator, PlayoutStrategy, SelectionStrategy}; pub use metrics::*; pub use minimax::{ HeuristicEvaluator, MinimaxAgent, MinimaxStats, MinimaxStatsAccumulator, PositionEvaluator, diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index b2b96fe..5ed382c 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -3,11 +3,13 @@ mod metrics; pub mod node; mod playout_strategy; +mod rave; mod selection_strategy; mod stats; use node::MctsNode; pub use playout_strategy::PlayoutStrategy; +use rave::RaveTracker; pub use selection_strategy::SelectionStrategy; pub use stats::{MctsStats, MctsStatsAccumulator}; @@ -108,14 +110,14 @@ impl Agent for MctsAgent { self.expand_node(&mut tree, current_idx, &mut sim_board, config); if status != Status::Ongoing { - let move_tracking = self.initial_move_tracking(); - self.backpropagate(&mut tree, leaf_idx, status, move_tracking); + let mut rave_tracker = self.new_rave_tracker(); + self.backpropagate(&mut tree, leaf_idx, status, &mut rave_tracker); self.current_stats.iterations += 1; continue; } // Phase 3 & 4: Playout and Backpropagation - let (_final_status, playout_steps, _move_tracking) = + let (_final_status, playout_steps) = self.do_playout_and_backprop(&mut tree, leaf_idx, &mut sim_board, config); self.current_stats.playout_steps += playout_steps as u64; @@ -145,50 +147,6 @@ impl Agent for MctsAgent { } impl MctsAgent { - /// Combined phase 3 (Playout) and 4 (Backpropagation) - fn do_playout_and_backprop( - &mut self, - tree: &mut Vec, - current_idx: usize, - sim_board: &mut Board, - config: &BoardConfig, - ) -> (Status, u32, Option) { - // Phase 3: Playout - Play out a random/heuristic game - let mut playout_steps = 0; - let mut status = sim_board.get_status(config); - - let mut move_tracking = self.initial_move_tracking(); - - while status == Status::Ongoing { - let plies = sim_board.get_legal_plies(config); - if plies.is_empty() { - status = if sim_board.turn.is_white() { - Status::BlackWon - } else { - Status::WhiteWon - }; - break; - } - - let chosen_ply = - self.playout_strategy - .select_move(&plies, sim_board, config, &mut self.rng); - - if let Some(ref mut tracking) = move_tracking { - tracking.record_move(sim_board.turn, chosen_ply); - } - - sim_board.apply_ply(chosen_ply); - status = sim_board.get_status(config); - playout_steps += 1; - } - - // Phase 4: Backpropagation - Update tree statistics - self.backpropagate(tree, current_idx, status, move_tracking.clone()); - - (status, playout_steps, move_tracking) - } - /// Phase 1: Selection fn select_node( &self, @@ -273,19 +231,60 @@ impl MctsAgent { (leaf_idx, status) } + /// Combined phase 3 (Playout) and 4 (Backpropagation) + fn do_playout_and_backprop( + &mut self, + tree: &mut Vec, + current_idx: usize, + sim_board: &mut Board, + config: &BoardConfig, + ) -> (Status, u32) { + // Phase 3: Playout - Play out a random/heuristic game + let mut playout_steps = 0; + let mut status = sim_board.get_status(config); + + let mut rave_tracker = self.new_rave_tracker(); + + while status == Status::Ongoing { + let plies = sim_board.get_legal_plies(config); + if plies.is_empty() { + status = if sim_board.turn.is_white() { + Status::BlackWon + } else { + Status::WhiteWon + }; + break; + } + + let chosen_ply = + self.playout_strategy + .select_move(&plies, sim_board, config, &mut self.rng); + + rave_tracker.record_playout_move(sim_board.turn, chosen_ply); + + sim_board.apply_ply(chosen_ply); + status = sim_board.get_status(config); + playout_steps += 1; + } + + // Phase 4: Backpropagation - Update tree statistics + self.backpropagate(tree, current_idx, status, &mut rave_tracker); + + (status, playout_steps) + } + /// Phase 4: Backpropagation fn backpropagate( &mut self, tree: &mut [MctsNode], mut leaf_idx: usize, status: Status, - mut move_tracking: Option, + rave_tracker: &mut RaveTracker, ) { while leaf_idx != usize::MAX { let node_turn = tree[leaf_idx].turn; let parent_idx = tree[leaf_idx].parent; let ply_to_reach = tree[leaf_idx].ply_to_reach; - let children_indices = tree[leaf_idx].children.clone(); tree[leaf_idx].visits += 1; @@ -300,76 +299,18 @@ impl MctsAgent { tree[leaf_idx].wins += 1.0; } - if self.selection_strategy.uses_amaf() { - if let Some(ref tracking) = move_tracking { - let turn_player_won = match status { - Status::WhiteWon => node_turn.is_white(), - Status::BlackWon => !node_turn.is_white(), - Status::Ongoing => false, - }; - - for &child_idx in &children_indices { - let child = &mut tree[child_idx]; - if let Some(ply) = child.ply_to_reach { - if tracking.contains_move(node_turn, ply) { - child.amaf_visits += 1; - if turn_player_won { - child.amaf_wins += 1.0; - } - } - } - } - } - } + rave_tracker.update_amaf_children(tree, leaf_idx, status); - if let Some(ref mut tracking) = move_tracking { - if let Some(ply) = ply_to_reach { - tracking.record_move(node_turn.opponent(), ply); - } + if let Some(ply) = ply_to_reach { + rave_tracker.record_path_move(node_turn.opponent(), ply); } leaf_idx = parent_idx.unwrap_or(usize::MAX); } } - fn initial_move_tracking(&self) -> Option { - if self.selection_strategy.uses_amaf() { - Some(MoveTracking::new()) - } else { - None - } - } -} - -/// Helper struct for tracking moves during playout (used for RAVE) -#[derive(Clone)] -struct MoveTracking { - white_moves: [[bool; 128]; 128], - black_moves: [[bool; 128]; 128], -} - -impl MoveTracking { - fn new() -> Self { - Self { - white_moves: [[false; 128]; 128], - black_moves: [[false; 128]; 128], - } - } - - fn record_move(&mut self, player: crate::core::Player, ply: Ply) { - if player.is_white() { - self.white_moves[ply.from as usize][ply.to as usize] = true; - } else { - self.black_moves[ply.from as usize][ply.to as usize] = true; - } - } - - fn contains_move(&self, player: crate::core::Player, ply: Ply) -> bool { - if player.is_white() { - self.white_moves[ply.from as usize][ply.to as usize] - } else { - self.black_moves[ply.from as usize][ply.to as usize] - } + fn new_rave_tracker(&self) -> RaveTracker { + RaveTracker::new(self.selection_strategy.uses_amaf()) } } @@ -406,7 +347,8 @@ mod tests { assert_eq!(leaf_idx, 1); assert_eq!(status, Status::WhiteWon); - agent.backpropagate(&mut tree, leaf_idx, status, None); + let mut rave_tracker = RaveTracker::Disabled; + agent.backpropagate(&mut tree, leaf_idx, status, &mut rave_tracker); assert_eq!(tree[0].visits, 1); assert_eq!(tree[0].wins, 0.0); @@ -428,9 +370,9 @@ mod tests { let mut tree = vec![MctsNode::new(None, None, vec![winning_ply], board.turn)]; let (leaf_idx, status) = agent.expand_node(&mut tree, 0, &mut board, &config); - let move_tracking = agent.initial_move_tracking(); + let mut rave_tracker = agent.new_rave_tracker(); - agent.backpropagate(&mut tree, leaf_idx, status, move_tracking); + agent.backpropagate(&mut tree, leaf_idx, status, &mut rave_tracker); assert_eq!(tree[1].amaf_visits, 1); assert_eq!(tree[1].amaf_wins, 1.0); diff --git a/src/compute/src/agents/mcts/playout_strategy.rs b/src/compute/src/agents/mcts/playout_strategy.rs index b26d8aa..47dc36c 100644 --- a/src/compute/src/agents/mcts/playout_strategy.rs +++ b/src/compute/src/agents/mcts/playout_strategy.rs @@ -1,8 +1,8 @@ -use rand::rngs::SmallRng; use rand::RngExt; +use rand::rngs::SmallRng; -use crate::core::{Board, BoardConfig, Ply}; use super::super::minimax::PositionEvaluator; +use crate::core::{Board, BoardConfig, Ply}; /// Strategy for selecting moves during the playout phase #[derive(Debug)] diff --git a/src/compute/src/agents/mcts/rave.rs b/src/compute/src/agents/mcts/rave.rs new file mode 100644 index 0000000..bd1c871 --- /dev/null +++ b/src/compute/src/agents/mcts/rave.rs @@ -0,0 +1,88 @@ +use crate::core::{Player, Ply, Status}; + +use super::node::MctsNode; + +#[derive(Debug)] +pub enum RaveTracker { + Disabled, + Enabled(MoveTracking), +} + +impl RaveTracker { + pub fn new(enabled: bool) -> Self { + if enabled { + Self::Enabled(MoveTracking::new()) + } else { + Self::Disabled + } + } + + pub fn record_playout_move(&mut self, player: Player, ply: Ply) { + if let Self::Enabled(tracking) = self { + tracking.record_move(player, ply); + } + } + + pub fn record_path_move(&mut self, player: Player, ply: Ply) { + if let Self::Enabled(tracking) = self { + tracking.record_move(player, ply); + } + } + + pub fn update_amaf_children(&self, tree: &mut [MctsNode], node_idx: usize, status: Status) { + let Self::Enabled(tracking) = self else { + return; + }; + + let node_turn = tree[node_idx].turn; + let children_indices = tree[node_idx].children.clone(); + let turn_player_won = match status { + Status::WhiteWon => node_turn.is_white(), + Status::BlackWon => !node_turn.is_white(), + Status::Ongoing => false, + }; + + for child_idx in children_indices { + let child = &mut tree[child_idx]; + if let Some(ply) = child.ply_to_reach { + if tracking.contains_move(node_turn, ply) { + child.amaf_visits += 1; + if turn_player_won { + child.amaf_wins += 1.0; + } + } + } + } + } +} + +#[derive(Debug)] +pub struct MoveTracking { + white_moves: [[bool; 128]; 128], + black_moves: [[bool; 128]; 128], +} + +impl MoveTracking { + fn new() -> Self { + Self { + white_moves: [[false; 128]; 128], + black_moves: [[false; 128]; 128], + } + } + + fn record_move(&mut self, player: Player, ply: Ply) { + if player.is_white() { + self.white_moves[ply.from as usize][ply.to as usize] = true; + } else { + self.black_moves[ply.from as usize][ply.to as usize] = true; + } + } + + fn contains_move(&self, player: Player, ply: Ply) -> bool { + if player.is_white() { + self.white_moves[ply.from as usize][ply.to as usize] + } else { + self.black_moves[ply.from as usize][ply.to as usize] + } + } +} diff --git a/src/compute/src/agents/mcts/selection_strategy.rs b/src/compute/src/agents/mcts/selection_strategy.rs index e3a11ba..15e9254 100644 --- a/src/compute/src/agents/mcts/selection_strategy.rs +++ b/src/compute/src/agents/mcts/selection_strategy.rs @@ -9,7 +9,7 @@ pub enum SelectionStrategy { impl SelectionStrategy { /// Computes the UCT score for a child node - /// + /// /// # Arguments /// * `child_visits` - Number of times this child has been visited /// * `child_wins` - Total wins accumulated at this child @@ -31,7 +31,8 @@ impl SelectionStrategy { } let exploitation = child_wins / (child_visits as f64); - let exploration = exploration_constant * ((parent_visits as f64).ln() / (child_visits as f64)).sqrt(); + let exploration = + exploration_constant * ((parent_visits as f64).ln() / (child_visits as f64)).sqrt(); match self { SelectionStrategy::Ucb1 => exploitation + exploration, From 8726ed5796bb8d2b23523f1a740c5626d896144d Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Mon, 18 May 2026 23:01:48 +0200 Subject: [PATCH 07/13] Dodane testy --- src/compute/src/agents/mcts.rs | 200 ++++++++++++++++++ .../src/agents/mcts/playout_strategy.rs | 86 ++++++++ src/compute/src/agents/mcts/rave.rs | 104 +++++++++ .../src/agents/mcts/selection_strategy.rs | 47 ++++ 4 files changed, 437 insertions(+) diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index 5ed382c..ace7f6b 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -330,6 +330,121 @@ mod tests { ) } + #[test] + fn select_node_stops_at_node_with_unexpanded_plies() { + let config = BoardConfig::new(3, 3); + let mut board = Board { + white_board: 1 << 3, + black_board: 1 << 8, + turn: Player::White, + }; + let original_board = board; + let mut root = MctsNode::new(None, None, vec![Ply { from: 3, to: 6 }], board.turn); + root.visits = 10; + root.children = vec![1]; + let child = MctsNode::new( + Some(0), + Some(Ply { from: 3, to: 7 }), + Vec::new(), + Player::Black, + ); + let tree = vec![root, child]; + let agent = test_agent(SelectionStrategy::Ucb1); + + let selected_idx = agent.select_node(&tree, &mut board, &config, 0); + + assert_eq!(selected_idx, 0); + assert_eq!(board, original_board); + } + + #[test] + fn select_node_descends_to_highest_scoring_child_and_applies_ply() { + let config = BoardConfig::new(3, 3); + let mut board = Board { + white_board: 1 << 3, + black_board: 1 << 8, + turn: Player::White, + }; + let mut root = MctsNode::new(None, None, Vec::new(), board.turn); + root.visits = 10; + root.children = vec![1, 2]; + + let mut good_child = MctsNode::new( + Some(0), + Some(Ply { from: 3, to: 6 }), + Vec::new(), + Player::Black, + ); + good_child.visits = 5; + good_child.wins = 5.0; + + let mut bad_child = MctsNode::new( + Some(0), + Some(Ply { from: 3, to: 7 }), + Vec::new(), + Player::Black, + ); + bad_child.visits = 5; + bad_child.wins = 0.0; + + let tree = vec![root, good_child, bad_child]; + let agent = test_agent(SelectionStrategy::Ucb1); + + let selected_idx = agent.select_node(&tree, &mut board, &config, 0); + + assert_eq!(selected_idx, 1); + assert_eq!(board.turn, Player::Black); + assert_eq!(board.white_board, 1 << 6); + } + + #[test] + fn expansion_creates_child_for_nonterminal_move() { + let config = BoardConfig::new(3, 3); + let mut board = Board { + white_board: 1 << 1, + black_board: 1 << 7, + turn: Player::White, + }; + let ply = Ply { from: 1, to: 4 }; + let mut agent = test_agent(SelectionStrategy::Ucb1); + let mut tree = vec![MctsNode::new(None, None, vec![ply], board.turn)]; + + let (leaf_idx, status) = agent.expand_node(&mut tree, 0, &mut board, &config); + + assert_eq!(leaf_idx, 1); + assert_eq!(status, Status::Ongoing); + assert_eq!(tree[0].children, vec![1]); + assert_eq!(tree[1].parent, Some(0)); + assert_eq!(tree[1].ply_to_reach, Some(ply)); + assert_eq!(tree[1].turn, Player::Black); + assert_eq!(agent.current_stats.nodes_created, 1); + } + + #[test] + fn expansion_does_not_create_child_for_already_terminal_node() { + let config = BoardConfig::new(3, 3); + let mut board = Board { + white_board: 1 << 6, + black_board: 1 << 8, + turn: Player::Black, + }; + let mut agent = test_agent(SelectionStrategy::Ucb1); + let mut tree = vec![MctsNode::new( + None, + None, + vec![Ply { from: 8, to: 5 }], + board.turn, + )]; + + let (leaf_idx, status) = agent.expand_node(&mut tree, 0, &mut board, &config); + + assert_eq!(leaf_idx, 0); + assert_eq!(status, Status::WhiteWon); + assert_eq!(tree.len(), 1); + assert!(tree[0].children.is_empty()); + assert_eq!(agent.current_stats.nodes_created, 0); + } + #[test] fn expansion_backpropagates_from_new_child() { let config = BoardConfig::new(3, 3); @@ -356,6 +471,91 @@ mod tests { assert_eq!(tree[1].wins, 1.0); } + #[test] + fn backpropagation_credits_black_child_on_black_win() { + let mut root = MctsNode::new(None, None, Vec::new(), Player::Black); + root.children = vec![1]; + let child = MctsNode::new( + Some(0), + Some(Ply { from: 5, to: 2 }), + Vec::new(), + Player::White, + ); + let mut tree = vec![root, child]; + let mut agent = test_agent(SelectionStrategy::Ucb1); + let mut rave_tracker = RaveTracker::Disabled; + + agent.backpropagate(&mut tree, 1, Status::BlackWon, &mut rave_tracker); + + assert_eq!(tree[0].visits, 1); + assert_eq!(tree[0].wins, 0.0); + assert_eq!(tree[1].visits, 1); + assert_eq!(tree[1].wins, 1.0); + } + + #[test] + fn do_playout_and_backprop_updates_leaf_and_counts_steps() { + let config = BoardConfig::new(4, 4); + let mut board = Board { + white_board: 1 << 4, + black_board: 1 << 15, + turn: Player::White, + }; + board.apply_ply(Ply { from: 4, to: 8 }); + let mut root = MctsNode::new(None, None, Vec::new(), Player::White); + root.children = vec![1]; + let child = MctsNode::new( + Some(0), + Some(Ply { from: 4, to: 8 }), + Vec::new(), + Player::Black, + ); + let mut tree = vec![root, child]; + let mut agent = test_agent(SelectionStrategy::Ucb1); + + let (status, steps) = agent.do_playout_and_backprop(&mut tree, 1, &mut board, &config); + + assert_ne!(status, Status::Ongoing); + assert!(steps > 0); + assert_eq!(tree[1].visits, 1); + assert_eq!(tree[0].visits, 1); + } + + #[test] + fn select_ply_returns_none_when_side_to_move_has_no_pieces() { + let config = BoardConfig::new(3, 3); + let board = Board { + white_board: 0, + black_board: 1 << 8, + turn: Player::White, + }; + let mut agent = test_agent(SelectionStrategy::Ucb1); + + let selected = agent.select_ply(&board, &config); + + assert_eq!(selected, None); + assert_eq!(agent.current_stats.iterations, 0); + } + + #[test] + fn select_ply_with_one_iteration_returns_a_legal_root_move() { + let config = BoardConfig::new(3, 3); + let board = Board { + white_board: 1 << 3, + black_board: 1 << 8, + turn: Player::White, + }; + let legal_plies = board.get_legal_plies(&config); + let mut agent = test_agent(SelectionStrategy::Ucb1); + + let selected = agent.select_ply(&board, &config); + + assert!(selected.is_some()); + assert!(legal_plies.contains(&selected.unwrap())); + assert_eq!(agent.current_stats.iterations, 1); + assert_eq!(agent.current_stats.nodes_created, 2); + } + #[test] fn terminal_expansion_records_path_move_for_rave() { let config = BoardConfig::new(3, 3); diff --git a/src/compute/src/agents/mcts/playout_strategy.rs b/src/compute/src/agents/mcts/playout_strategy.rs index 47dc36c..ab4a8d6 100644 --- a/src/compute/src/agents/mcts/playout_strategy.rs +++ b/src/compute/src/agents/mcts/playout_strategy.rs @@ -59,3 +59,89 @@ impl PlayoutStrategy { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::agents::HeuristicEvaluator; + use crate::core::Player; + use rand::SeedableRng; + + fn rng() -> SmallRng { + SmallRng::seed_from_u64(0) + } + + #[test] + fn random_playout_returns_the_only_available_move() { + let config = BoardConfig::new(3, 3); + let board = Board { + white_board: 1 << 3, + black_board: 1 << 8, + turn: Player::White, + }; + let plies = [Ply { from: 3, to: 6 }]; + + let selected = PlayoutStrategy::Random.select_move(&plies, &board, &config, &mut rng()); + + assert_eq!(selected, plies[0]); + } + + #[test] + fn heavy_playout_greedily_selects_white_terminal_win() { + let config = BoardConfig::new(3, 3); + let board = Board { + white_board: (1 << 0) | (1 << 3), + black_board: 1 << 8, + turn: Player::White, + }; + let winning_ply = Ply { from: 3, to: 6 }; + let plies = [Ply { from: 0, to: 4 }, winning_ply]; + let strategy = PlayoutStrategy::Heavy { + evaluator: Box::new(HeuristicEvaluator::default()), + epsilon: 0.0, + }; + + let selected = strategy.select_move(&plies, &board, &config, &mut rng()); + + assert_eq!(selected, winning_ply); + } + + #[test] + fn heavy_playout_greedily_selects_black_terminal_win() { + let config = BoardConfig::new(3, 3); + let board = Board { + white_board: 1 << 3, + black_board: (1 << 5) | (1 << 8), + turn: Player::Black, + }; + let winning_ply = Ply { from: 5, to: 2 }; + let plies = [Ply { from: 8, to: 4 }, winning_ply]; + let strategy = PlayoutStrategy::Heavy { + evaluator: Box::new(HeuristicEvaluator::default()), + epsilon: 0.0, + }; + + let selected = strategy.select_move(&plies, &board, &config, &mut rng()); + + assert_eq!(selected, winning_ply); + } + + #[test] + fn heavy_playout_with_full_epsilon_uses_random_branch() { + let config = BoardConfig::new(3, 3); + let board = Board { + white_board: 1 << 3, + black_board: 1 << 8, + turn: Player::White, + }; + let plies = [Ply { from: 3, to: 6 }]; + let strategy = PlayoutStrategy::Heavy { + evaluator: Box::new(HeuristicEvaluator::default()), + epsilon: 1.0, + }; + + let selected = strategy.select_move(&plies, &board, &config, &mut rng()); + + assert_eq!(selected, plies[0]); + } +} diff --git a/src/compute/src/agents/mcts/rave.rs b/src/compute/src/agents/mcts/rave.rs index bd1c871..7626337 100644 --- a/src/compute/src/agents/mcts/rave.rs +++ b/src/compute/src/agents/mcts/rave.rs @@ -86,3 +86,107 @@ impl MoveTracking { } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::core::BoardConfig; + + fn root_with_children() -> Vec { + let mut root = MctsNode::new( + None, + None, + Vec::new(), + Player::White, + ); + root.children = vec![1, 2]; + + let matching_child = MctsNode::new( + Some(0), + Some(Ply { from: 1, to: 4 }), + Vec::new(), + Player::Black, + ); + let other_child = MctsNode::new( + Some(0), + Some(Ply { from: 2, to: 5 }), + Vec::new(), + Player::Black, + ); + + vec![root, matching_child, other_child] + } + + #[test] + fn disabled_tracker_does_not_update_amaf_stats() { + let mut tree = root_with_children(); + let tracker = RaveTracker::Disabled; + + tracker.update_amaf_children(&mut tree, 0, Status::WhiteWon); + + assert_eq!(tree[1].amaf_visits, 0); + assert_eq!(tree[2].amaf_visits, 0); + } + + #[test] + fn playout_move_updates_matching_child_for_same_player() { + let mut tree = root_with_children(); + let mut tracker = RaveTracker::new(true); + + tracker.record_playout_move(Player::White, Ply { from: 1, to: 4 }); + tracker.update_amaf_children(&mut tree, 0, Status::WhiteWon); + + assert_eq!(tree[1].amaf_visits, 1); + assert_eq!(tree[1].amaf_wins, 1.0); + assert_eq!(tree[2].amaf_visits, 0); + } + + #[test] + fn move_from_opponent_does_not_update_node_turn_children() { + let mut tree = root_with_children(); + let mut tracker = RaveTracker::new(true); + + tracker.record_playout_move(Player::Black, Ply { from: 1, to: 4 }); + tracker.update_amaf_children(&mut tree, 0, Status::WhiteWon); + + assert_eq!(tree[1].amaf_visits, 0); + assert_eq!(tree[1].amaf_wins, 0.0); + } + + #[test] + fn amaf_visit_is_recorded_even_when_node_turn_loses() { + let mut tree = root_with_children(); + let mut tracker = RaveTracker::new(true); + + tracker.record_playout_move(Player::White, Ply { from: 1, to: 4 }); + tracker.update_amaf_children(&mut tree, 0, Status::BlackWon); + + assert_eq!(tree[1].amaf_visits, 1); + assert_eq!(tree[1].amaf_wins, 0.0); + } + + #[test] + fn path_move_can_be_used_by_ancestor_updates() { + let mut tree = root_with_children(); + let mut tracker = RaveTracker::new(true); + + tracker.record_path_move(Player::White, Ply { from: 1, to: 4 }); + tracker.update_amaf_children(&mut tree, 0, Status::WhiteWon); + + assert_eq!(tree[1].amaf_visits, 1); + assert_eq!(tree[1].amaf_wins, 1.0); + } + + #[test] + fn move_tracking_supports_highest_board_square() { + let config = BoardConfig::new(16, 8); + assert_eq!(config.width * config.height, 128); + + let mut tracker = MoveTracking::new(); + let ply = Ply { from: 126, to: 127 }; + tracker.record_move(Player::White, ply); + + assert!(tracker.contains_move(Player::White, ply)); + assert!(!tracker.contains_move(Player::Black, ply)); + } +} diff --git a/src/compute/src/agents/mcts/selection_strategy.rs b/src/compute/src/agents/mcts/selection_strategy.rs index 15e9254..3d30115 100644 --- a/src/compute/src/agents/mcts/selection_strategy.rs +++ b/src/compute/src/agents/mcts/selection_strategy.rs @@ -53,3 +53,50 @@ impl SelectionStrategy { matches!(self, SelectionStrategy::Rave { .. }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unvisited_child_has_infinite_priority() { + let score = SelectionStrategy::Ucb1.compute_uct_score(0, 0.0, 0, 0.0, 10, 1.41); + + assert!(score.is_infinite()); + assert!(score.is_sign_positive()); + } + + #[test] + fn ucb1_combines_exploitation_and_exploration() { + let score = SelectionStrategy::Ucb1.compute_uct_score(4, 3.0, 0, 0.0, 16, 2.0); + let expected = 0.75_f64 + 2.0_f64 * (16.0_f64.ln() / 4.0_f64).sqrt(); + + assert!((score - expected).abs() < 1e-12); + } + + #[test] + fn rave_falls_back_to_ucb_when_amaf_has_no_visits() { + let ucb = SelectionStrategy::Ucb1.compute_uct_score(4, 3.0, 0, 0.0, 16, 1.41); + let rave = SelectionStrategy::Rave { k: 1000.0 } + .compute_uct_score(4, 3.0, 0, 0.0, 16, 1.41); + + assert!((rave - ucb).abs() < 1e-12); + } + + #[test] + fn rave_blends_amaf_and_direct_value() { + let score = SelectionStrategy::Rave { k: 4.0 } + .compute_uct_score(4, 1.0, 8, 6.0, 16, 0.0); + + let beta = 4.0 / (4.0 + 4.0); + let expected = (1.0 - beta) * 0.25 + beta * 0.75; + + assert!((score - expected).abs() < 1e-12); + } + + #[test] + fn uses_amaf_only_for_rave() { + assert!(!SelectionStrategy::Ucb1.uses_amaf()); + assert!(SelectionStrategy::Rave { k: 1000.0 }.uses_amaf()); + } +} From 29d1eda4e38571a55980d6eb37903caa357b1662 Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Mon, 18 May 2026 23:02:37 +0200 Subject: [PATCH 08/13] Formatowanie --- src/compute/src/agents/mcts/rave.rs | 7 +------ src/compute/src/agents/mcts/selection_strategy.rs | 7 +++---- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/compute/src/agents/mcts/rave.rs b/src/compute/src/agents/mcts/rave.rs index 7626337..18b99d9 100644 --- a/src/compute/src/agents/mcts/rave.rs +++ b/src/compute/src/agents/mcts/rave.rs @@ -93,12 +93,7 @@ mod tests { use crate::core::BoardConfig; fn root_with_children() -> Vec { - let mut root = MctsNode::new( - None, - None, - Vec::new(), - Player::White, - ); + let mut root = MctsNode::new(None, None, Vec::new(), Player::White); root.children = vec![1, 2]; let matching_child = MctsNode::new( diff --git a/src/compute/src/agents/mcts/selection_strategy.rs b/src/compute/src/agents/mcts/selection_strategy.rs index 3d30115..cc74d76 100644 --- a/src/compute/src/agents/mcts/selection_strategy.rs +++ b/src/compute/src/agents/mcts/selection_strategy.rs @@ -77,16 +77,15 @@ mod tests { #[test] fn rave_falls_back_to_ucb_when_amaf_has_no_visits() { let ucb = SelectionStrategy::Ucb1.compute_uct_score(4, 3.0, 0, 0.0, 16, 1.41); - let rave = SelectionStrategy::Rave { k: 1000.0 } - .compute_uct_score(4, 3.0, 0, 0.0, 16, 1.41); + let rave = + SelectionStrategy::Rave { k: 1000.0 }.compute_uct_score(4, 3.0, 0, 0.0, 16, 1.41); assert!((rave - ucb).abs() < 1e-12); } #[test] fn rave_blends_amaf_and_direct_value() { - let score = SelectionStrategy::Rave { k: 4.0 } - .compute_uct_score(4, 1.0, 8, 6.0, 16, 0.0); + let score = SelectionStrategy::Rave { k: 4.0 }.compute_uct_score(4, 1.0, 8, 6.0, 16, 0.0); let beta = 4.0 / (4.0 + 4.0); let expected = (1.0 - beta) * 0.25 + beta * 0.75; From a89e36f0fa48fbbca088d12bbc17978201af66f4 Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Mon, 18 May 2026 23:17:42 +0200 Subject: [PATCH 09/13] =?UTF-8?q?Dodana=20funkcja=20sprawdzaj=C4=85ca=20kt?= =?UTF-8?q?o=20wygra=C5=82=20i=20oddzielone=20statystyki=20amaf=20od=20zwy?= =?UTF-8?q?k=C5=82ego=20w=C4=99z=C5=82a=20drzewa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/compute/src/agents/mcts.rs | 36 ++++++++++++++++--------- src/compute/src/agents/mcts/node.rs | 9 +++++-- src/compute/src/agents/mcts/rave.rs | 41 +++++++++++++---------------- 3 files changed, 50 insertions(+), 36 deletions(-) diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index ace7f6b..4b50522 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -18,7 +18,15 @@ use rand::{SeedableRng, seq::SliceRandom}; use std::time::Instant; use super::{Agent, AgentStats}; -use crate::core::{Board, BoardConfig, Ply, Status}; +use crate::core::{Board, BoardConfig, Player, Ply, Status}; + +pub(super) fn did_player_win(status: Status, player: Player) -> bool { + match status { + Status::WhiteWon => player.is_white(), + Status::BlackWon => player.is_black(), + Status::Ongoing => false, + } +} #[derive(Debug)] pub struct MctsAgent { @@ -174,8 +182,8 @@ impl MctsAgent { let score = self.selection_strategy.compute_uct_score( child.visits, child.wins, - child.amaf_visits, - child.amaf_wins, + child.amaf.visits, + child.amaf.wins, parent_visits, c, ); @@ -289,13 +297,7 @@ impl MctsAgent { tree[leaf_idx].visits += 1; let player_who_just_moved = node_turn.opponent(); - let is_win = match status { - Status::WhiteWon => player_who_just_moved.is_white(), - Status::BlackWon => !player_who_just_moved.is_white(), - Status::Ongoing => false, - }; - - if is_win { + if did_player_win(status, player_who_just_moved) { tree[leaf_idx].wins += 1.0; } @@ -330,6 +332,16 @@ mod tests { ) } + #[test] + fn did_player_win_matches_terminal_status() { + assert!(did_player_win(Status::WhiteWon, Player::White)); + assert!(!did_player_win(Status::WhiteWon, Player::Black)); + assert!(did_player_win(Status::BlackWon, Player::Black)); + assert!(!did_player_win(Status::BlackWon, Player::White)); + assert!(!did_player_win(Status::Ongoing, Player::White)); + assert!(!did_player_win(Status::Ongoing, Player::Black)); + } + #[test] fn select_node_stops_at_node_with_unexpanded_plies() { let config = BoardConfig::new(3, 3); @@ -574,7 +586,7 @@ mod tests { agent.backpropagate(&mut tree, leaf_idx, status, &mut rave_tracker); - assert_eq!(tree[1].amaf_visits, 1); - assert_eq!(tree[1].amaf_wins, 1.0); + assert_eq!(tree[1].amaf.visits, 1); + assert_eq!(tree[1].amaf.wins, 1.0); } } diff --git a/src/compute/src/agents/mcts/node.rs b/src/compute/src/agents/mcts/node.rs index 7135a90..3d3de57 100644 --- a/src/compute/src/agents/mcts/node.rs +++ b/src/compute/src/agents/mcts/node.rs @@ -1,5 +1,11 @@ use crate::core::{Player, Ply}; +#[derive(Debug, Default, Clone, Copy)] +pub struct AmafStats { + pub visits: u32, + pub wins: f64, +} + #[derive(Debug, Default)] pub struct MctsNode { pub parent: Option, @@ -12,8 +18,7 @@ pub struct MctsNode { pub turn: Player, - pub amaf_visits: u32, - pub amaf_wins: f64, + pub amaf: AmafStats, } impl MctsNode { diff --git a/src/compute/src/agents/mcts/rave.rs b/src/compute/src/agents/mcts/rave.rs index 18b99d9..2242fa4 100644 --- a/src/compute/src/agents/mcts/rave.rs +++ b/src/compute/src/agents/mcts/rave.rs @@ -1,6 +1,6 @@ use crate::core::{Player, Ply, Status}; -use super::node::MctsNode; +use super::{did_player_win, node::MctsNode}; #[derive(Debug)] pub enum RaveTracker { @@ -35,20 +35,17 @@ impl RaveTracker { }; let node_turn = tree[node_idx].turn; - let children_indices = tree[node_idx].children.clone(); - let turn_player_won = match status { - Status::WhiteWon => node_turn.is_white(), - Status::BlackWon => !node_turn.is_white(), - Status::Ongoing => false, - }; + let node_turn_won = did_player_win(status, node_turn); + let children_len = tree[node_idx].children.len(); - for child_idx in children_indices { + for child_position in 0..children_len { + let child_idx = tree[node_idx].children[child_position]; let child = &mut tree[child_idx]; if let Some(ply) = child.ply_to_reach { if tracking.contains_move(node_turn, ply) { - child.amaf_visits += 1; - if turn_player_won { - child.amaf_wins += 1.0; + child.amaf.visits += 1; + if node_turn_won { + child.amaf.wins += 1.0; } } } @@ -119,8 +116,8 @@ mod tests { tracker.update_amaf_children(&mut tree, 0, Status::WhiteWon); - assert_eq!(tree[1].amaf_visits, 0); - assert_eq!(tree[2].amaf_visits, 0); + assert_eq!(tree[1].amaf.visits, 0); + assert_eq!(tree[2].amaf.visits, 0); } #[test] @@ -131,9 +128,9 @@ mod tests { tracker.record_playout_move(Player::White, Ply { from: 1, to: 4 }); tracker.update_amaf_children(&mut tree, 0, Status::WhiteWon); - assert_eq!(tree[1].amaf_visits, 1); - assert_eq!(tree[1].amaf_wins, 1.0); - assert_eq!(tree[2].amaf_visits, 0); + assert_eq!(tree[1].amaf.visits, 1); + assert_eq!(tree[1].amaf.wins, 1.0); + assert_eq!(tree[2].amaf.visits, 0); } #[test] @@ -144,8 +141,8 @@ mod tests { tracker.record_playout_move(Player::Black, Ply { from: 1, to: 4 }); tracker.update_amaf_children(&mut tree, 0, Status::WhiteWon); - assert_eq!(tree[1].amaf_visits, 0); - assert_eq!(tree[1].amaf_wins, 0.0); + assert_eq!(tree[1].amaf.visits, 0); + assert_eq!(tree[1].amaf.wins, 0.0); } #[test] @@ -156,8 +153,8 @@ mod tests { tracker.record_playout_move(Player::White, Ply { from: 1, to: 4 }); tracker.update_amaf_children(&mut tree, 0, Status::BlackWon); - assert_eq!(tree[1].amaf_visits, 1); - assert_eq!(tree[1].amaf_wins, 0.0); + assert_eq!(tree[1].amaf.visits, 1); + assert_eq!(tree[1].amaf.wins, 0.0); } #[test] @@ -168,8 +165,8 @@ mod tests { tracker.record_path_move(Player::White, Ply { from: 1, to: 4 }); tracker.update_amaf_children(&mut tree, 0, Status::WhiteWon); - assert_eq!(tree[1].amaf_visits, 1); - assert_eq!(tree[1].amaf_wins, 1.0); + assert_eq!(tree[1].amaf.visits, 1); + assert_eq!(tree[1].amaf.wins, 1.0); } #[test] From 0c49e893b44aa67fc7f9b7f5329d26dd1160e902 Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Tue, 19 May 2026 20:38:37 +0200 Subject: [PATCH 10/13] Dodane gui do zmiany trybu grania agenta mcts --- src/compute/src/agents.rs | 89 ++++++++----- src/compute/src/gui/views/gameplay.rs | 179 +++++++++++++++++++++++--- 2 files changed, 224 insertions(+), 44 deletions(-) diff --git a/src/compute/src/agents.rs b/src/compute/src/agents.rs index 132abb6..cdd1652 100644 --- a/src/compute/src/agents.rs +++ b/src/compute/src/agents.rs @@ -14,43 +14,85 @@ use serde::{Deserialize, Serialize}; use crate::core::{Board, BoardConfig, Ply}; +pub const DEFAULT_MINIMAX_MAX_DEPTH: u8 = 4; +pub const DEFAULT_HEURISTIC_MATERIAL_WEIGHT: i32 = 100; +pub const DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT: i32 = 10; +pub const DEFAULT_HEURISTIC_DEFENDED_WEIGHT: i32 = 5; +pub const DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT: i32 = -2; +pub const DEFAULT_MCTS_MAX_ITERATIONS: u32 = 75000; +pub const DEFAULT_MCTS_EXPLORATION_CONSTANT: f64 = 1.41; +pub const DEFAULT_MCTS_RAVE_K: f64 = 1000.0; +pub const DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON: f64 = 0.1; + fn default_max_depth() -> u8 { - 4 + DEFAULT_MINIMAX_MAX_DEPTH } fn default_material() -> i32 { - 100 + DEFAULT_HEURISTIC_MATERIAL_WEIGHT } fn default_advancement() -> i32 { - 10 + DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT } fn default_defended() -> i32 { - 5 + DEFAULT_HEURISTIC_DEFENDED_WEIGHT } fn default_edge_penalty() -> i32 { - -2 + DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT } fn default_max_iterations() -> u32 { - 75000 + DEFAULT_MCTS_MAX_ITERATIONS } fn default_max_time_ms() -> Option { None } fn default_exploration_constant() -> f64 { - 1.41 + DEFAULT_MCTS_EXPLORATION_CONSTANT } fn default_use_heavy_playouts() -> bool { false } fn default_heavy_playouts_epsilon() -> f64 { - 0.1 + DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON } fn default_use_rave() -> bool { false } fn default_rave_k() -> f64 { - 1000.0 + DEFAULT_MCTS_RAVE_K +} + +pub fn build_mcts_selection_strategy(use_rave: bool, rave_k: f64) -> SelectionStrategy { + if use_rave { + SelectionStrategy::Rave { k: rave_k } + } else { + SelectionStrategy::Ucb1 + } +} + +pub fn build_mcts_playout_strategy( + use_heavy_playouts: bool, + heavy_playouts_epsilon: f64, + material_weight: i32, + advancement_weight: i32, + defended_weight: i32, + edge_penalty_weight: i32, +) -> PlayoutStrategy { + if use_heavy_playouts { + let evaluator = Box::new(HeuristicEvaluator::new( + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, + )); + PlayoutStrategy::Heavy { + evaluator, + epsilon: heavy_playouts_epsilon, + } + } else { + PlayoutStrategy::Random + } } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -150,26 +192,15 @@ impl AgentConfig { defended_weight, edge_penalty_weight, } => { - let selection_strategy = if *use_rave { - SelectionStrategy::Rave { k: *rave_k } - } else { - SelectionStrategy::Ucb1 - }; - - let playout_strategy = if *use_heavy_playouts { - let evaluator = Box::new(HeuristicEvaluator::new( - *material_weight, - *advancement_weight, - *defended_weight, - *edge_penalty_weight, - )); - PlayoutStrategy::Heavy { - evaluator, - epsilon: *heavy_playouts_epsilon, - } - } else { - PlayoutStrategy::Random - }; + let selection_strategy = build_mcts_selection_strategy(*use_rave, *rave_k); + let playout_strategy = build_mcts_playout_strategy( + *use_heavy_playouts, + *heavy_playouts_epsilon, + *material_weight, + *advancement_weight, + *defended_weight, + *edge_penalty_weight, + ); BreakthroughAgent::Mcts(MctsAgent::new( *max_iterations, diff --git a/src/compute/src/gui/views/gameplay.rs b/src/compute/src/gui/views/gameplay.rs index ad7a9c6..cf63dfd 100644 --- a/src/compute/src/gui/views/gameplay.rs +++ b/src/compute/src/gui/views/gameplay.rs @@ -6,8 +6,12 @@ use std::path::PathBuf; use crate::{ BreakthroughConfig, agents::{ - AgentConfig, AgentStatsAccumulator, BreakthroughAgent, CommonMetrics, HumanMetrics, - MctsAgent, MctsMetrics, MinimaxAgent, MinimaxMetrics, append_record_to_jsonl, + AgentConfig, AgentStatsAccumulator, BreakthroughAgent, CommonMetrics, + DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT, DEFAULT_HEURISTIC_DEFENDED_WEIGHT, + DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT, DEFAULT_HEURISTIC_MATERIAL_WEIGHT, + DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON, DEFAULT_MCTS_RAVE_K, HumanMetrics, MctsAgent, + MctsMetrics, MinimaxAgent, MinimaxMetrics, PlayoutStrategy, SelectionStrategy, + append_record_to_jsonl, build_mcts_playout_strategy, build_mcts_selection_strategy, }, core::{Board, BoardConfig, Player, Status}, gui::themes::BoardTheme, @@ -104,9 +108,15 @@ impl GameplayView { if !self.gameplay_controller.is_selecting() { let is_white_turn = self.board.turn.is_white(); + let current_turn = self.board.turn; + let opponent_config = if is_white_turn { + &mut self.config.black_player + } else { + &mut self.config.white_player + }; let opponent_agent = self .gameplay_controller - .get_opponent_agent_mut(self.board.turn); + .get_opponent_agent_mut(current_turn); let (current_player_label, opponent_player_label) = if is_white_turn { (Player::White.to_string(), Player::Black.to_string()) } else { @@ -126,7 +136,12 @@ impl GameplayView { Self::show_minimax_settings(ui, minimax, &opponent_player_label); } BreakthroughAgent::Mcts(mcts) => { - Self::show_mcts_settings(ui, mcts, &opponent_player_label); + Self::show_mcts_settings( + ui, + mcts, + opponent_config, + &opponent_player_label, + ); } BreakthroughAgent::Human => { ui.label(format!("Opponent ({opponent_player_label}) is also Human.",)); @@ -171,7 +186,12 @@ impl GameplayView { ui.add(egui::Slider::new(&mut agent.max_depth, 1..=8).text("Search Depth")); } - fn show_mcts_settings(ui: &mut egui::Ui, agent: &mut MctsAgent, player_label: &str) { + fn show_mcts_settings( + ui: &mut egui::Ui, + agent: &mut MctsAgent, + config: &mut AgentConfig, + player_label: &str, + ) { ui.label(egui::RichText::new(format!("{player_label} Player (MCTS):")).strong()); ui.add_space(5.0); ui.horizontal(|ui| { @@ -180,30 +200,159 @@ impl GameplayView { .clicked() { agent.max_time_ms = None; + if let AgentConfig::Mcts { max_time_ms, .. } = config { + *max_time_ms = None; + } } if ui .radio(agent.max_time_ms.is_some(), "Time Limit") .clicked() { agent.max_time_ms = Some(1000); + if let AgentConfig::Mcts { max_time_ms, .. } = config { + *max_time_ms = agent.max_time_ms; + } } }); if let Some(ref mut max_time) = agent.max_time_ms { - ui.add( - egui::Slider::new(max_time, 100..=10000) - .text("ms") - .logarithmic(true), - ); + if ui + .add( + egui::Slider::new(max_time, 100..=10000) + .text("ms") + .logarithmic(true), + ) + .changed() + && let AgentConfig::Mcts { max_time_ms, .. } = config + { + *max_time_ms = Some(*max_time); + } } else { - ui.add( - egui::Slider::new(&mut agent.max_iterations, 1000..=100000) - .text("iters") - .logarithmic(true), + if ui + .add( + egui::Slider::new(&mut agent.max_iterations, 1000..=100000) + .text("iters") + .logarithmic(true), + ) + .changed() + && let AgentConfig::Mcts { max_iterations, .. } = config + { + *max_iterations = agent.max_iterations; + } + } + + if ui + .add(egui::Slider::new(&mut agent.exploration_constant, 0.0..=5.0).text("exploration")) + .changed() + && let AgentConfig::Mcts { + exploration_constant, + .. + } = config + { + *exploration_constant = agent.exploration_constant; + } + + ui.separator(); + + let mut use_rave = matches!(agent.selection_strategy, SelectionStrategy::Rave { .. }); + if ui.checkbox(&mut use_rave, "RAVE").changed() { + let rave_k = match agent.selection_strategy { + SelectionStrategy::Rave { k } => k, + SelectionStrategy::Ucb1 => { + if let AgentConfig::Mcts { rave_k, .. } = config { + *rave_k + } else { + DEFAULT_MCTS_RAVE_K + } + } + }; + agent.selection_strategy = build_mcts_selection_strategy(use_rave, rave_k); + if let AgentConfig::Mcts { + use_rave: config_use_rave, + rave_k: config_rave_k, + .. + } = config + { + *config_use_rave = use_rave; + *config_rave_k = rave_k; + } + } + + if let SelectionStrategy::Rave { k } = &mut agent.selection_strategy + && ui + .add( + egui::Slider::new(k, 10.0..=10000.0) + .text("RAVE k") + .logarithmic(true), + ) + .changed() + && let AgentConfig::Mcts { rave_k, .. } = config + { + *rave_k = *k; + } + + let mut use_heavy_playouts = + matches!(agent.playout_strategy, PlayoutStrategy::Heavy { .. }); + if ui + .checkbox(&mut use_heavy_playouts, "Heavy playouts") + .changed() + { + let (epsilon, material, advancement, defended, edge_penalty) = + if let AgentConfig::Mcts { + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, + .. + } = config + { + ( + *heavy_playouts_epsilon, + *material_weight, + *advancement_weight, + *defended_weight, + *edge_penalty_weight, + ) + } else { + ( + DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON, + DEFAULT_HEURISTIC_MATERIAL_WEIGHT, + DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT, + DEFAULT_HEURISTIC_DEFENDED_WEIGHT, + DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT, + ) + }; + + agent.playout_strategy = build_mcts_playout_strategy( + use_heavy_playouts, + epsilon, + material, + advancement, + defended, + edge_penalty, ); + + if let AgentConfig::Mcts { + use_heavy_playouts: config_use_heavy_playouts, + .. + } = config + { + *config_use_heavy_playouts = use_heavy_playouts; + } } - ui.add(egui::Slider::new(&mut agent.exploration_constant, 0.0..=5.0).text("exploration")); + if let PlayoutStrategy::Heavy { epsilon, .. } = &mut agent.playout_strategy + && ui + .add(egui::Slider::new(epsilon, 0.0..=1.0).text("epsilon")) + .changed() + && let AgentConfig::Mcts { + heavy_playouts_epsilon, + .. + } = config + { + *heavy_playouts_epsilon = *epsilon; + } } fn show_agent_thinking(ui: &mut egui::Ui) { From 3f959ee7ee85cf82303d6fdc971f5cc8f79835bd Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Tue, 19 May 2026 21:14:05 +0200 Subject: [PATCH 11/13] Naprawione errory clippy --- src/compute/src/agents/mcts.rs | 2 +- src/compute/src/agents/mcts/rave.rs | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index 4b50522..f16d37f 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -242,7 +242,7 @@ impl MctsAgent { /// Combined phase 3 (Playout) and 4 (Backpropagation) fn do_playout_and_backprop( &mut self, - tree: &mut Vec, + tree: &mut [MctsNode], current_idx: usize, sim_board: &mut Board, config: &BoardConfig, diff --git a/src/compute/src/agents/mcts/rave.rs b/src/compute/src/agents/mcts/rave.rs index 2242fa4..0d9ad3c 100644 --- a/src/compute/src/agents/mcts/rave.rs +++ b/src/compute/src/agents/mcts/rave.rs @@ -5,13 +5,13 @@ use super::{did_player_win, node::MctsNode}; #[derive(Debug)] pub enum RaveTracker { Disabled, - Enabled(MoveTracking), + Enabled(Box), } impl RaveTracker { pub fn new(enabled: bool) -> Self { if enabled { - Self::Enabled(MoveTracking::new()) + Self::Enabled(Box::new(MoveTracking::new())) } else { Self::Disabled } @@ -41,12 +41,12 @@ impl RaveTracker { for child_position in 0..children_len { let child_idx = tree[node_idx].children[child_position]; let child = &mut tree[child_idx]; - if let Some(ply) = child.ply_to_reach { - if tracking.contains_move(node_turn, ply) { - child.amaf.visits += 1; - if node_turn_won { - child.amaf.wins += 1.0; - } + if let Some(ply) = child.ply_to_reach + && tracking.contains_move(node_turn, ply) + { + child.amaf.visits += 1; + if node_turn_won { + child.amaf.wins += 1.0; } } } From 7c008d47af43698eecafabf2e65ea18251ba2b9f Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Wed, 20 May 2026 11:08:38 +0200 Subject: [PATCH 12/13] Dodane poprawki do PR --- src/compute/src/agents.rs | 60 ++++++++--- src/compute/src/agents/mcts.rs | 41 +++---- src/compute/src/agents/mcts/node.rs | 1 - .../src/agents/mcts/playout_strategy.rs | 8 ++ src/compute/src/agents/mcts/rave.rs | 15 +-- .../src/agents/mcts/selection_strategy.rs | 16 +-- src/compute/src/bin/tournament.rs | 78 +++++--------- src/compute/src/core/status.rs | 9 ++ src/compute/src/gui/views/gameplay.rs | 101 +++++++----------- 9 files changed, 160 insertions(+), 169 deletions(-) diff --git a/src/compute/src/agents.rs b/src/compute/src/agents.rs index cdd1652..69e0442 100644 --- a/src/compute/src/agents.rs +++ b/src/compute/src/agents.rs @@ -15,14 +15,17 @@ use serde::{Deserialize, Serialize}; use crate::core::{Board, BoardConfig, Ply}; pub const DEFAULT_MINIMAX_MAX_DEPTH: u8 = 4; -pub const DEFAULT_HEURISTIC_MATERIAL_WEIGHT: i32 = 100; -pub const DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT: i32 = 10; +pub const DEFAULT_HEURISTIC_MATERIAL_WEIGHT: i32 = 10; +pub const DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT: i32 = 40; pub const DEFAULT_HEURISTIC_DEFENDED_WEIGHT: i32 = 5; pub const DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT: i32 = -2; pub const DEFAULT_MCTS_MAX_ITERATIONS: u32 = 75000; +pub const DEFAULT_MCTS_MAX_TIME_MS: Option = None; pub const DEFAULT_MCTS_EXPLORATION_CONSTANT: f64 = 1.41; -pub const DEFAULT_MCTS_RAVE_K: f64 = 1000.0; +pub const DEFAULT_MCTS_USE_HEAVY_PLAYOUTS: bool = false; pub const DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON: f64 = 0.1; +pub const DEFAULT_MCTS_USE_RAVE: bool = false; +pub const DEFAULT_MCTS_RAVE_K: f64 = 1000.0; fn default_max_depth() -> u8 { DEFAULT_MINIMAX_MAX_DEPTH @@ -45,19 +48,19 @@ fn default_max_iterations() -> u32 { DEFAULT_MCTS_MAX_ITERATIONS } fn default_max_time_ms() -> Option { - None + DEFAULT_MCTS_MAX_TIME_MS } fn default_exploration_constant() -> f64 { DEFAULT_MCTS_EXPLORATION_CONSTANT } fn default_use_heavy_playouts() -> bool { - false + DEFAULT_MCTS_USE_HEAVY_PLAYOUTS } fn default_heavy_playouts_epsilon() -> f64 { DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON } fn default_use_rave() -> bool { - false + DEFAULT_MCTS_USE_RAVE } fn default_rave_k() -> f64 { DEFAULT_MCTS_RAVE_K @@ -65,9 +68,9 @@ fn default_rave_k() -> f64 { pub fn build_mcts_selection_strategy(use_rave: bool, rave_k: f64) -> SelectionStrategy { if use_rave { - SelectionStrategy::Rave { k: rave_k } + SelectionStrategy::rave(rave_k) } else { - SelectionStrategy::Ucb1 + SelectionStrategy::ucb1() } } @@ -86,12 +89,45 @@ pub fn build_mcts_playout_strategy( defended_weight, edge_penalty_weight, )); - PlayoutStrategy::Heavy { - evaluator, - epsilon: heavy_playouts_epsilon, + PlayoutStrategy::heavy(evaluator, heavy_playouts_epsilon) + } else { + PlayoutStrategy::random() + } +} + +#[derive(Debug, Clone, Copy)] +pub struct HeavyPlayoutMetricsOptions { + pub epsilon: Option, + pub material_weight: Option, + pub advancement_weight: Option, + pub defended_weight: Option, + pub edge_penalty_weight: Option, +} + +pub fn heavy_playout_metrics_options( + use_heavy_playouts: bool, + heavy_playouts_epsilon: f64, + material_weight: i32, + advancement_weight: i32, + defended_weight: i32, + edge_penalty_weight: i32, +) -> HeavyPlayoutMetricsOptions { + if use_heavy_playouts { + HeavyPlayoutMetricsOptions { + epsilon: Some(heavy_playouts_epsilon), + material_weight: Some(material_weight), + advancement_weight: Some(advancement_weight), + defended_weight: Some(defended_weight), + edge_penalty_weight: Some(edge_penalty_weight), } } else { - PlayoutStrategy::Random + HeavyPlayoutMetricsOptions { + epsilon: None, + material_weight: None, + advancement_weight: None, + defended_weight: None, + edge_penalty_weight: None, + } } } diff --git a/src/compute/src/agents/mcts.rs b/src/compute/src/agents/mcts.rs index f16d37f..18eadb7 100644 --- a/src/compute/src/agents/mcts.rs +++ b/src/compute/src/agents/mcts.rs @@ -18,15 +18,7 @@ use rand::{SeedableRng, seq::SliceRandom}; use std::time::Instant; use super::{Agent, AgentStats}; -use crate::core::{Board, BoardConfig, Player, Ply, Status}; - -pub(super) fn did_player_win(status: Status, player: Player) -> bool { - match status { - Status::WhiteWon => player.is_white(), - Status::BlackWon => player.is_black(), - Status::Ongoing => false, - } -} +use crate::core::{Board, BoardConfig, Ply, Status}; #[derive(Debug)] pub struct MctsAgent { @@ -111,7 +103,7 @@ impl Agent for MctsAgent { let mut sim_board = *board; // Phase 1: Selection - current_idx = self.select_node(&tree, &mut sim_board, config, current_idx); + current_idx = self.select_node(&tree, &mut sim_board, current_idx); // Phase 2: Expansion let (leaf_idx, status) = @@ -160,14 +152,11 @@ impl MctsAgent { &self, tree: &[MctsNode], sim_board: &mut Board, - _config: &BoardConfig, mut current_idx: usize, ) -> usize { while tree[current_idx].unexpanded_plies.is_empty() && !tree[current_idx].children.is_empty() { - let parent_visits = tree[current_idx].visits; - let c = self.exploration_constant; let mut best_child_idx = 0; let mut best_score = f64::NEG_INFINITY; @@ -184,8 +173,8 @@ impl MctsAgent { child.wins, child.amaf.visits, child.amaf.wins, - parent_visits, - c, + tree[current_idx].visits, + self.exploration_constant, ); if score > best_score { @@ -297,7 +286,7 @@ impl MctsAgent { tree[leaf_idx].visits += 1; let player_who_just_moved = node_turn.opponent(); - if did_player_win(status, player_who_just_moved) { + if status.is_won_by(player_who_just_moved) { tree[leaf_idx].wins += 1.0; } @@ -333,18 +322,17 @@ mod tests { } #[test] - fn did_player_win_matches_terminal_status() { - assert!(did_player_win(Status::WhiteWon, Player::White)); - assert!(!did_player_win(Status::WhiteWon, Player::Black)); - assert!(did_player_win(Status::BlackWon, Player::Black)); - assert!(!did_player_win(Status::BlackWon, Player::White)); - assert!(!did_player_win(Status::Ongoing, Player::White)); - assert!(!did_player_win(Status::Ongoing, Player::Black)); + fn status_winner_check_matches_terminal_status() { + assert!(Status::WhiteWon.is_won_by(Player::White)); + assert!(!Status::WhiteWon.is_won_by(Player::Black)); + assert!(Status::BlackWon.is_won_by(Player::Black)); + assert!(!Status::BlackWon.is_won_by(Player::White)); + assert!(!Status::Ongoing.is_won_by(Player::White)); + assert!(!Status::Ongoing.is_won_by(Player::Black)); } #[test] fn select_node_stops_at_node_with_unexpanded_plies() { - let config = BoardConfig::new(3, 3); let mut board = Board { white_board: 1 << 3, black_board: 1 << 8, @@ -363,7 +351,7 @@ mod tests { let tree = vec![root, child]; let agent = test_agent(SelectionStrategy::Ucb1); - let selected_idx = agent.select_node(&tree, &mut board, &config, 0); + let selected_idx = agent.select_node(&tree, &mut board, 0); assert_eq!(selected_idx, 0); assert_eq!(board, original_board); @@ -371,7 +359,6 @@ mod tests { #[test] fn select_node_descends_to_highest_scoring_child_and_applies_ply() { - let config = BoardConfig::new(3, 3); let mut board = Board { white_board: 1 << 3, black_board: 1 << 8, @@ -402,7 +389,7 @@ mod tests { let tree = vec![root, good_child, bad_child]; let agent = test_agent(SelectionStrategy::Ucb1); - let selected_idx = agent.select_node(&tree, &mut board, &config, 0); + let selected_idx = agent.select_node(&tree, &mut board, 0); assert_eq!(selected_idx, 1); assert_eq!(board.turn, Player::Black); diff --git a/src/compute/src/agents/mcts/node.rs b/src/compute/src/agents/mcts/node.rs index 3d3de57..f05a957 100644 --- a/src/compute/src/agents/mcts/node.rs +++ b/src/compute/src/agents/mcts/node.rs @@ -30,7 +30,6 @@ impl MctsNode { ) -> Self { Self { parent, - children: Vec::new(), ply_to_reach, unexpanded_plies, turn, diff --git a/src/compute/src/agents/mcts/playout_strategy.rs b/src/compute/src/agents/mcts/playout_strategy.rs index ab4a8d6..841a2d5 100644 --- a/src/compute/src/agents/mcts/playout_strategy.rs +++ b/src/compute/src/agents/mcts/playout_strategy.rs @@ -17,6 +17,14 @@ pub enum PlayoutStrategy { } impl PlayoutStrategy { + pub fn random() -> Self { + Self::Random + } + + pub fn heavy(evaluator: Box, epsilon: f64) -> Self { + Self::Heavy { evaluator, epsilon } + } + /// Select a move according to this strategy pub fn select_move( &self, diff --git a/src/compute/src/agents/mcts/rave.rs b/src/compute/src/agents/mcts/rave.rs index 0d9ad3c..dbca1ab 100644 --- a/src/compute/src/agents/mcts/rave.rs +++ b/src/compute/src/agents/mcts/rave.rs @@ -1,6 +1,8 @@ use crate::core::{Player, Ply, Status}; -use super::{did_player_win, node::MctsNode}; +use super::node::MctsNode; + +const MAX_BOARD_SQUARES: usize = 128; #[derive(Debug)] pub enum RaveTracker { @@ -35,7 +37,7 @@ impl RaveTracker { }; let node_turn = tree[node_idx].turn; - let node_turn_won = did_player_win(status, node_turn); + let node_turn_won = status.is_won_by(node_turn); let children_len = tree[node_idx].children.len(); for child_position in 0..children_len { @@ -55,15 +57,16 @@ impl RaveTracker { #[derive(Debug)] pub struct MoveTracking { - white_moves: [[bool; 128]; 128], - black_moves: [[bool; 128]; 128], + // The board is represented as u128 bitboards, so legal square ids are always in 0..128. + white_moves: [[bool; MAX_BOARD_SQUARES]; MAX_BOARD_SQUARES], + black_moves: [[bool; MAX_BOARD_SQUARES]; MAX_BOARD_SQUARES], } impl MoveTracking { fn new() -> Self { Self { - white_moves: [[false; 128]; 128], - black_moves: [[false; 128]; 128], + white_moves: [[false; MAX_BOARD_SQUARES]; MAX_BOARD_SQUARES], + black_moves: [[false; MAX_BOARD_SQUARES]; MAX_BOARD_SQUARES], } } diff --git a/src/compute/src/agents/mcts/selection_strategy.rs b/src/compute/src/agents/mcts/selection_strategy.rs index cc74d76..788e248 100644 --- a/src/compute/src/agents/mcts/selection_strategy.rs +++ b/src/compute/src/agents/mcts/selection_strategy.rs @@ -8,15 +8,15 @@ pub enum SelectionStrategy { } impl SelectionStrategy { + pub fn ucb1() -> Self { + Self::Ucb1 + } + + pub fn rave(k: f64) -> Self { + Self::Rave { k } + } + /// Computes the UCT score for a child node - /// - /// # Arguments - /// * `child_visits` - Number of times this child has been visited - /// * `child_wins` - Total wins accumulated at this child - /// * `child_amaf_visits` - AMAF visits (only used for RAVE) - /// * `child_amaf_wins` - AMAF wins (only used for RAVE) - /// * `parent_visits` - Number of times the parent has been visited - /// * `exploration_constant` - UCB1 exploration parameter (typically ~1.41) pub fn compute_uct_score( &self, child_visits: u32, diff --git a/src/compute/src/bin/tournament.rs b/src/compute/src/bin/tournament.rs index 7e40be8..8b7ea14 100644 --- a/src/compute/src/bin/tournament.rs +++ b/src/compute/src/bin/tournament.rs @@ -5,7 +5,7 @@ use miette::{IntoDiagnostic, Result, miette}; use compute::{ BreakthroughConfig, agents::{ - AgentConfig, AgentRuntimeStats, AgentStatsAccumulator, AgentType, CommonMetrics, + self, AgentConfig, AgentRuntimeStats, AgentStatsAccumulator, AgentType, CommonMetrics, HumanMetrics, MctsMetrics, MinimaxMetrics, append_record_to_jsonl, }, cli::build_command, @@ -158,6 +158,14 @@ fn main() -> Result<()> { else { unreachable!(); }; + let heavy_options = agents::heavy_playout_metrics_options( + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, + ); let metrics = MctsMetrics { common: white_common, max_iterations, @@ -168,31 +176,11 @@ fn main() -> Result<()> { rave_k: if use_rave { Some(rave_k) } else { None }, - heavy_playouts_epsilon: if use_heavy_playouts { - Some(heavy_playouts_epsilon) - } else { - None - }, - material_weight: if use_heavy_playouts { - Some(material_weight) - } else { - None - }, - advancement_weight: if use_heavy_playouts { - Some(advancement_weight) - } else { - None - }, - defended_weight: if use_heavy_playouts { - Some(defended_weight) - } else { - None - }, - edge_penalty_weight: if use_heavy_playouts { - Some(edge_penalty_weight) - } else { - None - }, + heavy_playouts_epsilon: heavy_options.epsilon, + material_weight: heavy_options.material_weight, + advancement_weight: heavy_options.advancement_weight, + defended_weight: heavy_options.defended_weight, + edge_penalty_weight: heavy_options.edge_penalty_weight, total_iterations: acc.total_iterations, total_nodes_created: acc.total_nodes_created, @@ -263,6 +251,14 @@ fn main() -> Result<()> { else { unreachable!(); }; + let heavy_options = agents::heavy_playout_metrics_options( + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, + ); let metrics = MctsMetrics { common: black_common, max_iterations, @@ -274,31 +270,11 @@ fn main() -> Result<()> { rave_k: if use_rave { Some(rave_k) } else { None }, - heavy_playouts_epsilon: if use_heavy_playouts { - Some(heavy_playouts_epsilon) - } else { - None - }, - material_weight: if use_heavy_playouts { - Some(material_weight) - } else { - None - }, - advancement_weight: if use_heavy_playouts { - Some(advancement_weight) - } else { - None - }, - defended_weight: if use_heavy_playouts { - Some(defended_weight) - } else { - None - }, - edge_penalty_weight: if use_heavy_playouts { - Some(edge_penalty_weight) - } else { - None - }, + heavy_playouts_epsilon: heavy_options.epsilon, + material_weight: heavy_options.material_weight, + advancement_weight: heavy_options.advancement_weight, + defended_weight: heavy_options.defended_weight, + edge_penalty_weight: heavy_options.edge_penalty_weight, total_iterations: acc.total_iterations, total_nodes_created: acc.total_nodes_created, diff --git a/src/compute/src/core/status.rs b/src/compute/src/core/status.rs index e65b37c..8b5ba9a 100644 --- a/src/compute/src/core/status.rs +++ b/src/compute/src/core/status.rs @@ -1,5 +1,7 @@ use strum_macros::Display; +use super::Player; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] pub enum Status { Ongoing, @@ -19,4 +21,11 @@ impl Status { pub fn is_black_won(&self) -> bool { matches!(self, Status::BlackWon) } + + pub fn is_won_by(&self, player: Player) -> bool { + matches!( + (self, player), + (Status::WhiteWon, Player::White) | (Status::BlackWon, Player::Black) + ) + } } diff --git a/src/compute/src/gui/views/gameplay.rs b/src/compute/src/gui/views/gameplay.rs index 6e1f672..d19cd1d 100644 --- a/src/compute/src/gui/views/gameplay.rs +++ b/src/compute/src/gui/views/gameplay.rs @@ -6,12 +6,9 @@ use std::path::PathBuf; use crate::{ BreakthroughConfig, agents::{ - AgentConfig, AgentStatsAccumulator, BreakthroughAgent, CommonMetrics, - DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT, DEFAULT_HEURISTIC_DEFENDED_WEIGHT, - DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT, DEFAULT_HEURISTIC_MATERIAL_WEIGHT, - DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON, DEFAULT_MCTS_RAVE_K, HumanMetrics, MctsAgent, - MctsMetrics, MinimaxAgent, MinimaxMetrics, PlayoutStrategy, SelectionStrategy, - append_record_to_jsonl, build_mcts_playout_strategy, build_mcts_selection_strategy, + self, AgentConfig, AgentStatsAccumulator, BreakthroughAgent, CommonMetrics, HumanMetrics, + MctsAgent, MctsMetrics, MinimaxAgent, MinimaxMetrics, PlayoutStrategy, SelectionStrategy, + append_record_to_jsonl, }, core::{Board, BoardConfig, Player, Status}, gui::themes::BoardTheme, @@ -262,11 +259,11 @@ impl GameplayView { if let AgentConfig::Mcts { rave_k, .. } = config { *rave_k } else { - DEFAULT_MCTS_RAVE_K + agents::DEFAULT_MCTS_RAVE_K } } }; - agent.selection_strategy = build_mcts_selection_strategy(use_rave, rave_k); + agent.selection_strategy = agents::build_mcts_selection_strategy(use_rave, rave_k); if let AgentConfig::Mcts { use_rave: config_use_rave, rave_k: config_rave_k, @@ -316,15 +313,15 @@ impl GameplayView { ) } else { ( - DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON, - DEFAULT_HEURISTIC_MATERIAL_WEIGHT, - DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT, - DEFAULT_HEURISTIC_DEFENDED_WEIGHT, - DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT, + agents::DEFAULT_MCTS_HEAVY_PLAYOUTS_EPSILON, + agents::DEFAULT_HEURISTIC_MATERIAL_WEIGHT, + agents::DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT, + agents::DEFAULT_HEURISTIC_DEFENDED_WEIGHT, + agents::DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT, ) }; - agent.playout_strategy = build_mcts_playout_strategy( + agent.playout_strategy = agents::build_mcts_playout_strategy( use_heavy_playouts, epsilon, material, @@ -728,6 +725,14 @@ impl GameplayView { else { unreachable!(); }; + let heavy_options = agents::heavy_playout_metrics_options( + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, + ); let metrics = MctsMetrics { common: white_common, max_iterations, @@ -738,31 +743,11 @@ impl GameplayView { rave_k: if use_rave { Some(rave_k) } else { None }, - heavy_playouts_epsilon: if use_heavy_playouts { - Some(heavy_playouts_epsilon) - } else { - None - }, - material_weight: if use_heavy_playouts { - Some(material_weight) - } else { - None - }, - advancement_weight: if use_heavy_playouts { - Some(advancement_weight) - } else { - None - }, - defended_weight: if use_heavy_playouts { - Some(defended_weight) - } else { - None - }, - edge_penalty_weight: if use_heavy_playouts { - Some(edge_penalty_weight) - } else { - None - }, + heavy_playouts_epsilon: heavy_options.epsilon, + material_weight: heavy_options.material_weight, + advancement_weight: heavy_options.advancement_weight, + defended_weight: heavy_options.defended_weight, + edge_penalty_weight: heavy_options.edge_penalty_weight, total_iterations: acc.total_iterations, total_nodes_created: acc.total_nodes_created, }; @@ -832,6 +817,14 @@ impl GameplayView { else { unreachable!(); }; + let heavy_options = agents::heavy_playout_metrics_options( + use_heavy_playouts, + heavy_playouts_epsilon, + material_weight, + advancement_weight, + defended_weight, + edge_penalty_weight, + ); let metrics = MctsMetrics { common: black_common, max_iterations, @@ -842,31 +835,11 @@ impl GameplayView { rave_k: if use_rave { Some(rave_k) } else { None }, - heavy_playouts_epsilon: if use_heavy_playouts { - Some(heavy_playouts_epsilon) - } else { - None - }, - material_weight: if use_heavy_playouts { - Some(material_weight) - } else { - None - }, - advancement_weight: if use_heavy_playouts { - Some(advancement_weight) - } else { - None - }, - defended_weight: if use_heavy_playouts { - Some(defended_weight) - } else { - None - }, - edge_penalty_weight: if use_heavy_playouts { - Some(edge_penalty_weight) - } else { - None - }, + heavy_playouts_epsilon: heavy_options.epsilon, + material_weight: heavy_options.material_weight, + advancement_weight: heavy_options.advancement_weight, + defended_weight: heavy_options.defended_weight, + edge_penalty_weight: heavy_options.edge_penalty_weight, total_iterations: acc.total_iterations, total_nodes_created: acc.total_nodes_created, }; From 6cffa96b74535121674cb35912f069a098013fce Mon Sep 17 00:00:00 2001 From: p10tr13 <160889621+p10tr13@users.noreply.github.com> Date: Wed, 20 May 2026 11:34:03 +0200 Subject: [PATCH 13/13] =?UTF-8?q?Zmienione=20sta=C5=82e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/compute/src/agents.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compute/src/agents.rs b/src/compute/src/agents.rs index 69e0442..e85a7aa 100644 --- a/src/compute/src/agents.rs +++ b/src/compute/src/agents.rs @@ -15,8 +15,8 @@ use serde::{Deserialize, Serialize}; use crate::core::{Board, BoardConfig, Ply}; pub const DEFAULT_MINIMAX_MAX_DEPTH: u8 = 4; -pub const DEFAULT_HEURISTIC_MATERIAL_WEIGHT: i32 = 10; -pub const DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT: i32 = 40; +pub const DEFAULT_HEURISTIC_MATERIAL_WEIGHT: i32 = 200; +pub const DEFAULT_HEURISTIC_ADVANCEMENT_WEIGHT: i32 = 5; pub const DEFAULT_HEURISTIC_DEFENDED_WEIGHT: i32 = 5; pub const DEFAULT_HEURISTIC_EDGE_PENALTY_WEIGHT: i32 = -2; pub const DEFAULT_MCTS_MAX_ITERATIONS: u32 = 75000;