From 5e81a7952f16f838136014de24e7b548ec61bf42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=A4nner?= Date: Fri, 17 Jan 2025 00:33:54 +0100 Subject: [PATCH] correct simulation --- battlesnake/src/main.rs | 10 +- battlesnake/src/types/mod.rs | 10 ++ battlesnake/src/types/simulation.rs | 220 ++++++++++++++++++++++++++-- 3 files changed, 228 insertions(+), 12 deletions(-) diff --git a/battlesnake/src/main.rs b/battlesnake/src/main.rs index 6859f83..ad44498 100644 --- a/battlesnake/src/main.rs +++ b/battlesnake/src/main.rs @@ -9,7 +9,7 @@ use axum::{ use log::{debug, info, warn}; use rand::prelude::*; use serde::Serialize; -use tokio::net::TcpListener; +use tokio::{net::TcpListener, time::Instant}; use types::{ simulation::Board, wire::{Request, Response}, @@ -65,6 +65,14 @@ async fn get_move(request: Json) -> response::Json { 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 elapsed = start.elapsed(); + debug!("simulated 100 random games in {elapsed:?}"); let action = actions.choose(&mut thread_rng()).copied(); if action.is_none() { warn!("unable to find a valid action"); diff --git a/battlesnake/src/types/mod.rs b/battlesnake/src/types/mod.rs index 2394dfc..c3d372b 100644 --- a/battlesnake/src/types/mod.rs +++ b/battlesnake/src/types/mod.rs @@ -19,6 +19,16 @@ impl Coord { Direction::Right => self.x.checked_add(1).map(|x| Self { x, y: self.y }), } } + + pub fn wrapping_apply(mut self, direction: Direction) -> Self { + match direction { + Direction::Up => self.y = self.y.wrapping_add(1), + Direction::Down => self.y = self.y.wrapping_sub(1), + Direction::Left => self.x = self.x.wrapping_sub(1), + Direction::Right => self.x = self.x.wrapping_add(1), + } + self + } } #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Sequence)] diff --git a/battlesnake/src/types/simulation.rs b/battlesnake/src/types/simulation.rs index 2285ced..299c381 100644 --- a/battlesnake/src/types/simulation.rs +++ b/battlesnake/src/types/simulation.rs @@ -2,6 +2,7 @@ use std::{collections::VecDeque, fmt::Display}; use bitvec::prelude::*; use log::{error, warn}; +use rand::prelude::*; use super::{wire::Request, Coord, Direction}; @@ -17,6 +18,7 @@ pub struct Board { hazard: BitBox, free: BitBox, snakes: Vec, + constrictor: bool, } impl From<&Request> for Board { @@ -35,21 +37,26 @@ impl From<&Request> for Board { hazard: bitbox![0; fields], free: bitbox![1; fields], snakes: Vec::with_capacity(value.board.snakes.len()), + constrictor: value.game.ruleset.name == "constrictor", }; for &food in &value.board.food { - let index = usize::from(board.coord_to_linear(food)); + let index = board.coord_to_linear(food); board.food.set(index, true); } for &hazard in &value.board.hazards { - let index = usize::from(board.coord_to_linear(hazard)); + let index = board.coord_to_linear(hazard); board.hazard.set(index, true); } for (id, snake) in value.board.snakes.iter().enumerate() { - for &tile in &snake.body { - let index = usize::from(board.coord_to_linear(tile)); + for &tile in snake + .body + .iter() + .take(snake.body.len() - usize::from(!board.constrictor)) + { + let index = board.coord_to_linear(tile); board.free.set(index, false); } let snake = Snake { @@ -68,7 +75,8 @@ impl Display for Board { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!( f, - "{}x{} {}% ({}) {}dmg @ {}", + "{} {}x{} {}% ({}) {}dmg @ {}", + if self.constrictor { "constrictor" } else { "" }, self.width, self.height, self.food_spawn_chance, @@ -89,7 +97,7 @@ impl Display for Board { continue; } - let index = usize::from(self.coord_to_linear(tile)); + let index = self.coord_to_linear(tile); if !self.free[index] { write!(f, "S")?; continue; @@ -113,18 +121,25 @@ impl Display for Board { } impl Board { + pub fn num_snakes(&self) -> usize { + self.snakes.len() + } + pub fn is_food(&self, tile: Coord) -> bool { - let index = usize::from(self.coord_to_linear(tile)); + let index = self.coord_to_linear(tile); self.food[index] } pub fn is_hazard(&self, tile: Coord) -> bool { - let index = usize::from(self.coord_to_linear(tile)); + let index = self.coord_to_linear(tile); self.hazard[index] } pub fn is_free(&self, tile: Coord) -> bool { - let index = usize::from(self.coord_to_linear(tile)); + if !(tile.x < self.width && tile.y < self.height) { + return false; + } + let index = self.coord_to_linear(tile); self.free[index] } @@ -150,8 +165,191 @@ impl Board { .map(|(direction, _)| direction) } - fn coord_to_linear(&self, coord: Coord) -> u16 { - u16::from(coord.x) + u16::from(coord.y) * u16::from(self.width) + pub fn simulate_random(&mut self, stop: impl Fn(&Self) -> Option) -> T { + loop { + if let Some(score) = stop(self) { + break score; + } + self.next_turn(&[]); + } + } + + pub fn next_turn(&mut self, actions: &[(u8, Direction)]) { + self.move_standard(actions); + self.starvation_standard(); + self.hazard_damage_standard(); + self.feed_snakes_standard(); + self.eliminate_snake_standard(); + self.update_free_map(); + self.spawn_food(); + } + + fn move_standard(&mut self, actions: &[(u8, Direction)]) { + for i in 0..self.snakes.len() { + let snake = &self.snakes[i]; + let action = actions.iter().find(|(id, _)| *id == snake.id).map_or_else( + || { + self.valid_actions(snake.id) + .choose(&mut thread_rng()) + .unwrap_or(Direction::Up) + }, + |(_, action)| *action, + ); + let new_head = snake.head().wrapping_apply(action); + let snake = &mut self.snakes[i]; + snake.body.push_front(new_head); + snake.body.pop_back(); + } + } + + fn starvation_standard(&mut self) { + for snake in &mut self.snakes { + snake.health = snake.health.saturating_sub(1); + } + } + + fn hazard_damage_standard(&mut self) { + let mut i = 0; + while i < self.snakes.len() { + let head = self.snakes[i].head(); + if self.is_in_bounds(head) { + let head_index = self.coord_to_linear(head); + if self.hazard[head_index] && !self.food[head_index] { + let health = &mut self.snakes[i].health; + *health = health.saturating_sub(1); + if *health == 0 { + let snake = self.snakes.remove(i); + for tile in snake.body { + let index = self.coord_to_linear(tile); + self.free.set(index, true); + } + continue; + } + } + } + i += 1; + } + } + + fn feed_snakes_standard(&mut self) { + let mut eaten_food = vec![]; + for i in 0..self.snakes.len() { + let head = self.snakes[i].head(); + if self.is_in_bounds(head) { + let head_index = self.coord_to_linear(head); + if self.food[head_index] { + eaten_food.push(head_index); + let snake = &mut self.snakes[i]; + snake.health = 100; + let tail = snake.tail(); + snake.body.push_back(tail); + } + } + } + for food_index in eaten_food { + self.food.set(food_index, false); + } + } + + fn eliminate_snake_standard(&mut self) { + // eliminate out of health and out of bounds + let mut i = 0; + while i < self.snakes.len() { + 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); + self.free.set(index, true); + } + } + continue; + } + i += 1; + } + + // look for collisions + let mut collisions = vec![]; + for snake in &self.snakes { + let head = snake.head(); + let head_index = self.coord_to_linear(head); + if !self.free[head_index] { + collisions.push(snake.id); + continue; + } + for snake2 in &self.snakes { + if snake.id != snake2.id + && snake.head() == snake2.head() + && snake.body.len() <= snake2.body.len() + { + collisions.push(snake.id); + break; + } + } + } + + // apply collisions + let mut i = 0; + while i < self.snakes.len() { + if collisions.contains(&self.snakes[i].id) { + let snake = self.snakes.remove(i); + for tile in snake.body { + let index = self.coord_to_linear(tile); + self.free.set(index, true); + } + continue; + } + i += 1; + } + } + + fn update_free_map(&mut self) { + // free tails + for snake in &self.snakes { + let tail = snake.tail(); + let pre_tail = snake.body[snake.body.len() - 2]; + if tail != pre_tail { + let tail_index = self.coord_to_linear(tail); + self.free.set(tail_index, true); + } + } + // block heads + for snake in &self.snakes { + let head = snake.head(); + let head_index = self.coord_to_linear(head); + self.free.set(head_index, false); + } + } + + fn spawn_food(&mut self) { + let num_food = self.food.count_ones(); + let needed_food = if num_food < usize::from(self.min_food) { + usize::from(self.min_food) - num_food + } else { + usize::from( + self.food_spawn_chance > 0 + && thread_rng().gen_range(0..100) < self.food_spawn_chance, + ) + }; + + let food_spots = self + .free + .iter() + .enumerate() + .filter_map(|(i, free)| free.then_some(i)) + .choose_multiple(&mut thread_rng(), needed_food); + for index in food_spots { + self.food.set(index, true); + } + } + + fn coord_to_linear(&self, coord: Coord) -> usize { + usize::from(coord.x) + usize::from(coord.y) * usize::from(self.width) + } + + const fn is_in_bounds(&self, coord: Coord) -> bool { + coord.x < self.width && coord.y < self.height } }