create simple simulation datastructures

This commit is contained in:
Max Känner 2025-01-16 20:05:36 +01:00
parent f3eba2ba75
commit d9ee6ae2f7
6 changed files with 280 additions and 53 deletions

40
Cargo.lock generated
View File

@ -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"

View File

@ -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"

View File

@ -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<Request>) {
info!("got start request: {request:#?}");
let board = Board::from(&*request);
info!("got start request: {board}");
}
async fn get_move(request: Json<Request>) -> response::Json<Response> {
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<Request>) -> response::Json<Response> {
}
async fn end(request: Json<Request>) {
info!("got end request: {request:#?}");
let board = Board::from(&*request);
info!("got end request: {board}");
}

View File

@ -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,
}

View File

@ -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<Snake>,
}
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<Coord>,
}
impl PartialOrd for Snake {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
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 }
})
}
}

View File

@ -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<Coord>,
pub food: Vec<Coord>,
/// Array of coordinates representing hazardous locations on the game board.
hazards: Vec<Coord>,
pub hazards: Vec<Coord>,
/// Array of Battlesnake objects representing all Battlesnakes remaining on the game board
/// (including yourself if you haven't been eliminated).
snakes: Vec<Battlesnake>,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
pub struct Coord {
x: u16,
y: u16,
pub snakes: Vec<Battlesnake>,
}
#[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<Coord>,
pub body: Vec<Coord>,
/// 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<String>,
}
#[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,
}