modularize simulation
This commit is contained in:
		
							
								
								
									
										1256
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1256
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -3,6 +3,7 @@ authors = ["Max Känner"] | ||||
| name = "battlesnake" | ||||
| version = "2.0.0" | ||||
| edition = "2024" | ||||
| rust-version = "1.86" | ||||
|  | ||||
| readme = "README.md" | ||||
| repository = "https://git.mkaenner.de/max/battlesnake" | ||||
| @@ -10,11 +11,21 @@ keywords = ["battlesnake"] | ||||
| description = """ | ||||
| A simple Battlesnake written in Rust | ||||
| """ | ||||
| default-run = "battlesnake" | ||||
|  | ||||
| [lints.clippy] | ||||
| pedantic = "warn" | ||||
| nursery = "warn" | ||||
|  | ||||
| [[bin]] | ||||
| name = "battlesnake" | ||||
|  | ||||
| [[bin]] | ||||
| name = "seed-cracker" | ||||
|  | ||||
| [[bin]] | ||||
| name = "generate" | ||||
|  | ||||
| [dependencies] | ||||
| # server | ||||
| tokio = { version = "1.43", features = ["full"] } | ||||
| @@ -31,6 +42,19 @@ enum-iterator = "2.1" | ||||
| rand = "0.9" | ||||
| float-ord = "0.3" | ||||
| futures-util = "0.3.31" | ||||
| az = "1.2.1" | ||||
| blanket = "0.4.0" | ||||
| hashbrown = "0.15.4" | ||||
| serde_json = "1.0.140" | ||||
| clap = { version = "4.5.39", features = ["derive"] } | ||||
| sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] } | ||||
| enum_dispatch = "0.3.13" | ||||
| lru = "0.14.0" | ||||
| lfu_cache = "1.3.0" | ||||
| memmap2 = "0.9.5" | ||||
| bytemuck = "1.23.1" | ||||
| flame = "0.2.2" | ||||
| flamer = "0.5.0" | ||||
|  | ||||
| [dev-dependencies] | ||||
| criterion = "0.5" | ||||
|   | ||||
| @@ -10,7 +10,7 @@ use criterion::{Bencher, BenchmarkId, Criterion, criterion_group, criterion_main | ||||
|  | ||||
| use battlesnake::types::{ | ||||
|     Coord, | ||||
|     simulation::Board, | ||||
|     simulation::Game as Board, | ||||
|     wire::{Battlesnake, Board as WireBoard, Game, Request, RoyaleSettings, Ruleset, Settings}, | ||||
| }; | ||||
| use rand::{SeedableRng, rngs::SmallRng}; | ||||
| @@ -78,13 +78,8 @@ fn standard(c: &mut Criterion) { | ||||
|     let benchmark = |b: &mut Bencher, board: &Board| { | ||||
|         b.iter(|| { | ||||
|             let mut board = board.clone(); | ||||
|             let turn = board.simulate_random(&mut SmallRng::from_os_rng(), |board| { | ||||
|                 if board.num_snakes() <= 1 { | ||||
|                     Some(board.turn()) | ||||
|                 } else { | ||||
|                     None | ||||
|                 } | ||||
|             }); | ||||
|             board.simulate_random(&mut SmallRng::from_os_rng()); | ||||
|             let turn = board.board.turn(); | ||||
|  | ||||
|             if turn < turns_min.load(Ordering::Relaxed) { | ||||
|                 turns_min.store(turn, Ordering::Relaxed); | ||||
| @@ -155,13 +150,8 @@ fn constrictor(c: &mut Criterion) { | ||||
|     let benchmark = |b: &mut Bencher, board: &Board| { | ||||
|         b.iter(|| { | ||||
|             let mut board = board.clone(); | ||||
|             let turn = board.simulate_random(&mut SmallRng::from_os_rng(), |board| { | ||||
|                 if board.num_snakes() <= 1 { | ||||
|                     Some(board.turn()) | ||||
|                 } else { | ||||
|                     None | ||||
|                 } | ||||
|             }); | ||||
|             board.simulate_random(&mut SmallRng::from_os_rng()); | ||||
|             let turn = board.board.turn(); | ||||
|  | ||||
|             if turn < turns_min.load(Ordering::Relaxed) { | ||||
|                 turns_min.store(turn, Ordering::Relaxed); | ||||
|   | ||||
| @@ -5,22 +5,26 @@ use std::{ | ||||
|  | ||||
| use axum::{ | ||||
|     Router, | ||||
|     extract::Json, | ||||
|     extract::{Json, State}, | ||||
|     response, | ||||
|     routing::{get, post}, | ||||
| }; | ||||
| use battlesnake::types::{ | ||||
|     Direction, | ||||
|     simulation::Board, | ||||
|     simulation::{Board, Game}, | ||||
|     wire::{Request, Response}, | ||||
| }; | ||||
| use float_ord::FloatOrd; | ||||
| use futures_util::future::join_all; | ||||
| use hashbrown::HashMap; | ||||
| use log::{debug, error, info, trace, warn}; | ||||
| use rand::prelude::*; | ||||
| use serde::Serialize; | ||||
| use tokio::{ | ||||
|     fs::File, | ||||
|     io::AsyncWriteExt, | ||||
|     net::TcpListener, | ||||
|     sync::mpsc::{UnboundedSender, unbounded_channel}, | ||||
|     time::{Duration, Instant}, | ||||
| }; | ||||
|  | ||||
| @@ -30,12 +34,15 @@ static THREADS: AtomicUsize = AtomicUsize::new(1); | ||||
| async fn main() { | ||||
|     env_logger::init(); | ||||
|  | ||||
|     let (sender, mut receiver) = unbounded_channel(); | ||||
|  | ||||
|     debug!("Creating routes"); | ||||
|     let app = Router::new() | ||||
|         .route("/", get(info)) | ||||
|         .route("/start", post(start)) | ||||
|         .route("/move", post(get_move)) | ||||
|         .route("/end", post(end)); | ||||
|         .route("/end", post(end)) | ||||
|         .with_state(sender); | ||||
|  | ||||
|     let threads = env::var("THREADS") | ||||
|         .ok() | ||||
| @@ -51,6 +58,53 @@ async fn main() { | ||||
|     debug!("Creating listener"); | ||||
|     let port = env::var("PORT").unwrap_or_else(|_| "8000".into()); | ||||
|     let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap(); | ||||
|  | ||||
|     debug!("Starting observer"); | ||||
|     tokio::spawn(async move { | ||||
|         let mut games = HashMap::new(); | ||||
|         while let Some((request_type, request)) = receiver.recv().await { | ||||
|             match request_type { | ||||
|                 RequestType::Start => { | ||||
|                     let game_id = request.game.id.clone(); | ||||
|                     info!("Got start request {game_id}"); | ||||
|                     if let Some(old_requests) = games.insert(game_id, vec![request]) { | ||||
|                         warn!("evicted duplicate game: {old_requests:?}"); | ||||
|                     } | ||||
|                 } | ||||
|                 RequestType::GetMove => { | ||||
|                     let game_id = request.game.id.clone(); | ||||
|                     info!("Got move request {game_id}"); | ||||
|                     games.entry(game_id).or_default().push(request); | ||||
|                 } | ||||
|                 RequestType::End => { | ||||
|                     let game_id = request.game.id.clone(); | ||||
|                     info!("Got end request {game_id}"); | ||||
|                     if let Some(mut requests) = games.remove(&game_id) { | ||||
|                         requests.push(request); | ||||
|                         let json = match serde_json::to_vec_pretty(&requests) { | ||||
|                             Ok(json) => json, | ||||
|                             Err(e) => { | ||||
|                                 error!("Unable to serealize json: {e}"); | ||||
|                                 continue; | ||||
|                             } | ||||
|                         }; | ||||
|                         match File::create_new(format!("games/{game_id}.json")).await { | ||||
|                             Ok(mut file) => { | ||||
|                                 if let Err(e) = file.write_all(&json).await { | ||||
|                                     error!("Unable to write jsone: {e}"); | ||||
|                                 } | ||||
|                             } | ||||
|                             Err(e) => error!("Unable to open file: {e}"), | ||||
|                         } | ||||
|                     } else { | ||||
|                         warn!("end of game without game: {request:?}"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         warn!("Observer stopped"); | ||||
|     }); | ||||
|  | ||||
|     debug!("Starting server"); | ||||
|     axum::serve(listener, app).await.unwrap(); | ||||
| } | ||||
| @@ -77,22 +131,41 @@ struct Info { | ||||
|     version: &'static str, | ||||
| } | ||||
|  | ||||
| async fn start(request: Json<Request>) { | ||||
|     let board = Board::from(&*request); | ||||
| #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] | ||||
| enum RequestType { | ||||
|     Start, | ||||
|     GetMove, | ||||
|     End, | ||||
| } | ||||
|  | ||||
| async fn start( | ||||
|     State(sender): State<UnboundedSender<(RequestType, Request)>>, | ||||
|     Json(request): Json<Request>, | ||||
| ) { | ||||
|     if let Err(e) = sender.send((RequestType::Start, request.clone())) { | ||||
|         warn!("Unable to observe request: {e}"); | ||||
|     } | ||||
|     let board = Board::from(&request); | ||||
|     info!("got start request: {board}"); | ||||
| } | ||||
|  | ||||
| #[allow(clippy::too_many_lines)] | ||||
| async fn get_move(request: Json<Request>) -> response::Json<Response> { | ||||
| async fn get_move( | ||||
|     State(sender): State<UnboundedSender<(RequestType, Request)>>, | ||||
|     Json(request): Json<Request>, | ||||
| ) -> response::Json<Response> { | ||||
|     let start = Instant::now(); | ||||
|     let board = Board::from(&*request); | ||||
|     if let Err(e) = sender.send((RequestType::GetMove, request.clone())) { | ||||
|         warn!("Unable to observe request: {e}"); | ||||
|     } | ||||
|     let board = Game::from(&request); | ||||
|     let timeout = Duration::from_millis(u64::from(request.game.timeout)); | ||||
|     let id = board.get_id(&request.you.id).unwrap_or_else(|| { | ||||
|     let id = board.board.get_id(&request.you.id).unwrap_or_else(|| { | ||||
|         error!("My id is not in the simulation board"); | ||||
|         0 | ||||
|     }); | ||||
|     debug!("got move request: {board}"); | ||||
|     let actions = board.valid_actions(id).collect::<Vec<_>>(); | ||||
|     debug!("got move request: {}", board.board); | ||||
|     let actions = board.board.valid_actions(id).collect::<Vec<_>>(); | ||||
|     if actions.len() <= 1 { | ||||
|         info!( | ||||
|             "only one possible action. Fast forwarding {:?}", | ||||
| @@ -111,10 +184,6 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> { | ||||
|             start.elapsed().as_millis() | ||||
|         ); | ||||
|     } | ||||
|     let end_condition: fn(&Board) -> Option<()> = match &*request.game.ruleset.name { | ||||
|         "solo" => end_solo, | ||||
|         _ => end_standard, | ||||
|     }; | ||||
|     let score_fn: fn(&Board, u8) -> u32 = match &*request.game.ruleset.name { | ||||
|         "solo" => score_solo, | ||||
|         _ => score_standard, | ||||
| @@ -132,22 +201,25 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> { | ||||
|             let mut mcts_actions = Vec::new(); | ||||
|             while start.elapsed() < timeout * 4 / 5 { | ||||
|                 let mut board = board.clone(); | ||||
|                 while end_condition(&board).is_none() { | ||||
|                 let mut game_over = false; | ||||
|                 while !game_over { | ||||
|                     mcts_actions.clear(); | ||||
|                     mcts_actions.extend(mcts_managers.iter_mut().filter_map(|mcts_manager| { | ||||
|                         mcts_manager | ||||
|                             .next_action(&board, c, &mut rng) | ||||
|                             .next_action(&board.board, c, &mut rng) | ||||
|                             .map(|action| (mcts_manager.snake, action)) | ||||
|                     })); | ||||
|                     board.next_turn(&mcts_actions, &mut rng); | ||||
|                     game_over = board.next_turn_random(&mcts_actions, &mut rng); | ||||
|                     if mcts_actions.is_empty() { | ||||
|                         break; | ||||
|                     } | ||||
|                 } | ||||
|                 board.simulate_random(&mut rng, end_condition); | ||||
|                 if !game_over { | ||||
|                     board.simulate_random(&mut rng); | ||||
|                 } | ||||
|                 for mcts_manager in &mut mcts_managers { | ||||
|                     let id = mcts_manager.snake; | ||||
|                     let score = score_fn(&board, id); | ||||
|                     let score = score_fn(&board.board, id); | ||||
|                     mcts_manager.apply_score(score); | ||||
|                 } | ||||
|             } | ||||
| @@ -200,16 +272,8 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> { | ||||
|     }) | ||||
| } | ||||
|  | ||||
| fn end_solo(board: &Board) -> Option<()> { | ||||
|     (board.valid_actions(0).count() == 0).then_some(()) | ||||
| } | ||||
|  | ||||
| fn end_standard(board: &Board) -> Option<()> { | ||||
|     (board.num_snakes() <= 1).then_some(()) | ||||
| } | ||||
|  | ||||
| fn score_solo(board: &Board, id: u8) -> u32 { | ||||
|     u32::try_from(board.length(id)).unwrap_or(0) | ||||
| const fn score_solo(board: &Board, _id: u8) -> u32 { | ||||
|     board.turn() | ||||
| } | ||||
|  | ||||
| fn score_standard(board: &Board, id: u8) -> u32 { | ||||
| @@ -220,8 +284,14 @@ fn score_standard(board: &Board, id: u8) -> u32 { | ||||
|     } | ||||
| } | ||||
|  | ||||
| async fn end(request: Json<Request>) { | ||||
|     let board = Board::from(&*request); | ||||
| async fn end( | ||||
|     State(sender): State<UnboundedSender<(RequestType, Request)>>, | ||||
|     Json(request): Json<Request>, | ||||
| ) { | ||||
|     if let Err(e) = sender.send((RequestType::End, request.clone())) { | ||||
|         warn!("Unable to observe request: {e}"); | ||||
|     } | ||||
|     let board = Board::from(&request); | ||||
|     info!("got end request: {board}"); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| use std::fmt::Display; | ||||
|  | ||||
| use enum_iterator::Sequence; | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| pub mod simulation; | ||||
| pub mod wire; | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] | ||||
| pub struct Coord { | ||||
|     pub x: u8, | ||||
|     pub y: u8, | ||||
| @@ -33,6 +35,12 @@ impl Coord { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Display for Coord { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         write!(f, "({}, {})", self.x, self.y) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Serialize, Sequence)] | ||||
| #[serde(rename_all = "lowercase")] | ||||
| pub enum Direction { | ||||
|   | ||||
							
								
								
									
										43
									
								
								battlesnake/src/types/simulation/maps/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								battlesnake/src/types/simulation/maps/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| mod royale; | ||||
| mod standard; | ||||
|  | ||||
| use blanket::blanket; | ||||
| use enum_dispatch::enum_dispatch; | ||||
| use rand::Rng; | ||||
| use royale::Royale; | ||||
| use standard::Standard; | ||||
|  | ||||
| use crate::types::wire::Game; | ||||
|  | ||||
| use super::Board; | ||||
|  | ||||
| #[blanket(derive(Ref, Arc, Mut, Box))] | ||||
| #[enum_dispatch(Maps)] | ||||
| pub trait Map { | ||||
|     /// Called before the board is updated | ||||
|     fn pre_update(&self, board: &mut Board, rng: &mut impl Rng); | ||||
|  | ||||
|     /// Called after the board is updated | ||||
|     fn post_update(&self, board: &mut Board, rng: &mut impl Rng); | ||||
| } | ||||
|  | ||||
| #[enum_dispatch] | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum Maps { | ||||
|     Standard, | ||||
|     Royale, | ||||
| } | ||||
|  | ||||
| pub trait MapId { | ||||
|     /// ID of the map for detection | ||||
|     const ID: &str; | ||||
| } | ||||
|  | ||||
| impl From<&Game> for Maps { | ||||
|     fn from(value: &Game) -> Self { | ||||
|         match value.map.as_ref() { | ||||
|             Royale::ID => Royale::from(&value.ruleset.settings).into(), | ||||
|             _ => Standard::from(&value.ruleset.settings).into(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										86
									
								
								battlesnake/src/types/simulation/maps/royale.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								battlesnake/src/types/simulation/maps/royale.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| use rand::{Rng, seq::IteratorRandom}; | ||||
|  | ||||
| use crate::types::{Coord, Direction, simulation::Board, wire::Settings}; | ||||
|  | ||||
| use super::{Map, MapId, standard::Standard}; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub struct Royale { | ||||
|     standard: Standard, | ||||
|     shrink_every_n_turns: u8, | ||||
| } | ||||
|  | ||||
| impl Map for Royale { | ||||
|     fn pre_update(&self, board: &mut Board, rng: &mut impl Rng) { | ||||
|         self.standard.pre_update(board, rng); | ||||
|     } | ||||
|  | ||||
|     fn post_update(&self, board: &mut Board, rng: &mut impl Rng) { | ||||
|         self.standard.post_update(board, rng); | ||||
|  | ||||
|         // Royale uses the current turn to generate hazards, not the previous turn that's in the | ||||
|         // board state | ||||
|         let turn = board.turn + 1; | ||||
|  | ||||
|         if turn < u32::from(self.shrink_every_n_turns) { | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         let side = enum_iterator::all::<Direction>() | ||||
|             .choose(rng) | ||||
|             .unwrap_or(Direction::Up); | ||||
|         match side { | ||||
|             Direction::Up => { | ||||
|                 if let Some(i) = board.hazard.first_zero() { | ||||
|                     let y = board.linear_to_coord(i).y; | ||||
|                     for x in 0..board.width { | ||||
|                         let i = board.coord_to_linear(Coord { x, y }); | ||||
|                         board.hazard.set(i, true); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Direction::Down => { | ||||
|                 if let Some(i) = board.hazard.last_zero() { | ||||
|                     let y = board.linear_to_coord(i).y; | ||||
|                     for x in 0..board.width { | ||||
|                         let i = board.coord_to_linear(Coord { x, y }); | ||||
|                         board.hazard.set(i, true); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Direction::Left => { | ||||
|                 if let Some(i) = board.hazard.first_zero() { | ||||
|                     let x = board.linear_to_coord(i).y; | ||||
|                     for y in 0..board.height { | ||||
|                         let i = board.coord_to_linear(Coord { x, y }); | ||||
|                         board.hazard.set(i, true); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             Direction::Right => { | ||||
|                 if let Some(i) = board.hazard.last_zero() { | ||||
|                     let x = board.linear_to_coord(i).y; | ||||
|                     for y in 0..board.height { | ||||
|                         let i = board.coord_to_linear(Coord { x, y }); | ||||
|                         board.hazard.set(i, true); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl MapId for Royale { | ||||
|     const ID: &str = "royale"; | ||||
| } | ||||
|  | ||||
| impl From<&Settings> for Royale { | ||||
|     fn from(value: &Settings) -> Self { | ||||
|         let standard = Standard::from(value); | ||||
|         let shrink_every_n_turns = value.royale.shrink_every_n_turns; | ||||
|         Self { | ||||
|             standard, | ||||
|             shrink_every_n_turns, | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										78
									
								
								battlesnake/src/types/simulation/maps/standard.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								battlesnake/src/types/simulation/maps/standard.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| use az::SaturatingAs; | ||||
| use rand::{Rng, seq::SliceRandom}; | ||||
|  | ||||
| use crate::types::{Coord, simulation::Board, wire::Settings}; | ||||
|  | ||||
| use super::{Map, MapId}; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub struct Standard { | ||||
|     min_food: u16, | ||||
|     food_spawn_chance: u8, | ||||
| } | ||||
|  | ||||
| impl Map for Standard { | ||||
|     fn pre_update(&self, _board: &mut Board, _rng: &mut impl Rng) {} | ||||
|  | ||||
|     fn post_update(&self, board: &mut Board, rng: &mut impl Rng) { | ||||
|         let food_needed = self.check_food_needing_placement(board, rng); | ||||
|         if food_needed > 0 { | ||||
|             Self::place_food_randomly(board, rng, food_needed); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl MapId for Standard { | ||||
|     const ID: &str = "standard"; | ||||
| } | ||||
|  | ||||
| impl From<&Settings> for Standard { | ||||
|     fn from(value: &Settings) -> Self { | ||||
|         let min_food = value.minimum_food; | ||||
|         let food_spawn_chance = value.food_spawn_chance; | ||||
|         Self { | ||||
|             min_food, | ||||
|             food_spawn_chance, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Standard { | ||||
|     fn check_food_needing_placement(self, board: &Board, rng: &mut impl rand::Rng) -> u16 { | ||||
|         let num_current_food: u16 = board.food.count_ones().saturating_as(); | ||||
|  | ||||
|         if num_current_food < self.min_food { | ||||
|             return self.min_food - num_current_food; | ||||
|         } | ||||
|         if self.food_spawn_chance > 0 && (100 - rng.random_range(0..100)) < self.food_spawn_chance { | ||||
|             return 1; | ||||
|         } | ||||
|         0 | ||||
|     } | ||||
|  | ||||
|     fn place_food_randomly(board: &mut Board, rng: &mut impl rand::Rng, food_needed: u16) { | ||||
|         let mut unoccupied_points: Vec<_> = board.get_unoccupied_points(false, false).collect(); | ||||
|         Self::place_food_ramdomly_at_positions( | ||||
|             board, | ||||
|             rng, | ||||
|             food_needed, | ||||
|             unoccupied_points.as_mut_slice(), | ||||
|         ); | ||||
|     } | ||||
|  | ||||
|     fn place_food_ramdomly_at_positions( | ||||
|         board: &mut Board, | ||||
|         rng: &mut impl rand::Rng, | ||||
|         food_needed: u16, | ||||
|         positions: &mut [Coord], | ||||
|     ) { | ||||
|         let food_needed = usize::from(food_needed).min(positions.len()); | ||||
|  | ||||
|         positions.shuffle(rng); | ||||
|  | ||||
|         for tile in &positions[..food_needed] { | ||||
|             let i = board.coord_to_linear(*tile); | ||||
|             board.food.set(i, true); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,3 +1,6 @@ | ||||
| mod maps; | ||||
| mod rules; | ||||
| 
 | ||||
| use std::{ | ||||
|     collections::VecDeque, | ||||
|     fmt::Display, | ||||
| @@ -6,9 +9,12 @@ use std::{ | ||||
|     sync::Arc, | ||||
| }; | ||||
| 
 | ||||
| use az::SaturatingAs; | ||||
| use bitvec::prelude::*; | ||||
| use log::{error, warn}; | ||||
| use maps::{Map, Maps}; | ||||
| use rand::prelude::*; | ||||
| use rules::{Ruleset, Rulesets}; | ||||
| 
 | ||||
| use super::{Coord, Direction, wire::Request}; | ||||
| 
 | ||||
| @@ -58,19 +64,79 @@ impl DerefMut for SmallBitBox { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Clone)] | ||||
| pub struct Game { | ||||
|     pub board: Board, | ||||
|     map: Maps, | ||||
|     ruleset: Rulesets, | ||||
| } | ||||
| 
 | ||||
| impl From<&Request> for Game { | ||||
|     fn from(value: &Request) -> Self { | ||||
|         let board = value.into(); | ||||
|         let map = (&value.game).into(); | ||||
|         let ruleset = (&value.game.ruleset).into(); | ||||
|         Self { | ||||
|             board, | ||||
|             map, | ||||
|             ruleset, | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| impl Game { | ||||
|     pub fn simulate_random(&mut self, rng: &mut impl Rng) { | ||||
|         loop { | ||||
|             let random_actions: Vec<_> = (0..self.board.snakes.len()) | ||||
|                 .filter_map(|i| { | ||||
|                     self.board | ||||
|                         .valid_actions_index(i) | ||||
|                         .choose(rng) | ||||
|                         .map(|direction| (i.saturating_as(), direction)) | ||||
|                 }) | ||||
|                 .collect(); | ||||
|             if self.next_turn(&random_actions, rng) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn next_turn_random(&mut self, actions: &[(u8, Direction)], rng: &mut impl Rng) -> bool { | ||||
|         let random_actions: Vec<_> = (0..self.board.snakes.len()) | ||||
|             .filter_map(|i| { | ||||
|                 actions | ||||
|                     .iter() | ||||
|                     .find(|(j, _)| i == usize::from(*j)) | ||||
|                     .copied() | ||||
|                     .or_else(|| { | ||||
|                         self.board | ||||
|                             .valid_actions_index(i) | ||||
|                             .choose(rng) | ||||
|                             .map(|direction| (i.saturating_as(), direction)) | ||||
|                     }) | ||||
|             }) | ||||
|             .collect(); | ||||
|         self.next_turn(&random_actions, rng) | ||||
|     } | ||||
| 
 | ||||
|     pub fn next_turn(&mut self, actions: &[(u8, Direction)], rng: &mut impl Rng) -> bool { | ||||
|         self.map.pre_update(&mut self.board, rng); | ||||
|         let game_over = self.ruleset.execute(&mut self.board, actions); | ||||
|         self.map.post_update(&mut self.board, rng); | ||||
|         self.board.turn += 1; | ||||
|         game_over | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| #[derive(Debug, PartialEq, Eq, Clone)] | ||||
| pub struct Board { | ||||
|     width: u8, | ||||
|     height: u8, | ||||
|     hazard_damage: u8, | ||||
|     food_spawn_chance: u8, | ||||
|     min_food: u16, | ||||
|     turn: u32, | ||||
|     food: SmallBitBox, | ||||
|     hazard: SmallBitBox, | ||||
|     free: SmallBitBox, | ||||
|     snakes: Vec<Snake>, | ||||
|     constrictor: bool, | ||||
|     id_map: Arc<[(u8, Arc<str>)]>, | ||||
| } | ||||
| 
 | ||||
| @@ -94,15 +160,11 @@ impl From<&Request> for Board { | ||||
|         let mut board = Self { | ||||
|             width, | ||||
|             height, | ||||
|             hazard_damage: value.game.ruleset.settings.hazard_damage_per_turn, | ||||
|             food_spawn_chance: value.game.ruleset.settings.food_spawn_chance, | ||||
|             min_food: value.game.ruleset.settings.minimum_food, | ||||
|             turn: value.turn, | ||||
|             food: SmallBitBox::new(false, fields), | ||||
|             hazard: SmallBitBox::new(false, fields), | ||||
|             free: SmallBitBox::new(true, fields), | ||||
|             snakes: Vec::with_capacity(value.board.snakes.len()), | ||||
|             constrictor: &*value.game.ruleset.name == "constrictor", | ||||
|             id_map: id_map.into(), | ||||
|         }; | ||||
| 
 | ||||
| @@ -116,11 +178,12 @@ impl From<&Request> for Board { | ||||
|             board.hazard.set(index, true); | ||||
|         } | ||||
| 
 | ||||
|         let constrictor = value.game.ruleset.name.as_ref() == "constrictor"; | ||||
|         for (id, snake) in value.board.snakes.iter().enumerate() { | ||||
|             for &tile in snake | ||||
|                 .body | ||||
|                 .iter() | ||||
|                 .take(snake.body.len() - usize::from(!board.constrictor)) | ||||
|                 .take(snake.body.len() - usize::from(!constrictor)) | ||||
|             { | ||||
|                 let index = board.coord_to_linear(tile); | ||||
|                 board.free.set(index, false); | ||||
| @@ -139,17 +202,7 @@ impl From<&Request> for Board { | ||||
| 
 | ||||
| impl Display for Board { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|         writeln!( | ||||
|             f, | ||||
|             "{} {}x{} {}% ({}) {}dmg @ {}", | ||||
|             if self.constrictor { "constrictor" } else { "" }, | ||||
|             self.width, | ||||
|             self.height, | ||||
|             self.food_spawn_chance, | ||||
|             self.min_food, | ||||
|             self.hazard_damage, | ||||
|             self.turn | ||||
|         )?; | ||||
|         writeln!(f, "{}x{} @ {}", self.width, self.height, self.turn)?; | ||||
| 
 | ||||
|         for y in (0..self.height).rev() { | ||||
|             for x in 0..self.width { | ||||
| @@ -279,30 +332,6 @@ impl Board { | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     pub fn simulate_random<T>( | ||||
|         &mut self, | ||||
|         rng: &mut impl RngCore, | ||||
|         stop: impl Fn(&Self) -> Option<T>, | ||||
|     ) -> T { | ||||
|         loop { | ||||
|             if let Some(score) = stop(self) { | ||||
|                 break score; | ||||
|             } | ||||
|             self.next_turn(&[], rng); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn next_turn(&mut self, actions: &[(u8, Direction)], rng: &mut impl RngCore) { | ||||
|         self.move_standard(actions, rng); | ||||
|         self.starvation_standard(); | ||||
|         self.hazard_damage_standard(); | ||||
|         self.feed_snakes_standard(); | ||||
|         self.eliminate_snake_standard(); | ||||
|         self.update_free_map(); | ||||
|         self.spawn_food(rng); | ||||
|         self.turn += 1; | ||||
|     } | ||||
| 
 | ||||
|     fn id_to_index(&self, id: u8) -> Option<usize> { | ||||
|         self.snakes.binary_search_by_key(&id, |snake| snake.id).ok() | ||||
|     } | ||||
| @@ -313,127 +342,6 @@ impl Board { | ||||
|             .filter(move |direction| self.is_free(head.wrapping_apply(*direction))) | ||||
|     } | ||||
| 
 | ||||
|     fn move_standard(&mut self, actions: &[(u8, Direction)], rng: &mut impl RngCore) { | ||||
|         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_index(i) | ||||
|                         .choose(rng) | ||||
|                         .unwrap_or(Direction::Up) | ||||
|                 }, | ||||
|                 |(_, action)| *action, | ||||
|             ); | ||||
|             let new_head = snake.head().wrapping_apply(action); | ||||
|             self.snakes[i].advance(new_head); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     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) { | ||||
|         if self.constrictor { | ||||
|             for snake in &mut self.snakes { | ||||
|                 snake.feed(); | ||||
|             } | ||||
|         } else { | ||||
|             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); | ||||
|                         self.snakes[i].feed(); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             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.iter().skip(1) { | ||||
|                     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 { | ||||
| @@ -452,65 +360,49 @@ impl Board { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn spawn_food(&mut self, rng: &mut impl RngCore) { | ||||
|         let food_needed = self.check_food_needing_placement(rng); | ||||
| 
 | ||||
|         if food_needed > 0 { | ||||
|             self.place_food_randomly(food_needed, rng); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fn check_food_needing_placement(&self, rng: &mut impl RngCore) -> u16 { | ||||
|         let min_food = self.min_food; | ||||
|         let food_spawn_chance = self.food_spawn_chance; | ||||
|         let num_current_food = u16::try_from(self.food.count_ones()).unwrap_or(u16::MAX); | ||||
| 
 | ||||
|         if num_current_food < min_food { | ||||
|             return min_food - num_current_food; | ||||
|         } | ||||
|         if food_spawn_chance > 0 && (100 - rng.random_range(0..100)) < food_spawn_chance { | ||||
|             return 1; | ||||
|         } | ||||
| 
 | ||||
|         0 | ||||
|     } | ||||
| 
 | ||||
|     fn place_food_randomly(&mut self, amount: u16, rng: &mut impl RngCore) { | ||||
|         let tails: Vec<_> = self | ||||
|             .snakes | ||||
|     fn get_unoccupied_points( | ||||
|         &self, | ||||
|         include_possible_moves: bool, | ||||
|         include_hazards: bool, | ||||
|     ) -> impl Iterator<Item = Coord> + use<'_> { | ||||
|         let possible_moves: Vec<_> = if include_possible_moves { | ||||
|             Vec::new() | ||||
|         } else { | ||||
|             self.snakes | ||||
|                 .iter() | ||||
|                 .flat_map(|snake| { | ||||
|                     let head = snake.head(); | ||||
|                     enum_iterator::all::<Direction>() | ||||
|                         .map(move |direction| head.wrapping_apply(direction)) | ||||
|                         .filter(|tile| self.is_in_bounds(*tile)) | ||||
|                         .map(|tile| self.coord_to_linear(tile)) | ||||
|                 }) | ||||
|                 .collect() | ||||
|         }; | ||||
|         self.free | ||||
|             .iter() | ||||
|             .map(|snake| self.coord_to_linear(snake.tail())) | ||||
|             .collect(); | ||||
|         let possible_moves: Vec<_> = self | ||||
|             .snakes | ||||
|             .iter() | ||||
|             .flat_map(|snake| { | ||||
|                 let head = snake.head(); | ||||
|                 enum_iterator::all::<Direction>() | ||||
|                     .map(move |direction| head.wrapping_apply(direction)) | ||||
|                     .filter(|tile| self.is_in_bounds(*tile)) | ||||
|                     .map(|tile| self.coord_to_linear(tile)) | ||||
|             }) | ||||
|             .collect(); | ||||
|         let unoccupied_points = self | ||||
|             .free | ||||
|             .iter() | ||||
|             .by_vals() | ||||
|             .zip(self.food.iter()) | ||||
|             .zip(self.hazard.iter()) | ||||
|             .enumerate() | ||||
|             .zip(self.hazard.iter().by_vals()) | ||||
|             .filter_map(|((i, free), hazard)| (!hazard && free).then_some(i)) | ||||
|             .filter(|i| !tails.contains(i)) | ||||
|             .filter(|i| !possible_moves.contains(i)); | ||||
| 
 | ||||
|         for food_spot in unoccupied_points.choose_multiple(rng, usize::from(amount)) { | ||||
|             self.food.set(food_spot, true); | ||||
|         } | ||||
|             .filter(move |(i, ((free, food), hazard))| { | ||||
|                 **free | ||||
|                     && !**food | ||||
|                     && (include_hazards || !**hazard) | ||||
|                     && (include_possible_moves || !possible_moves.contains(i)) | ||||
|             }) | ||||
|             .map(|(i, _)| self.linear_to_coord(i)) | ||||
|     } | ||||
| 
 | ||||
|     fn coord_to_linear(&self, coord: Coord) -> usize { | ||||
|         usize::from(coord.x) + usize::from(coord.y) * usize::from(self.width) | ||||
|     } | ||||
| 
 | ||||
|     fn linear_to_coord(&self, linear: usize) -> Coord { | ||||
|         let x = (linear % usize::from(self.width)).saturating_as(); | ||||
|         let y = (linear / usize::from(self.width)).saturating_as(); | ||||
|         Coord { x, y } | ||||
|     } | ||||
| 
 | ||||
|     const fn is_in_bounds(&self, coord: Coord) -> bool { | ||||
|         coord.x < self.width && coord.y < self.height | ||||
|     } | ||||
							
								
								
									
										38
									
								
								battlesnake/src/types/simulation/rules/constrictor.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								battlesnake/src/types/simulation/rules/constrictor.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| use crate::types::{Direction, simulation::Board, wire}; | ||||
|  | ||||
| use super::{ | ||||
|     Ruleset, | ||||
|     standard::{DamageHazards, eliminate_snakes, game_over, move_snakes, reduce_snake_health}, | ||||
| }; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub struct Constrictor { | ||||
|     damage_hazards: DamageHazards, | ||||
| } | ||||
|  | ||||
| impl From<&wire::Ruleset> for Constrictor { | ||||
|     fn from(value: &wire::Ruleset) -> Self { | ||||
|         Self { | ||||
|             damage_hazards: (&value.settings).into(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Ruleset for Constrictor { | ||||
|     fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool { | ||||
|         let game_over = game_over(board); | ||||
|         move_snakes(board, actions); | ||||
|         reduce_snake_health(board); | ||||
|         self.damage_hazards.execute(board); | ||||
|         eliminate_snakes(board); | ||||
|         grow_snakes(board); | ||||
|         board.update_free_map(); | ||||
|         game_over | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn grow_snakes(board: &mut Board) { | ||||
|     for snake in &mut board.snakes { | ||||
|         snake.feed(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										37
									
								
								battlesnake/src/types/simulation/rules/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								battlesnake/src/types/simulation/rules/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| mod constrictor; | ||||
| mod solo; | ||||
| mod standard; | ||||
|  | ||||
| use constrictor::Constrictor; | ||||
| use enum_dispatch::enum_dispatch; | ||||
| use solo::Solo; | ||||
| use standard::Standard; | ||||
|  | ||||
| use crate::types::{Direction, wire}; | ||||
|  | ||||
| use super::Board; | ||||
|  | ||||
| #[enum_dispatch] | ||||
| pub trait Ruleset { | ||||
|     /// executes one turn of the ruleset. | ||||
|     /// Returns true if the game is over | ||||
|     fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool; | ||||
| } | ||||
|  | ||||
| #[enum_dispatch(Ruleset)] | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub enum Rulesets { | ||||
|     Standard, | ||||
|     Constrictor, | ||||
|     Solo, | ||||
| } | ||||
|  | ||||
| impl From<&wire::Ruleset> for Rulesets { | ||||
|     fn from(value: &wire::Ruleset) -> Self { | ||||
|         match value.name.as_ref() { | ||||
|             "solo" => Solo::from(value).into(), | ||||
|             "constrictor" => Constrictor::from(value).into(), | ||||
|             _ => Standard::from(value).into(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										36
									
								
								battlesnake/src/types/simulation/rules/solo.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								battlesnake/src/types/simulation/rules/solo.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| use crate::types::{Direction, simulation::Board, wire}; | ||||
|  | ||||
| use super::{ | ||||
|     Ruleset, | ||||
|     standard::{DamageHazards, eliminate_snakes, feed_snakes, move_snakes, reduce_snake_health}, | ||||
| }; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub struct Solo { | ||||
|     damage_hazards: DamageHazards, | ||||
| } | ||||
|  | ||||
| impl From<&wire::Ruleset> for Solo { | ||||
|     fn from(value: &wire::Ruleset) -> Self { | ||||
|         Self { | ||||
|             damage_hazards: (&value.settings).into(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Ruleset for Solo { | ||||
|     fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool { | ||||
|         let game_over = game_over(board); | ||||
|         move_snakes(board, actions); | ||||
|         reduce_snake_health(board); | ||||
|         self.damage_hazards.execute(board); | ||||
|         feed_snakes(board); | ||||
|         eliminate_snakes(board); | ||||
|         board.update_free_map(); | ||||
|         game_over | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn game_over(board: &Board) -> bool { | ||||
|     board.num_snakes() == 0 | ||||
| } | ||||
							
								
								
									
										180
									
								
								battlesnake/src/types/simulation/rules/standard.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								battlesnake/src/types/simulation/rules/standard.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,180 @@ | ||||
| use crate::types::{ | ||||
|     Direction, | ||||
|     simulation::Board, | ||||
|     wire::{self, Settings}, | ||||
| }; | ||||
|  | ||||
| use super::Ruleset; | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub struct Standard { | ||||
|     damage_hazards: DamageHazards, | ||||
| } | ||||
|  | ||||
| impl From<&wire::Ruleset> for Standard { | ||||
|     fn from(value: &wire::Ruleset) -> Self { | ||||
|         Self { | ||||
|             damage_hazards: (&value.settings).into(), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Ruleset for Standard { | ||||
|     fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool { | ||||
|         let game_over = game_over(board); | ||||
|         move_snakes(board, actions); | ||||
|         reduce_snake_health(board); | ||||
|         self.damage_hazards.execute(board); | ||||
|         feed_snakes(board); | ||||
|         eliminate_snakes(board); | ||||
|         board.update_free_map(); | ||||
|         game_over | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn move_snakes(board: &mut Board, actions: &[(u8, Direction)]) { | ||||
|     for i in 0..board.snakes.len() { | ||||
|         let snake = &board.snakes[i]; | ||||
|         let action = actions.iter().find(|(id, _)| *id == snake.id).map_or_else( | ||||
|             || { | ||||
|                 let head = snake.body[0]; | ||||
|                 let previous_head = snake.body[1]; | ||||
|                 let delta_x = i16::from(head.x) - i16::from(previous_head.x); | ||||
|                 let delta_y = i16::from(head.y) - i16::from(previous_head.y); | ||||
|                 if delta_x == 0 && delta_y == 0 { | ||||
|                     Direction::Up | ||||
|                 } else if delta_x.abs() > delta_y.abs() { | ||||
|                     if delta_x < 0 { | ||||
|                         Direction::Left | ||||
|                     } else { | ||||
|                         Direction::Right | ||||
|                     } | ||||
|                 } else if delta_y < 0 { | ||||
|                     Direction::Down | ||||
|                 } else { | ||||
|                     Direction::Up | ||||
|                 } | ||||
|             }, | ||||
|             |(_, action)| *action, | ||||
|         ); | ||||
|         let new_head = snake.head().wrapping_apply(action); | ||||
|         board.snakes[i].advance(new_head); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn reduce_snake_health(board: &mut Board) { | ||||
|     for snake in &mut board.snakes { | ||||
|         snake.health = snake.health.saturating_sub(1); | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Debug, Clone, Copy)] | ||||
| pub struct DamageHazards { | ||||
|     damage: u8, | ||||
| } | ||||
|  | ||||
| impl DamageHazards { | ||||
|     pub fn execute(self, board: &mut Board) { | ||||
|         let mut i = 0; | ||||
|         while i < board.snakes.len() { | ||||
|             let head = board.snakes[i].head(); | ||||
|             if board.is_in_bounds(head) { | ||||
|                 let head_index = board.coord_to_linear(head); | ||||
|                 if board.hazard[head_index] && !board.food[head_index] { | ||||
|                     let health = &mut board.snakes[i].health; | ||||
|                     *health = health.saturating_sub(self.damage); | ||||
|                     if *health == 0 { | ||||
|                         let snake = board.snakes.remove(i); | ||||
|                         for tile in snake.body { | ||||
|                             let index = board.coord_to_linear(tile); | ||||
|                             board.free.set(index, true); | ||||
|                         } | ||||
|                         continue; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             i += 1; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<&Settings> for DamageHazards { | ||||
|     fn from(value: &Settings) -> Self { | ||||
|         let damage = value.hazard_damage_per_turn; | ||||
|         Self { damage } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn eliminate_snakes(board: &mut Board) { | ||||
|     // eliminate out of health and out of bounds | ||||
|     let mut i = 0; | ||||
|     while i < board.snakes.len() { | ||||
|         let snake = &board.snakes[i]; | ||||
|         if snake.health == 0 || !board.is_in_bounds(snake.head()) { | ||||
|             let snake = board.snakes.remove(i); | ||||
|             for tile in snake.body.iter().skip(1) { | ||||
|                 if board.is_in_bounds(*tile) { | ||||
|                     let index = board.coord_to_linear(*tile); | ||||
|                     board.free.set(index, true); | ||||
|                 } | ||||
|             } | ||||
|             continue; | ||||
|         } | ||||
|         i += 1; | ||||
|     } | ||||
|  | ||||
|     // look for collisions | ||||
|     let mut collisions = vec![]; | ||||
|     for snake in &board.snakes { | ||||
|         let head = snake.head(); | ||||
|         let head_index = board.coord_to_linear(head); | ||||
|         if !board.free[head_index] { | ||||
|             collisions.push(snake.id); | ||||
|             continue; | ||||
|         } | ||||
|         for snake2 in &board.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 < board.snakes.len() { | ||||
|         if collisions.contains(&board.snakes[i].id) { | ||||
|             let snake = board.snakes.remove(i); | ||||
|             for tile in snake.body { | ||||
|                 let index = board.coord_to_linear(tile); | ||||
|                 board.free.set(index, true); | ||||
|             } | ||||
|             continue; | ||||
|         } | ||||
|         i += 1; | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn feed_snakes(board: &mut Board) { | ||||
|     let mut eaten_food = vec![]; | ||||
|     for i in 0..board.snakes.len() { | ||||
|         let head = board.snakes[i].head(); | ||||
|         if board.is_in_bounds(head) { | ||||
|             let head_index = board.coord_to_linear(head); | ||||
|             if board.food[head_index] { | ||||
|                 eaten_food.push(head_index); | ||||
|                 board.snakes[i].feed(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     for food_index in eaten_food { | ||||
|         board.food.set(food_index, false); | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub fn game_over(board: &Board) -> bool { | ||||
|     board.num_snakes() <= 1 | ||||
| } | ||||
| @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| use super::{Coord, Direction}; | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] | ||||
| pub struct Request { | ||||
|     /// Game object describing the game being played. | ||||
|     pub game: Game, | ||||
| @@ -16,7 +16,7 @@ pub struct Request { | ||||
|     pub you: Battlesnake, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] | ||||
| pub struct Game { | ||||
|     /// A unique identifier for this Game. | ||||
|     pub id: Arc<str>, | ||||
| @@ -36,7 +36,7 @@ pub struct Game { | ||||
|     pub source: Arc<str>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] | ||||
| pub struct Ruleset { | ||||
|     /// Name of the ruleset being used to run this game. | ||||
|     pub name: Arc<str>, | ||||
| @@ -47,7 +47,7 @@ pub struct Ruleset { | ||||
|     pub settings: Settings, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Settings { | ||||
|     /// Percentage chance of spawning a new food every round. | ||||
| @@ -61,14 +61,14 @@ pub struct Settings { | ||||
|     pub royale: RoyaleSettings, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct RoyaleSettings { | ||||
|     /// The number of turns between generating new hazards (shrinking the safe board space). | ||||
|     pub shrink_every_n_turns: u8, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] | ||||
| pub struct Board { | ||||
|     /// The number of rows in the y-axis of the game board. | ||||
|     pub height: u8, | ||||
| @@ -83,7 +83,7 @@ pub struct Board { | ||||
|     pub snakes: Vec<Battlesnake>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] | ||||
| pub struct Battlesnake { | ||||
|     /// Unique identifier for this Battlesnake in the context of the current Game. | ||||
|     pub id: Arc<str>, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user