diff --git a/battlesnake/src/main.rs b/battlesnake/src/main.rs index 6cfc1e0..608e341 100644 --- a/battlesnake/src/main.rs +++ b/battlesnake/src/main.rs @@ -9,10 +9,13 @@ use battlesnake::types::{ wire::{Request, Response}, Direction, }; -use log::{debug, info, warn}; +use log::{debug, error, info, warn}; use rand::prelude::*; use serde::Serialize; -use tokio::{net::TcpListener, time::Instant}; +use tokio::{ + net::TcpListener, + time::{Duration, Instant}, +}; #[tokio::main] async fn main() { @@ -59,20 +62,59 @@ async fn start(request: Json) { } async fn get_move(request: Json) -> response::Json { - let board = Board::from(&*request); - info!("got move request: {board}"); - let actions = board.valid_actions(0).collect::>(); - info!("valid actions: {:?}", actions); let start = Instant::now(); - for _ in 0..100 { - let mut board = board.clone(); - let score = board.simulate_random(|board| (board.num_snakes() <= 1).then_some(1)); - std::hint::black_box(score); + let board = Board::from(&*request); + let id = board.get_id(&request.you.id).unwrap_or_else(|| { + error!("My id is not in the simulation board"); + 0 + }); + info!("got move request: {board}"); + let actions = board.valid_actions(id).collect::>(); + if actions.len() <= 1 { + info!( + "only one possible action. Fast forwarding {:?}", + actions.first() + ); + return response::Json(Response { + direction: actions.first().copied().unwrap_or(Direction::Up), + shout: None, + }); } - let elapsed = start.elapsed(); - debug!("simulated 100 random games in {elapsed:?}"); - let action = actions.choose(&mut thread_rng()).copied(); - if action.is_none() { + info!("valid actions: {:?}", actions); + + let mut action_data = [(0, 0); 4]; + let mut total_simulations = 0; + let base_turns = board.turn(); + while start.elapsed() < Duration::from_millis(250) { + let mut board = board.clone(); + let action = *actions.choose(&mut thread_rng()).unwrap_or(&Direction::Up); + board.next_turn(&[(id, action)]); + let turns = board.simulate_random(|board| { + if board.num_snakes() == 0 + || board.turn() > base_turns + u32::from(request.you.length) * 3 + { + Some(board.turn()) + } else { + None + } + }); + let action_data = &mut action_data[usize::from(action)]; + action_data.0 += turns - base_turns; + action_data.1 += 1; + total_simulations += 1; + } + debug!("action data: {action_data:?}"); + + let action = actions.into_iter().max_by_key(|action| { + let action_data = action_data[usize::from(*action)]; + action_data.0 / action_data.1 + }); + + if let Some(action) = action { + let action_data = action_data[usize::from(action)]; + let avg_turns = action_data.0 / action_data.1; + info!("found action {action:?} after {total_simulations} simulations with an average of {avg_turns} turns."); + } else { warn!("unable to find a valid action"); } info!("chose {action:?}"); diff --git a/battlesnake/src/types/mod.rs b/battlesnake/src/types/mod.rs index 06313d7..fa62d61 100644 --- a/battlesnake/src/types/mod.rs +++ b/battlesnake/src/types/mod.rs @@ -45,3 +45,14 @@ pub enum Direction { /// Move in positive x direction Right, } + +impl From for usize { + fn from(value: Direction) -> Self { + match value { + Direction::Up => 0, + Direction::Down => 1, + Direction::Left => 2, + Direction::Right => 3, + } + } +} diff --git a/battlesnake/src/types/simulation.rs b/battlesnake/src/types/simulation.rs index 1e3e174..2f1522b 100644 --- a/battlesnake/src/types/simulation.rs +++ b/battlesnake/src/types/simulation.rs @@ -187,6 +187,14 @@ impl Display for Board { } impl Board { + #[must_use] + pub fn get_id(&self, str_id: &str) -> Option { + self.id_map + .iter() + .find(|(_, str_id2)| *str_id == **str_id2) + .map(|(id, _)| *id) + } + #[must_use] pub const fn turn(&self) -> u32 { self.turn @@ -197,6 +205,11 @@ impl Board { self.snakes.len() } + #[must_use] + pub fn alive(&self, id: u8) -> bool { + self.id_to_index(id).is_some() + } + #[must_use] pub fn is_food(&self, tile: Coord) -> bool { let index = self.coord_to_linear(tile); @@ -219,7 +232,7 @@ impl Board { } pub fn valid_actions(&self, id: u8) -> impl Iterator + use<'_> { - let index = self.snakes.binary_search_by_key(&id, |snake| snake.id).ok(); + let index = self.id_to_index(id); if index.is_none() { warn!("Asked for a snake that doesn't exist"); } @@ -228,10 +241,25 @@ impl Board { .flat_map(|index| self.valid_actions_index(index)) } - pub fn random_actions( - &self, - ) -> impl Iterator + use<'_>)> { - (0..self.snakes.len()).map(|index| (self.snakes[index].id, self.valid_actions_index(index))) + #[must_use] + pub fn random_action(&self, id: u8) -> Direction { + let Some(index) = self.id_to_index(id) else { + return Direction::Up; + }; + self.valid_actions_index(index) + .choose(&mut thread_rng()) + .unwrap_or(Direction::Up) + } + + pub fn random_actions(&self) -> impl Iterator + use<'_> { + (0..self.snakes.len()).map(|index| { + ( + self.snakes[index].id, + self.valid_actions_index(index) + .choose(&mut thread_rng()) + .unwrap_or(Direction::Up), + ) + }) } pub fn simulate_random(&mut self, stop: impl Fn(&Self) -> Option) -> T { @@ -254,6 +282,10 @@ impl Board { self.turn += 1; } + fn id_to_index(&self, id: u8) -> Option { + self.snakes.binary_search_by_key(&id, |snake| snake.id).ok() + } + fn valid_actions_index(&self, index: usize) -> impl Iterator + use<'_> { let head = self.snakes[index].head(); enum_iterator::all::() @@ -338,9 +370,9 @@ impl Board { let snake = &self.snakes[i]; if snake.health == 0 || !self.is_in_bounds(snake.head()) { let snake = self.snakes.remove(i); - for tile in snake.body { - if self.is_in_bounds(tile) { - let index = self.coord_to_linear(tile); + for tile in snake.body.iter().skip(1) { + if self.is_in_bounds(*tile) { + let index = self.coord_to_linear(*tile); self.free.set(index, true); } } @@ -413,11 +445,21 @@ impl Board { ) }; + if needed_food == 0 { + return; + } + let food_spots = self .free .iter() .enumerate() .filter_map(|(i, free)| free.then_some(i)) + .filter(|i| { + self.snakes + .iter() + .all(|snake| self.coord_to_linear(snake.tail()) != *i) + }) + .filter(|i| !self.food[*i]) .choose_multiple(&mut thread_rng(), needed_food); for index in food_spots { self.food.set(index, true);