diff --git a/battlesnake/src/lib.rs b/battlesnake/src/lib.rs new file mode 100644 index 0000000..97a6ba1 --- /dev/null +++ b/battlesnake/src/lib.rs @@ -0,0 +1,183 @@ +use enum_iterator::Sequence; +use serde::{Deserialize, Serialize}; + +pub mod logic; +pub mod simulation; + +pub const MAX_HEALTH: i32 = 100; + +// API and Response Objects +// See https://docs.battlesnake.com/api + +#[derive( + Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Deserialize, Serialize, Sequence, +)] +#[serde(rename_all = "lowercase")] +pub enum Direction { + /// Move left (-x) + Left, + /// Move up (+y) + Up, + /// Move right (+x) + Right, + /// Move down (-y) + Down, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Action { + /// In which direction the snake should move + pub r#move: Direction, + /// Say something to the other snakes + #[serde(default, skip_serializing_if = "is_default")] + pub shout: Option, +} + +fn is_default(value: &T) -> bool { + *value == T::default() +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct Game { + /// A unique identifier for this Game + pub id: String, + /// Information about the ruleset being used to run this game + pub ruleset: Ruleset, + /// The name of the map being played on. + pub map: String, + /// How much time your snake has to respond to requests for this Game + pub timeout: u32, + /// The source of this game. + /// + /// One of: + /// - "tournament" + /// - "league" + /// - "arena" + /// - "challenge" + /// - "custom" + /// + /// The values may change. + pub source: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Ruleset { + /// Name of the ruleset being used to run this game. + pub name: String, + /// The release version of the [Rules](https://github.com/BattlesnakeOfficial/rules) module used in this game. + pub version: String, + /// A collection of specific settings being used by the current game that control how the rules + /// are applied. + pub settings: RulesetSettings, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RulesetSettings { + /// Percentage chance of spawning a new food every round. + #[serde(rename = "foodSpawnChance")] + pub food_spawn_chance: u8, + /// Minimum food to keep on the board every turn. + #[serde(rename = "minimumFood")] + pub minimum_food: u8, + /// Health damage a snake will take when ending its turn in a hazard. This stacks on top of the + /// regular 1 damage a snake takes per turn. + #[serde(rename = "hazardDamagePerTurn")] + pub hazard_damage_per_turn: u8, + /// rules for the royale mode + pub royale: RulesetRoyale, + /// rules for the squad mode + pub squad: RulesetSquad, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct RulesetRoyale { + /// The number of turns between generating new hazards (shrinking the safe board space). + #[serde(rename = "shrinkEveryNTurns")] + pub shrink_every_n_turns: i32, +} + +#[allow(clippy::struct_excessive_bools)] +#[derive(Debug, Deserialize, Serialize)] +pub struct RulesetSquad { + /// Allow members of the same squad to move over each other without dying. + #[serde(rename = "allowBodyCollisions")] + pub allow_body_collisions: bool, + /// All squad members are eliminated when one is eliminated. + #[serde(rename = "sharedElimination")] + pub shared_elimination: bool, + /// All squad members share health. + #[serde(rename = "sharedHealth")] + pub shared_health: bool, + /// All squad members share length. + #[serde(rename = "sharedLength")] + pub shared_length: bool, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Board { + /// The number of rows in the y-axis of the game board. + pub height: i32, + /// The number of columns in the x-axis of the game board. + pub width: i32, + /// Array of coordinates representing food locations on the game board. + pub food: Vec, + /// Array of Battlesnakes representing all Battlesnakes remaining on the game board (including + /// yourself if you haven't been eliminated). + pub snakes: Vec, + /// Array of coordinates representing hazardous locations on the game board. + pub hazards: Vec, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct Battlesnake { + /// Unique identifier for this Battlesnake in the context of the current Game + pub id: String, + /// Name given to this Battlesnake by its author + pub name: String, + /// Health value of this Battlesnake, between 0 and 100 + pub health: i32, + /// Array of coordinates representing this Battlesnake's location on the game board. This array + /// is ordered from head to tail + pub body: Vec, + /// Coordinates for this Battlesnake's head. Equivalent to the first element of the body array. + pub head: Coord, + /// Length of this Battlesnake from head to tail. Equivalent to the length of the body array. + pub length: i32, + /// The previous response time of this Battlesnake, in milliseconds. If the Battlesnake timed + /// out and failed to respond, the game timeout will be returned. + pub latency: String, + /// Message shouted by this Battlesnake on the previous turn. + pub shout: Option, + /// The squad that the Battlesnake belongs to. Used to identify squad members in Squad Mode + /// games. + pub squad: String, + // /// The collection of customizations that control how this Battlesnake is displayed. + // customizations: {color, head, tail} +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Deserialize, Serialize)] +pub struct Coord { + pub x: i32, + pub y: i32, +} + +impl Coord { + #[must_use] + pub const fn move_to(mut self, direction: Direction) -> Self { + match direction { + Direction::Left => self.x -= 1, + Direction::Up => self.y += 1, + Direction::Right => self.x += 1, + Direction::Down => self.y -= 1, + } + self + } +} + +#[derive(Deserialize, Serialize, Debug)] +pub struct GameState { + pub game: Game, + pub turn: i32, + pub board: Board, + pub you: Battlesnake, +} diff --git a/battlesnake/src/logic.rs b/battlesnake/src/logic.rs index 8de4b80..de24baa 100644 --- a/battlesnake/src/logic.rs +++ b/battlesnake/src/logic.rs @@ -19,6 +19,7 @@ use std::{ use log::{error, info}; use ordered_float::OrderedFloat; +use rand::thread_rng; use serde_json::{json, Value}; use crate::{ @@ -205,7 +206,9 @@ impl Node { ) -> Result, DeadlineError> { let winner = if self.statistic.played == 0 { // We didn't simulate a game for this node yet. Do that - board.simulate_until(|board| board.alive_snakes() <= 1 || Instant::now() >= *deadline); + board.simulate_until(&mut thread_rng(), |board| { + board.alive_snakes() <= 1 || Instant::now() >= *deadline + }); if Instant::now() >= *deadline { return Err(DeadlineError); } @@ -239,7 +242,7 @@ impl Node { if Instant::now() >= *deadline { return Err(DeadlineError); } - board.simulate_actions(&actions); + board.simulate_actions(&actions, &mut thread_rng()); let winner = self .childs .entry(actions.clone()) @@ -287,7 +290,7 @@ impl Node { .snakes() .filter_map(|snake| Some((snake, board.snake_length(snake)?))) .collect(); - board.simulate_until(|board| { + board.simulate_until(&mut thread_rng(), |board| { if Instant::now() >= *deadline { return true; } @@ -333,7 +336,7 @@ impl Node { if Instant::now() >= *deadline { return Err(DeadlineError); } - board.simulate_actions(&actions); + board.simulate_actions(&actions, &mut thread_rng()); let lengths = self .childs .entry(actions.clone()) diff --git a/battlesnake/src/main.rs b/battlesnake/src/main.rs index 1f3336d..d4e5440 100644 --- a/battlesnake/src/main.rs +++ b/battlesnake/src/main.rs @@ -1,6 +1,6 @@ #![allow(clippy::needless_pass_by_value)] -use enum_iterator::Sequence; +use battlesnake::{logic, Action, GameState}; use log::{error, info}; use rocket::{ fairing::AdHoc, @@ -10,190 +10,9 @@ use rocket::{ serde::{json::Json, Deserialize}, tokio::task, }; -use serde::Serialize; use serde_json::Value; use std::env; -mod logic; -mod simulation; - -const MAX_HEALTH: i32 = 100; - -// API and Response Objects -// See https://docs.battlesnake.com/api - -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Deserialize, Serialize, Sequence, -)] -#[serde(rename_all = "lowercase")] -enum Direction { - /// Move left (-x) - Left, - /// Move up (+y) - Up, - /// Move right (+x) - Right, - /// Move down (-y) - Down, -} - -#[derive(Debug, Deserialize, Serialize)] -struct Action { - /// In which direction the snake should move - r#move: Direction, - /// Say something to the other snakes - #[serde(default, skip_serializing_if = "is_default")] - shout: Option, -} - -fn is_default(value: &T) -> bool { - *value == T::default() -} - -#[derive(Deserialize, Serialize, Debug)] -struct Game { - /// A unique identifier for this Game - id: String, - /// Information about the ruleset being used to run this game - ruleset: Ruleset, - /// The name of the map being played on. - map: String, - /// How much time your snake has to respond to requests for this Game - timeout: u32, - /// The source of this game. - /// - /// One of: - /// - "tournament" - /// - "league" - /// - "arena" - /// - "challenge" - /// - "custom" - /// - /// The values may change. - source: String, -} - -#[derive(Debug, Deserialize, Serialize)] -struct Ruleset { - /// Name of the ruleset being used to run this game. - name: String, - /// The release version of the [Rules](https://github.com/BattlesnakeOfficial/rules) module used in this game. - version: String, - /// A collection of specific settings being used by the current game that control how the rules - /// are applied. - settings: RulesetSettings, -} - -#[derive(Debug, Deserialize, Serialize)] -struct RulesetSettings { - /// Percentage chance of spawning a new food every round. - #[serde(rename = "foodSpawnChance")] - food_spawn_chance: u8, - /// Minimum food to keep on the board every turn. - #[serde(rename = "minimumFood")] - minimum_food: u8, - /// Health damage a snake will take when ending its turn in a hazard. This stacks on top of the - /// regular 1 damage a snake takes per turn. - #[serde(rename = "hazardDamagePerTurn")] - hazard_damage_per_turn: u8, - /// rules for the royale mode - royale: RulesetRoyale, - /// rules for the squad mode - squad: RulesetSquad, -} - -#[derive(Debug, Deserialize, Serialize)] -struct RulesetRoyale { - /// The number of turns between generating new hazards (shrinking the safe board space). - #[serde(rename = "shrinkEveryNTurns")] - shrink_every_n_turns: i32, -} - -#[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Deserialize, Serialize)] -struct RulesetSquad { - /// Allow members of the same squad to move over each other without dying. - #[serde(rename = "allowBodyCollisions")] - allow_body_collisions: bool, - /// All squad members are eliminated when one is eliminated. - #[serde(rename = "sharedElimination")] - shared_elimination: bool, - /// All squad members share health. - #[serde(rename = "sharedHealth")] - shared_health: bool, - /// All squad members share length. - #[serde(rename = "sharedLength")] - shared_length: bool, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -struct Board { - /// The number of rows in the y-axis of the game board. - height: i32, - /// The number of columns in the x-axis of the game board. - width: i32, - /// Array of coordinates representing food locations on the game board. - food: Vec, - /// Array of Battlesnakes representing all Battlesnakes remaining on the game board (including - /// yourself if you haven't been eliminated). - snakes: Vec, - /// Array of coordinates representing hazardous locations on the game board. - hazards: Vec, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -struct Battlesnake { - /// Unique identifier for this Battlesnake in the context of the current Game - id: String, - /// Name given to this Battlesnake by its author - name: String, - /// Health value of this Battlesnake, between 0 and 100 - health: i32, - /// Array of coordinates representing this Battlesnake's location on the game board. This array - /// is ordered from head to tail - body: Vec, - /// Coordinates for this Battlesnake's head. Equivalent to the first element of the body array. - head: Coord, - /// Length of this Battlesnake from head to tail. Equivalent to the length of the body array. - length: i32, - /// The previous response time of this Battlesnake, in milliseconds. If the Battlesnake timed - /// out and failed to respond, the game timeout will be returned. - latency: String, - /// Message shouted by this Battlesnake on the previous turn. - shout: Option, - /// The squad that the Battlesnake belongs to. Used to identify squad members in Squad Mode - /// games. - squad: String, - // /// The collection of customizations that control how this Battlesnake is displayed. - // customizations: {color, head, tail} -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Deserialize, Serialize)] -struct Coord { - x: i32, - y: i32, -} - -impl Coord { - const fn move_to(mut self, direction: Direction) -> Self { - match direction { - Direction::Left => self.x -= 1, - Direction::Up => self.y += 1, - Direction::Right => self.x += 1, - Direction::Down => self.y -= 1, - } - self - } -} - -#[derive(Deserialize, Serialize, Debug)] -struct GameState { - game: Game, - turn: i32, - board: Board, - you: Battlesnake, -} - #[get("/")] fn handle_index() -> Json { Json(logic::info()) diff --git a/battlesnake/src/simulation.rs b/battlesnake/src/simulation.rs index 22eae29..f89976e 100644 --- a/battlesnake/src/simulation.rs +++ b/battlesnake/src/simulation.rs @@ -86,6 +86,10 @@ impl Board { } } + pub const fn turn(&self) -> i32 { + self.turn + } + #[allow(clippy::cast_sign_loss)] pub const fn spaces(&self) -> usize { self.height as usize * self.width as usize @@ -103,7 +107,11 @@ impl Board { self.snakes.get(&token).map(|snake| snake.body.len()) } - pub fn simulate_actions(&mut self, actions: &BTreeMap) { + pub fn simulate_actions( + &mut self, + actions: &BTreeMap, + rng: &mut impl Rng, + ) { // move snakes for (token, snake) in &mut self.snakes { snake.perform_action(actions.get(token).copied().unwrap_or(Direction::Up)); @@ -152,7 +160,7 @@ impl Board { // spawn new food if self.food.len() < usize::from(self.min_food) - || rand::thread_rng().gen_ratio(u32::from(self.food_chance), 100) + || rng.gen_ratio(u32::from(self.food_chance), 100) { let free_fields = (0..self.width) .flat_map(|x| (0..self.height).map(move |y| Coord { x, y })) @@ -164,7 +172,7 @@ impl Board { .any(|body| body == coord) }) .filter(|coord| self.food.contains(coord)); - if let Some(field) = free_fields.choose(&mut rand::thread_rng()) { + if let Some(field) = free_fields.choose(rng) { self.food.insert(field); } } @@ -172,7 +180,7 @@ impl Board { self.turn += 1; } - pub fn simulate_until(&mut self, mut exit: impl FnMut(&Self) -> bool) { + pub fn simulate_until(&mut self, rng: &mut impl Rng, mut exit: impl FnMut(&Self) -> bool) { while !exit(self) { let actions = self .possible_actions() @@ -180,15 +188,11 @@ impl Board { .map(|(token, actions)| { ( *token, - actions - .iter() - .choose(&mut rand::thread_rng()) - .copied() - .unwrap_or(Direction::Up), + actions.iter().choose(rng).copied().unwrap_or(Direction::Up), ) }) .collect(); - self.simulate_actions(&actions); + self.simulate_actions(&actions, rng); } }