correct simulation
Some checks failed
Build / build (push) Failing after 1m54s

This commit is contained in:
Max Känner 2025-01-17 00:33:54 +01:00
parent 9c76df1f69
commit 5e81a7952f
3 changed files with 228 additions and 12 deletions

View File

@ -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<Request>) -> response::Json<Response> {
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 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");

View File

@ -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)]

View File

@ -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<Snake>,
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<T>(&mut self, stop: impl Fn(&Self) -> Option<T>) -> 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
}
}