separate library and executable

This commit is contained in:
Max Känner 2024-10-05 20:19:05 +02:00
parent c5b0319ac4
commit a4dc6eb6da
4 changed files with 205 additions and 196 deletions

183
battlesnake/src/lib.rs Normal file
View File

@ -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<String>,
}
fn is_default<T: Default + PartialEq>(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<Coord>,
/// Array of Battlesnakes representing all Battlesnakes remaining on the game board (including
/// yourself if you haven't been eliminated).
pub snakes: Vec<Battlesnake>,
/// Array of coordinates representing hazardous locations on the game board.
pub hazards: Vec<Coord>,
}
#[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<Coord>,
/// 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<String>,
/// 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,
}

View File

@ -19,6 +19,7 @@ use std::{
use log::{error, info}; use log::{error, info};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use rand::thread_rng;
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::{ use crate::{
@ -205,7 +206,9 @@ impl Node {
) -> Result<Option<SnakeToken>, DeadlineError> { ) -> Result<Option<SnakeToken>, DeadlineError> {
let winner = if self.statistic.played == 0 { let winner = if self.statistic.played == 0 {
// We didn't simulate a game for this node yet. Do that // 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 { if Instant::now() >= *deadline {
return Err(DeadlineError); return Err(DeadlineError);
} }
@ -239,7 +242,7 @@ impl Node {
if Instant::now() >= *deadline { if Instant::now() >= *deadline {
return Err(DeadlineError); return Err(DeadlineError);
} }
board.simulate_actions(&actions); board.simulate_actions(&actions, &mut thread_rng());
let winner = self let winner = self
.childs .childs
.entry(actions.clone()) .entry(actions.clone())
@ -287,7 +290,7 @@ impl Node {
.snakes() .snakes()
.filter_map(|snake| Some((snake, board.snake_length(snake)?))) .filter_map(|snake| Some((snake, board.snake_length(snake)?)))
.collect(); .collect();
board.simulate_until(|board| { board.simulate_until(&mut thread_rng(), |board| {
if Instant::now() >= *deadline { if Instant::now() >= *deadline {
return true; return true;
} }
@ -333,7 +336,7 @@ impl Node {
if Instant::now() >= *deadline { if Instant::now() >= *deadline {
return Err(DeadlineError); return Err(DeadlineError);
} }
board.simulate_actions(&actions); board.simulate_actions(&actions, &mut thread_rng());
let lengths = self let lengths = self
.childs .childs
.entry(actions.clone()) .entry(actions.clone())

View File

@ -1,6 +1,6 @@
#![allow(clippy::needless_pass_by_value)] #![allow(clippy::needless_pass_by_value)]
use enum_iterator::Sequence; use battlesnake::{logic, Action, GameState};
use log::{error, info}; use log::{error, info};
use rocket::{ use rocket::{
fairing::AdHoc, fairing::AdHoc,
@ -10,190 +10,9 @@ use rocket::{
serde::{json::Json, Deserialize}, serde::{json::Json, Deserialize},
tokio::task, tokio::task,
}; };
use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use std::env; 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<String>,
}
fn is_default<T: Default + PartialEq>(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<Coord>,
/// Array of Battlesnakes representing all Battlesnakes remaining on the game board (including
/// yourself if you haven't been eliminated).
snakes: Vec<Battlesnake>,
/// Array of coordinates representing hazardous locations on the game board.
hazards: Vec<Coord>,
}
#[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<Coord>,
/// 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<String>,
/// 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("/")] #[get("/")]
fn handle_index() -> Json<Value> { fn handle_index() -> Json<Value> {
Json(logic::info()) Json(logic::info())

View File

@ -86,6 +86,10 @@ impl Board {
} }
} }
pub const fn turn(&self) -> i32 {
self.turn
}
#[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_sign_loss)]
pub const fn spaces(&self) -> usize { pub const fn spaces(&self) -> usize {
self.height as usize * self.width as usize self.height as usize * self.width as usize
@ -103,7 +107,11 @@ impl Board {
self.snakes.get(&token).map(|snake| snake.body.len()) self.snakes.get(&token).map(|snake| snake.body.len())
} }
pub fn simulate_actions(&mut self, actions: &BTreeMap<SnakeToken, Direction>) { pub fn simulate_actions(
&mut self,
actions: &BTreeMap<SnakeToken, Direction>,
rng: &mut impl Rng,
) {
// move snakes // move snakes
for (token, snake) in &mut self.snakes { for (token, snake) in &mut self.snakes {
snake.perform_action(actions.get(token).copied().unwrap_or(Direction::Up)); snake.perform_action(actions.get(token).copied().unwrap_or(Direction::Up));
@ -152,7 +160,7 @@ impl Board {
// spawn new food // spawn new food
if self.food.len() < usize::from(self.min_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) let free_fields = (0..self.width)
.flat_map(|x| (0..self.height).map(move |y| Coord { x, y })) .flat_map(|x| (0..self.height).map(move |y| Coord { x, y }))
@ -164,7 +172,7 @@ impl Board {
.any(|body| body == coord) .any(|body| body == coord)
}) })
.filter(|coord| self.food.contains(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); self.food.insert(field);
} }
} }
@ -172,7 +180,7 @@ impl Board {
self.turn += 1; 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) { while !exit(self) {
let actions = self let actions = self
.possible_actions() .possible_actions()
@ -180,15 +188,11 @@ impl Board {
.map(|(token, actions)| { .map(|(token, actions)| {
( (
*token, *token,
actions actions.iter().choose(rng).copied().unwrap_or(Direction::Up),
.iter()
.choose(&mut rand::thread_rng())
.copied()
.unwrap_or(Direction::Up),
) )
}) })
.collect(); .collect();
self.simulate_actions(&actions); self.simulate_actions(&actions, rng);
} }
} }