simple monte carlo simulation
All checks were successful
Build / build (push) Successful in 1m58s

This commit is contained in:
Max Känner 2025-01-22 00:43:07 +01:00
parent d858d88e76
commit 5b440bc7db
3 changed files with 117 additions and 22 deletions

View File

@ -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<Request>) {
}
async fn get_move(request: Json<Request>) -> response::Json<Response> {
let board = Board::from(&*request);
info!("got move request: {board}");
let actions = board.valid_actions(0).collect::<Vec<_>>();
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::<Vec<_>>();
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:?}");

View File

@ -45,3 +45,14 @@ pub enum Direction {
/// Move in positive x direction
Right,
}
impl From<Direction> for usize {
fn from(value: Direction) -> Self {
match value {
Direction::Up => 0,
Direction::Down => 1,
Direction::Left => 2,
Direction::Right => 3,
}
}
}

View File

@ -187,6 +187,14 @@ impl Display for Board {
}
impl Board {
#[must_use]
pub fn get_id(&self, str_id: &str) -> Option<u8> {
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<Item = Direction> + 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<Item = (u8, impl Iterator<Item = Direction> + 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<Item = (u8, Direction)> + 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<T>(&mut self, stop: impl Fn(&Self) -> Option<T>) -> T {
@ -254,6 +282,10 @@ impl Board {
self.turn += 1;
}
fn id_to_index(&self, id: u8) -> Option<usize> {
self.snakes.binary_search_by_key(&id, |snake| snake.id).ok()
}
fn valid_actions_index(&self, index: usize) -> impl Iterator<Item = Direction> + use<'_> {
let head = self.snakes[index].head();
enum_iterator::all::<Direction>()
@ -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);