From 11ba9b7c940e3b0bac3b807284a08bc4e5bdfc40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=A4nner?= Date: Thu, 3 Oct 2024 04:20:52 +0200 Subject: [PATCH] solo mode (not verry good) --- battlesnake/src/logic.rs | 90 ++++++++++++++++++++++++++++++++++- battlesnake/src/simulation.rs | 11 ++++- 2 files changed, 99 insertions(+), 2 deletions(-) diff --git a/battlesnake/src/logic.rs b/battlesnake/src/logic.rs index 0c8fcf9..d1e1bdb 100644 --- a/battlesnake/src/logic.rs +++ b/battlesnake/src/logic.rs @@ -186,7 +186,11 @@ pub fn get_move(game: &Game, turn: i32, board: &Board, you: &Battlesnake) -> Opt while start.elapsed() < game_info.calculation_time.get() { let mut board = board.clone(); - tree.monte_carlo_step(&mut board); + if game.ruleset.name == "solo" { + tree.monte_carlo_step_solo(&mut board); + } else { + tree.monte_carlo_step(&mut board); + } } let actions = tree.child_statistics.entry(game_info.my_token).or_default(); @@ -299,4 +303,88 @@ impl Node { } winner } + + /// Performs one monte carlo simulation step for a solo game + /// + /// Returns the lengths before death + fn monte_carlo_step_solo( + &mut self, + board: &mut simulation::Board, + ) -> BTreeMap { + let lengths = if self.statistic.played == 0 { + // We didn't simulate a game for this node yet. Do that + let mut lengths: BTreeMap<_, _> = board + .snakes() + .filter_map(|snake| Some((snake, board.snake_length(snake)?))) + .collect(); + board.simulate_until(|board| { + for snake in board.snakes() { + if let Some(length) = board.snake_length(snake) { + lengths.insert(snake, length); + } + } + board.alive_snakes() == 0 + }); + lengths + } else { + // select a node to simulate + let possible_actions = board.possible_actions(); + + let actions = possible_actions + .iter() + .filter_map(|(token, actions)| { + let statistics = self.child_statistics.entry(*token).or_default(); + let selected = actions.iter().copied().max_by_key(|direction| { + let statistics = statistics.entry(*direction).or_default(); + if statistics.played == 0 { + return OrderedFloat(f64::INFINITY); + } + #[allow(clippy::cast_precision_loss)] + let exploitation = statistics.won as f64 + / board.spaces() as f64 + / statistics.played as f64; + #[allow(clippy::cast_precision_loss)] + let exploration = f64::consts::SQRT_2 + * f64::sqrt( + f64::ln(self.statistic.played as f64) / statistics.played as f64, + ); + OrderedFloat(exploitation + exploration) + })?; + Some((*token, selected)) + }) + .collect(); + + board.simulate_actions(&actions); + let lengths = self + .childs + .entry(actions.clone()) + .or_default() + .monte_carlo_step_solo(board); + + // update child statistics + for (token, action) in &actions { + let entry = self + .child_statistics + .entry(*token) + .or_default() + .entry(*action) + .or_default(); + entry.played += 1; + if let Some(length) = lengths.get(&token) { + entry.won += length; + } + } + + lengths + }; + self.statistic.played += 1; + for (token, length) in &lengths { + self.statistic + .won + .entry(*token) + .and_modify(|won| *won += length) + .or_insert(*length); + } + lengths + } } diff --git a/battlesnake/src/simulation.rs b/battlesnake/src/simulation.rs index 6ef3751..9daceb3 100644 --- a/battlesnake/src/simulation.rs +++ b/battlesnake/src/simulation.rs @@ -81,6 +81,11 @@ impl Board { } } + #[allow(clippy::cast_sign_loss)] + pub const fn spaces(&self) -> usize { + self.height as usize * self.width as usize + } + pub fn alive_snakes(&self) -> usize { self.snakes.len() } @@ -89,6 +94,10 @@ impl Board { self.snakes.keys().copied() } + pub fn snake_length(&self, token: SnakeToken) -> Option { + self.snakes.get(&token).map(|snake| snake.body.len()) + } + pub fn simulate_actions(&mut self, actions: &BTreeMap) { // move snakes for (token, snake) in &mut self.snakes { @@ -158,7 +167,7 @@ impl Board { self.turn += 1; } - pub fn simulate_until(&mut self, exit: impl Fn(&Self) -> bool) { + pub fn simulate_until(&mut self, mut exit: impl FnMut(&Self) -> bool) { while !exit(self) { let actions = self .possible_actions()