This commit is contained in:
		| @@ -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"); | ||||
|   | ||||
| @@ -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)] | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user