diff --git a/battlesnake/src/logic.rs b/battlesnake/src/logic.rs index a0bd6cd..0c8fcf9 100644 --- a/battlesnake/src/logic.rs +++ b/battlesnake/src/logic.rs @@ -11,9 +11,14 @@ // For more info see docs.battlesnake.com use core::f64; -use std::collections::BTreeMap; +use std::{ + cell::Cell, + collections::{BTreeMap, HashMap}, + sync::{Arc, LazyLock, Mutex}, + time::{Duration, Instant}, +}; -use log::info; +use log::{error, info}; use ordered_float::OrderedFloat; use serde_json::{json, Value}; @@ -101,41 +106,92 @@ pub fn info() -> Value { }) } +#[derive(Debug, PartialEq, Eq, Clone)] +struct GameInfo { + calculation_time: Cell, + token_mapping: Arc>, + my_token: SnakeToken, +} + +static GAME_INFOS: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + // start is called when your Battlesnake begins a game -pub fn start(_game: &Game, _turn: i32, _board: &Board, _you: &Battlesnake) { +pub fn start(game: &Game, _turn: i32, board: &Board, you: &Battlesnake) { info!("GAME START"); + let token_mapping = Arc::new(SnakeToken::from_board(board)); + let my_token = token_mapping[&you.id]; + let Ok(mut game_infos) = GAME_INFOS.lock() else { + error!("unable to lock game infos"); + return; + }; + game_infos.insert( + (game.id.clone(), you.id.clone()), + GameInfo { + calculation_time: Cell::new(Duration::from_millis(50)), + token_mapping, + my_token, + }, + ); } // end is called when your Battlesnake finishes a game -pub fn end(_game: &Game, _turn: i32, _board: &Board, _you: &Battlesnake) { +pub fn end(game: &Game, _turn: i32, _board: &Board, you: &Battlesnake) { info!("GAME OVER"); + let Ok(mut game_infos) = GAME_INFOS.lock() else { + error!("unable to lock game infos"); + return; + }; + game_infos.remove(&(game.id.clone(), you.id.clone())); } // move is called on every turn and returns your next move // Valid moves are "up", "down", "left", or "right" // See https://docs.battlesnake.com/api/example-move for available data pub fn get_move(game: &Game, turn: i32, board: &Board, you: &Battlesnake) -> Option { - let token_map = SnakeToken::from_board(board); + let start = Instant::now(); + let game_info = GAME_INFOS + .lock() + .ok() + .and_then(|guard| guard.get(&(game.id.clone(), you.id.clone())).cloned()) + .unwrap_or_else(|| { + let token_mapping = Arc::new(SnakeToken::from_board(board)); + let my_token = token_mapping[&you.id]; + GameInfo { + calculation_time: Cell::new(Duration::from_millis(50)), + token_mapping, + my_token, + } + }); + + // do some latency compensation + game_info.calculation_time.set( + game_info.calculation_time.get() + + Duration::from_millis( + u64::from( + game.timeout * 3 / 4 - you.latency.parse().unwrap_or(game.timeout * 3 / 4), + ) / 3, + ), + ); + let board = simulation::Board::from_game_board( board, - &token_map, + &game_info.token_mapping, turn, game.ruleset.settings.food_spawn_chance, game.ruleset.settings.minimum_food, ); - let my_token = token_map[&you.id]; - let mut tree = Node::default(); - for _ in 0..300 { + while start.elapsed() < game_info.calculation_time.get() { let mut board = board.clone(); tree.monte_carlo_step(&mut board); } - let actions = tree.child_statistics.entry(my_token).or_default(); + let actions = tree.child_statistics.entry(game_info.my_token).or_default(); - info!("actions: {actions:?}"); + info!("actions {}: {actions:?}", you.name); #[allow(clippy::cast_precision_loss)] let chosen = actions @@ -143,7 +199,11 @@ pub fn get_move(game: &Game, turn: i32, board: &Board, you: &Battlesnake) -> Opt .max_by_key(|(_, stat)| OrderedFloat(stat.won as f64 / stat.played as f64)) .map(|(direction, _)| *direction)?; - info!("DIRECTION {}: {:?}", turn, chosen); + info!( + "DIRECTION {turn}: {chosen:?} after {}ms ({})", + start.elapsed().as_millis(), + you.name, + ); Some(Action { r#move: chosen, shout: None, @@ -186,28 +246,23 @@ impl Node { let actions = possible_actions .iter() - .map(|(token, actions)| { + .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 / 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) - }) - .unwrap_or_default(); - (*token, selected) + 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 / 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(); diff --git a/battlesnake/src/main.rs b/battlesnake/src/main.rs index cafd627..21bb033 100644 --- a/battlesnake/src/main.rs +++ b/battlesnake/src/main.rs @@ -19,23 +19,11 @@ const MAX_HEALTH: i32 = 100; // See https://docs.battlesnake.com/api #[derive( - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - Deserialize, - Serialize, - Sequence, - Default, + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Deserialize, Serialize, Sequence, )] #[serde(rename_all = "lowercase")] pub enum Direction { /// Move left (-x) - #[default] Left, /// Move up (+y) Up, diff --git a/battlesnake/src/simulation.rs b/battlesnake/src/simulation.rs index 4947210..6ef3751 100644 --- a/battlesnake/src/simulation.rs +++ b/battlesnake/src/simulation.rs @@ -92,7 +92,7 @@ impl Board { pub fn simulate_actions(&mut self, actions: &BTreeMap) { // move snakes for (token, snake) in &mut self.snakes { - snake.perform_action(actions.get(token).copied().unwrap_or_default()); + snake.perform_action(actions.get(token).copied().unwrap_or(Direction::Up)); } // feed snakes @@ -170,7 +170,7 @@ impl Board { .iter() .choose(&mut rand::thread_rng()) .copied() - .unwrap_or_default(), + .unwrap_or(Direction::Up), ) }) .collect();