From d9ee6ae2f7c56d63a647caa5efa147c41c273905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20K=C3=A4nner?= Date: Thu, 16 Jan 2025 20:05:36 +0100 Subject: [PATCH] create simple simulation datastructures --- Cargo.lock | 40 +++++++ battlesnake/Cargo.toml | 6 + battlesnake/src/main.rs | 15 ++- battlesnake/src/types/mod.rs | 21 ++++ battlesnake/src/types/simulation.rs | 169 ++++++++++++++++++++++++++++ battlesnake/src/types/wire.rs | 82 ++++++-------- 6 files changed, 280 insertions(+), 53 deletions(-) create mode 100644 battlesnake/src/types/simulation.rs diff --git a/Cargo.lock b/Cargo.lock index bb4650a..c9dfcde 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,12 +172,25 @@ name = "battlesnake" version = "2.0.0" dependencies = [ "axum", + "bitvec", "env_logger", "log", "serde", "tokio", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -330,6 +343,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures-channel" version = "0.3.31" @@ -660,6 +679,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -870,6 +895,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "thiserror" version = "2.0.11" @@ -1126,6 +1157,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "xtask" version = "0.1.0" diff --git a/battlesnake/Cargo.toml b/battlesnake/Cargo.toml index db23914..f19b503 100644 --- a/battlesnake/Cargo.toml +++ b/battlesnake/Cargo.toml @@ -16,8 +16,14 @@ pedantic = "warn" nursery = "warn" [dependencies] +# server tokio = { version = "1.43", features = ["net", "macros", "rt-multi-thread"] } axum = { version = "0.8", features = ["http2", "multipart", "ws"] } serde = { version = "1.0", features = ["derive"] } + +# logging log = "0.4" env_logger = "0.11" + +# other +bitvec = "1.0" diff --git a/battlesnake/src/main.rs b/battlesnake/src/main.rs index d317019..2433bba 100644 --- a/battlesnake/src/main.rs +++ b/battlesnake/src/main.rs @@ -9,7 +9,11 @@ use axum::{ use log::{debug, info}; use serde::Serialize; use tokio::net::TcpListener; -use types::wire::{Direction, Request, Response}; +use types::{ + simulation::Board, + wire::{Request, Response}, + Direction, +}; #[tokio::main] async fn main() { @@ -51,11 +55,13 @@ struct Info { } async fn start(request: Json) { - info!("got start request: {request:#?}"); + let board = Board::from(&*request); + info!("got start request: {board}"); } async fn get_move(request: Json) -> response::Json { - info!("got move request: {request:#?}"); + let board = Board::from(&*request); + info!("got move request: {board}"); response::Json(Response { direction: Direction::Up, shout: None, @@ -63,5 +69,6 @@ async fn get_move(request: Json) -> response::Json { } async fn end(request: Json) { - info!("got end request: {request:#?}"); + let board = Board::from(&*request); + info!("got end request: {board}"); } diff --git a/battlesnake/src/types/mod.rs b/battlesnake/src/types/mod.rs index 349da7c..74ebf56 100644 --- a/battlesnake/src/types/mod.rs +++ b/battlesnake/src/types/mod.rs @@ -1 +1,22 @@ +use serde::{Deserialize, Serialize}; + +pub mod simulation; pub mod wire; + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] +pub struct Coord { + x: u8, + y: u8, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)] +pub enum Direction { + /// Move in positive y direction + Up, + /// Move in negative y direction + Down, + /// Move in negative x direction + Left, + /// Move in positive x direction + Right, +} diff --git a/battlesnake/src/types/simulation.rs b/battlesnake/src/types/simulation.rs new file mode 100644 index 0000000..bff271a --- /dev/null +++ b/battlesnake/src/types/simulation.rs @@ -0,0 +1,169 @@ +use std::{collections::VecDeque, fmt::Display}; + +use bitvec::prelude::*; +use log::error; + +use super::{wire::Request, Coord}; + +#[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: BitBox, + hazard: BitBox, + free: BitBox, + snakes: Vec, +} + +impl From<&Request> for Board { + fn from(value: &Request) -> Self { + let width = value.board.width; + let height = value.board.height; + let fields = usize::from(width) * usize::from(height); + 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: bitbox![0; fields], + hazard: bitbox![0; fields], + free: bitbox![1; fields], + snakes: Vec::with_capacity(value.board.snakes.len()), + }; + + for &food in &value.board.food { + let index = usize::from(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)); + 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)); + board.free.set(index, false); + } + let snake = Snake { + body: snake.body.iter().copied().collect(), + id: u8::try_from(id).unwrap_or(u8::MAX), + health: snake.health, + }; + board.snakes.push(snake); + } + + board + } +} + +impl Display for Board { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + writeln!( + f, + "{}x{} {}% ({}) {}dmg @ {}", + self.width, + self.height, + self.food_spawn_chance, + self.min_food, + self.hazard_damage, + self.turn + )?; + + for y in (0..self.height).rev() { + for x in 0..self.width { + let tile = Coord { x, y }; + if self.snakes.iter().any(|snake| snake.head() == tile) { + write!(f, "H")?; + continue; + } + if self.snakes.iter().any(|snake| snake.tail() == tile) { + write!(f, "T")?; + continue; + } + + let index = usize::from(self.coord_to_linear(tile)); + if !self.free[index] { + write!(f, "S")?; + continue; + } + if self.food[index] { + write!(f, "f")?; + continue; + } + if self.hazard[index] { + write!(f, "h")?; + continue; + } + + write!(f, ".")?; + } + writeln!(f)?; + } + + Ok(()) + } +} + +impl Board { + pub fn is_food(&self, tile: Coord) -> bool { + let index = usize::from(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)); + self.hazard[index] + } + + pub fn is_blocked(&self, tile: Coord) -> bool { + let index = usize::from(self.coord_to_linear(tile)); + !self.free[index] + } + + fn coord_to_linear(&self, coord: Coord) -> u16 { + u16::from(coord.x) + u16::from(coord.y) * u16::from(self.width) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct Snake { + id: u8, + health: u8, + body: VecDeque, +} + +impl PartialOrd for Snake { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Snake { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + +impl Snake { + pub fn head(&self) -> Coord { + self.body.front().copied().unwrap_or_else(|| { + error!("Snake without a head: {self:?}"); + Coord { x: 0, y: 0 } + }) + } + + pub fn tail(&self) -> Coord { + self.body.front().copied().unwrap_or_else(|| { + error!("Snake without a tail: {self:?}"); + Coord { x: 0, y: 0 } + }) + } +} diff --git a/battlesnake/src/types/wire.rs b/battlesnake/src/types/wire.rs index d7c26c1..214686b 100644 --- a/battlesnake/src/types/wire.rs +++ b/battlesnake/src/types/wire.rs @@ -1,27 +1,29 @@ use serde::{Deserialize, Serialize}; +use super::{Coord, Direction}; + #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] pub struct Request { /// Game object describing the game being played. - game: Game, + pub game: Game, /// Turn number for this move. - turn: u32, + pub turn: u32, /// Board object describing the initial state of the game board. - board: Board, + pub board: Board, /// Battlesnake Object describing your Battlesnake. - you: Battlesnake, + pub you: Battlesnake, } #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] pub struct Game { /// A unique identifier for this Game. - id: String, + pub id: String, /// Information about the ruleset being used to run this Game. - ruleset: Ruleset, + pub ruleset: Ruleset, /// The name of the map being played on. - map: String, + pub map: String, /// How much time your snake has to respond to requests for this Game. - timeout: u16, + pub timeout: u16, /// The source of this Game. /// One of: /// - tournament @@ -29,87 +31,81 @@ pub struct Game { /// - arena /// - challenge /// - custom - source: String, + pub source: String, } #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] pub struct Ruleset { /// Name of the ruleset being used to run this game. - name: String, + pub name: String, /// The release version of the Rules module used in this game. - version: String, + pub version: String, /// A collection of specific settings being used by the current game that control how the rules /// are applied. - settings: Settings, + pub settings: Settings, } #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Settings { /// Percentage chance of spawning a new food every round. - food_spawn_chance: u8, + pub food_spawn_chance: u8, /// Minimum food to keep on the board every turn. - minimum_food: u16, + pub minimum_food: u16, /// 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. - hazard_damage_per_turn: u8, + pub hazard_damage_per_turn: u8, /// Settings for the royale game mode - royale: RoyaleSettings, + pub royale: RoyaleSettings, } #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RoyaleSettings { /// The number of turns between generating new hazards (shrinking the safe board space). - shrink_every_n_turns: u8, + pub shrink_every_n_turns: u8, } #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] pub struct Board { /// The number of rows in the y-axis of the game board. - height: u16, + pub height: u8, /// The number of rows in the x-axis of the game board. - width: u16, + pub width: u8, /// Array of coordinates representing food locations on the game board. - food: Vec, + pub food: Vec, /// Array of coordinates representing hazardous locations on the game board. - hazards: Vec, + pub hazards: Vec, /// Array of Battlesnake objects representing all Battlesnakes remaining on the game board /// (including yourself if you haven't been eliminated). - snakes: Vec, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] -pub struct Coord { - x: u16, - y: u16, + pub snakes: Vec, } #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] pub struct Battlesnake { /// Unique identifier for this Battlesnake in the context of the current Game. - id: String, + pub id: String, /// Name given to this Battlesnake by its author - name: String, + pub name: String, /// Health value of this Battlesnake, between 0 and 100 - health: u8, + pub health: u8, /// Array of coordinates representing the Battlesnake's location on the game board. /// This array is ordered from head to tail. - body: Vec, + pub body: Vec, /// 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, + pub latency: String, /// Coordinates for this Battlesnake's head. /// Equivalent to the first element of the body array. - head: Coord, + pub head: Coord, /// Length of this Battlesnake from head to tail. /// Equivalent to the length of the body array. - length: u32, + pub length: u16, /// Message shouted by this Battlesnake on the previous turn - shout: String, + pub shout: String, /// The squad that the Battlesnake belongs to. /// Used to identify squad members in Squad Mode games. - squad: String, + pub squad: String, } #[derive(Debug, PartialEq, Eq, Clone, Serialize)] @@ -121,15 +117,3 @@ pub struct Response { /// Must be 256 characters or less. pub shout: Option, } - -#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)] -pub enum Direction { - /// Move in positive y direction - Up, - /// Move in negative y direction - Down, - /// Move in negative x direction - Left, - /// Move in positive x direction - Right, -}