Compare commits

..

25 Commits

Author SHA1 Message Date
879f99e23f multi agent mcts
All checks were successful
Build / build (push) Successful in 2m10s
2025-01-26 19:29:45 +01:00
302f5cac50 fix regression test 2025-01-26 19:29:24 +01:00
d715aa47b3 allow changing the port
All checks were successful
Build / build (push) Successful in 2m31s
2025-01-25 23:55:56 +01:00
6f04d7cb7f single agent mcts
All checks were successful
Build / build (push) Successful in 2m30s
2025-01-25 23:49:05 +01:00
3f91660583 improve scoring system
All checks were successful
Build / build (push) Successful in 1m59s
2025-01-22 01:33:45 +01:00
32883b2a1b spawn blocking task for simulations
All checks were successful
Build / build (push) Successful in 2m2s
2025-01-22 01:17:11 +01:00
c26da8869c use full tokio features 2025-01-22 01:16:00 +01:00
d3abaf61a7 use division remainder as second criterion
All checks were successful
Build / build (push) Successful in 1m59s
2025-01-22 01:00:29 +01:00
d46e2d8163 correct food spawning algorithm 2025-01-22 00:59:55 +01:00
5b440bc7db simple monte carlo simulation
All checks were successful
Build / build (push) Successful in 1m58s
2025-01-22 00:43:07 +01:00
d858d88e76 fix health reset on feeding 2025-01-22 00:42:55 +01:00
3a163fd6e7 save u8 to string id mapping
All checks were successful
Build / build (push) Successful in 1m59s
2025-01-21 23:20:43 +01:00
108aeaa49d skip calculating index from id when index already present
All checks were successful
Build / build (push) Successful in 1m59s
2025-01-21 22:40:01 +01:00
ea96a0eb0d put small field bitfields on the stack
All checks were successful
Build / build (push) Successful in 2m1s
2025-01-21 20:25:54 +01:00
47f75563ae add clone benchmark 2025-01-21 20:21:23 +01:00
5bb7476af6 use VecDeque in intended direction
All checks were successful
Build / build (push) Successful in 2m2s
2025-01-21 19:26:43 +01:00
2f2b7ac11e add simulation benchmarks
All checks were successful
Build / build (push) Successful in 2m7s
2025-01-21 19:04:30 +01:00
1410602c6c bump rust version
All checks were successful
Build / build (push) Successful in 1m53s
2025-01-17 00:36:38 +01:00
5e81a7952f correct simulation
Some checks failed
Build / build (push) Failing after 1m54s
2025-01-17 00:33:54 +01:00
9c76df1f69 calculate valid moves
Some checks failed
Build / build (push) Failing after 1m52s
2025-01-16 21:06:49 +01:00
d91d0e0a82 remove rocket artefact 2025-01-16 20:33:17 +01:00
f9e34d119a fix docker lint
All checks were successful
Build / build (push) Successful in 1m56s
2025-01-16 20:18:02 +01:00
d9ee6ae2f7 create simple simulation datastructures 2025-01-16 20:17:40 +01:00
f3eba2ba75 add simple ci 2025-01-16 20:17:40 +01:00
932023451a accept battlesnake requests 2025-01-16 17:59:35 +01:00
13 changed files with 1844 additions and 1485 deletions

17
.github/workflows/build_docker.yaml vendored Normal file
View File

@ -0,0 +1,17 @@
name: Build
run-name: ${{ gitea.actor }} is runs ci pipeline
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: false

1543
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
FROM rust:1.81-bookworm as build FROM rust:1.84-bookworm AS build
COPY battlesnake/ /usr/app COPY battlesnake/ /usr/app
WORKDIR /usr/app WORKDIR /usr/app

View File

@ -1,7 +1,7 @@
[package] [package]
authors = ["Max Känner"] authors = ["Max Känner"]
name = "battlesnake" name = "battlesnake"
version = "1.0.0" version = "2.0.0"
edition = "2021" edition = "2021"
readme = "README.md" readme = "README.md"
@ -16,22 +16,24 @@ pedantic = "warn"
nursery = "warn" nursery = "warn"
[dependencies] [dependencies]
rocket = { version = "0.5.0", features = ["json"] } # server
serde = { version = "1.0.117", features = ["derive"] } tokio = { version = "1.43", features = ["full"] }
serde_json = "1.0.59" axum = { version = "0.8", features = ["http2", "multipart", "ws"] }
log = "0.4.0" serde = { version = "1.0", features = ["derive", "rc"] }
env_logger = "0.11.5"
rand = "0.8.4" # logging
log = "0.4"
env_logger = "0.11"
# other
bitvec = "1.0"
enum-iterator = "2.1" enum-iterator = "2.1"
iter_tools = "0.24" rand = "0.8"
ordered-float = "4.3.0" float-ord = "0.3"
dashmap = "6.1.0"
nalgebra = "0.33.2"
battlesnake-game-types = "0.17.0" [dev-dependencies]
criterion = "0.5"
[[bench]]
[profile.release] name = "simulation"
lto = "fat" harness = false
codegen-units = 1
panic = "abort"

View File

@ -1,4 +0,0 @@
[default]
address = "0.0.0.0"
port = 8000
keep_alive = 0

View File

@ -0,0 +1,235 @@
use std::sync::{
atomic::{AtomicU32, Ordering},
Arc,
};
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use battlesnake::types::{
simulation::Board,
wire::{Battlesnake, Board as WireBoard, Game, Request, RoyaleSettings, Ruleset, Settings},
Coord,
};
fn create_start_snake(coord: Coord) -> Battlesnake {
let id: Arc<str> = format!("{coord:?}").into();
Battlesnake {
id: id.clone(),
name: id.clone(),
health: 100,
body: vec![coord; 3],
latency: "0".into(),
head: coord,
length: 3,
shout: None,
squad: id,
}
}
fn create_standard_start_request(starts: [Coord; 4]) -> Request {
Request {
game: Game {
id: "test".into(),
ruleset: Ruleset {
name: "standard".into(),
version: "0".into(),
settings: Settings {
food_spawn_chance: 15,
minimum_food: 1,
hazard_damage_per_turn: 30,
royale: RoyaleSettings {
shrink_every_n_turns: 20,
},
},
},
map: "standard".into(),
timeout: 500,
source: "other".into(),
},
turn: 0,
board: WireBoard {
height: 11,
width: 11,
food: vec![Coord { x: 5, y: 5 }],
hazards: vec![],
snakes: vec![
create_start_snake(starts[0]),
create_start_snake(starts[1]),
create_start_snake(starts[2]),
create_start_snake(starts[3]),
],
},
you: create_start_snake(starts[0]),
}
}
fn standard(c: &mut Criterion) {
let turns_min = AtomicU32::new(u32::MAX);
let turns_max = AtomicU32::new(u32::MIN);
let turns_sum = AtomicU32::new(0);
let turns_total = AtomicU32::new(0);
let mut group = c.benchmark_group("standard");
group.sample_size(10000);
let benchmark = |b: &mut Bencher, board: &Board| {
b.iter(|| {
let mut board = board.clone();
let turn = board.simulate_random(|board| {
if board.num_snakes() <= 1 {
Some(board.turn())
} else {
None
}
});
if turn < turns_min.load(Ordering::Relaxed) {
turns_min.store(turn, Ordering::Relaxed);
}
if turn > turns_max.load(Ordering::Relaxed) {
turns_max.store(turn, Ordering::Relaxed);
}
turns_sum.fetch_add(turn, Ordering::Relaxed);
turns_total.fetch_add(1, Ordering::Relaxed);
});
};
let request = create_standard_start_request([
Coord { x: 1, y: 1 },
Coord { x: 9, y: 1 },
Coord { x: 1, y: 9 },
Coord { x: 9, y: 9 },
]);
let board = Board::from(&request);
group.bench_with_input(
BenchmarkId::from_parameter("start x"),
black_box(&board),
benchmark,
);
{
let max = turns_max.load(Ordering::Relaxed);
let min = turns_min.load(Ordering::Relaxed);
let sum = turns_sum.load(Ordering::Relaxed);
let total = turns_total.load(Ordering::Relaxed);
let avg = sum / total;
println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
turns_max.store(u32::MIN, Ordering::Relaxed);
turns_min.store(u32::MAX, Ordering::Relaxed);
turns_sum.store(0, Ordering::Relaxed);
turns_total.store(0, Ordering::Relaxed);
}
let request = create_standard_start_request([
Coord { x: 5, y: 1 },
Coord { x: 1, y: 5 },
Coord { x: 5, y: 9 },
Coord { x: 9, y: 5 },
]);
let board = Board::from(&request);
group.bench_with_input(
BenchmarkId::from_parameter("start +"),
black_box(&board),
benchmark,
);
{
let max = turns_max.load(Ordering::Relaxed);
let min = turns_min.load(Ordering::Relaxed);
let sum = turns_sum.load(Ordering::Relaxed);
let total = turns_total.load(Ordering::Relaxed);
let avg = sum / total;
println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
}
}
fn constrictor(c: &mut Criterion) {
let turns_min = AtomicU32::new(u32::MAX);
let turns_max = AtomicU32::new(u32::MIN);
let turns_sum = AtomicU32::new(0);
let turns_total = AtomicU32::new(0);
let mut group = c.benchmark_group("constrictor");
group.sample_size(10000);
let benchmark = |b: &mut Bencher, board: &Board| {
b.iter(|| {
let mut board = board.clone();
let turn = board.simulate_random(|board| {
if board.num_snakes() <= 1 {
Some(board.turn())
} else {
None
}
});
if turn < turns_min.load(Ordering::Relaxed) {
turns_min.store(turn, Ordering::Relaxed);
}
if turn > turns_max.load(Ordering::Relaxed) {
turns_max.store(turn, Ordering::Relaxed);
}
turns_sum.fetch_add(turn, Ordering::Relaxed);
turns_total.fetch_add(1, Ordering::Relaxed);
});
};
let mut request = create_standard_start_request([
Coord { x: 1, y: 1 },
Coord { x: 9, y: 1 },
Coord { x: 1, y: 9 },
Coord { x: 9, y: 9 },
]);
request.game.ruleset.name = "constrictor".into();
let board = Board::from(&request);
group.bench_with_input(
BenchmarkId::from_parameter("start x"),
black_box(&board),
benchmark,
);
{
let max = turns_max.load(Ordering::Relaxed);
let min = turns_min.load(Ordering::Relaxed);
let sum = turns_sum.load(Ordering::Relaxed);
let total = turns_total.load(Ordering::Relaxed);
let avg = sum / total;
println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
turns_max.store(u32::MIN, Ordering::Relaxed);
turns_min.store(u32::MAX, Ordering::Relaxed);
turns_sum.store(0, Ordering::Relaxed);
turns_total.store(0, Ordering::Relaxed);
}
let mut request = create_standard_start_request([
Coord { x: 5, y: 1 },
Coord { x: 1, y: 5 },
Coord { x: 5, y: 9 },
Coord { x: 9, y: 5 },
]);
request.game.ruleset.name = "constrictor".into();
let board = Board::from(&request);
group.bench_with_input(
BenchmarkId::from_parameter("start +"),
black_box(&board),
benchmark,
);
{
let max = turns_max.load(Ordering::Relaxed);
let min = turns_min.load(Ordering::Relaxed);
let sum = turns_sum.load(Ordering::Relaxed);
let total = turns_total.load(Ordering::Relaxed);
let avg = sum / total;
println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
}
}
fn clone(c: &mut Criterion) {
let request = create_standard_start_request([
Coord { x: 1, y: 1 },
Coord { x: 9, y: 1 },
Coord { x: 1, y: 9 },
Coord { x: 9, y: 9 },
]);
let board = Board::from(&request);
c.bench_function("clone", |b| b.iter(|| board.clone()));
}
criterion_group!(benches, standard, constrictor, clone);
criterion_main!(benches);

View File

@ -1,24 +1 @@
use battlesnake_game_types::types::Move; pub mod types;
use serde::{Deserialize, Serialize};
pub mod logic;
#[derive(Debug, Deserialize, Serialize)]
pub struct Response {
/// In which direction the snake should move
r#move: &'static str,
}
impl Response {
#[must_use]
pub const fn new(value: Move) -> Self {
Self {
r#move: match value {
Move::Left => "left",
Move::Down => "down",
Move::Up => "up",
Move::Right => "right",
},
}
}
}

View File

@ -1,359 +0,0 @@
// Welcome to
// __________ __ __ .__ __
// \______ \_____ _/ |__/ |_| | ____ ______ ____ _____ | | __ ____
// | | _/\__ \\ __\ __\ | _/ __ \ / ___// \\__ \ | |/ // __ \
// | | \ / __ \| | | | | |_\ ___/ \___ \| | \/ __ \| <\ ___/
// |________/(______/__| |__| |____/\_____>______>___|__(______/__|__\\_____>
//
// This file can be a nice home for your Battlesnake logic and helper functions.
//
// To get you started we've included code to prevent your Battlesnake from moving backwards.
// For more info see docs.battlesnake.com
use core::f64;
use std::{collections::HashMap, time::Instant};
use battlesnake_game_types::{
compact_representation::standard::CellBoard4Snakes11x11,
types::{
build_snake_id_map, LengthGettableGame, Move, RandomReasonableMovesGame,
ReasonableMovesGame, SimulableGame, SimulatorInstruments, SnakeIDMap, SnakeId,
VictorDeterminableGame, YouDeterminableGame,
},
wire_representation::Game,
};
use log::{error, info};
use ordered_float::OrderedFloat;
use rand::{prelude::*, thread_rng};
use rocket::time::{ext::NumericalDuration, Duration};
use serde_json::{json, Value};
// info is called when you create your Battlesnake on play.battlesnake.com
// and controls your Battlesnake's appearance
// TIP: If you open your Battlesnake URL in a browser you should see this data
#[must_use]
pub fn info() -> Value {
info!("INFO");
json!({
"apiversion": "1",
"author": "der-informatiker",
"color": "#00FFEE",
"head": "smart-caterpillar",
"tail": "mouse",
})
}
#[derive(Debug)]
pub struct GameState {
calculation_time: Duration,
snake_id_map: SnakeIDMap,
}
// start is called when your Battlesnake begins a game
#[must_use]
pub fn start(game: &Game) -> GameState {
info!("GAME START");
let snake_id_map = build_snake_id_map(game);
let calculation_time = (game.game.timeout / 2).milliseconds();
GameState {
calculation_time,
snake_id_map,
}
}
// end is called when your Battlesnake finishes a game
pub fn end(game: &Game, state: GameState) {
std::mem::drop(state);
info!("GAME OVER after {} turns", game.turn);
}
// move is called on every turn and returns your next move
// Valid moves are "up", "down", "left", or "right"
// See https://docs.battlesnake.com/api/example-move for available data
pub fn get_move(game: Game, state: &mut GameState, start: &Instant) -> Move {
let calc_start = Instant::now();
if calc_start - *start > 10.milliseconds() {
error!(
"The calculation was started long after the request ({}ms)",
(calc_start - *start).as_millis()
);
}
let deadline = *start + state.calculation_time;
let name = game.you.name.clone();
let turn = game.turn;
let solo = game.game.ruleset.name == "solo";
let Ok(board) = CellBoard4Snakes11x11::convert_from_game(game, &state.snake_id_map) else {
error!("Unable to fit board");
return Move::Down;
};
let mut tree = Node {
statistic: Statistics {
played: 0,
won: HashMap::new(),
},
child_statistics: HashMap::new(),
childs: HashMap::new(),
};
while Instant::now() < deadline {
if solo {
tree.monte_carlo_solo_step(&board);
} else {
tree.monte_carlo_step(&board);
}
}
let actions = tree.child_statistics.entry(*board.you_id()).or_default();
info!("actions {}: {actions:?}", name);
#[allow(clippy::cast_precision_loss)]
let chosen = actions
.iter()
.max_by_key(|(_, stat)| stat.played)
.map(|(direction, _)| *direction)
.or_else(|| {
board
.random_reasonable_move_for_each_snake(&mut thread_rng())
.find(|(snake_id, _)| snake_id == board.you_id())
.map(|(_, direction)| direction)
})
.unwrap_or(Move::Down);
info!(
"DIRECTION {turn}: {chosen:?} after {}ms ({name})",
start.elapsed().as_millis(),
);
chosen
}
#[derive(Debug, Clone, Copy)]
struct Instruments;
impl SimulatorInstruments for Instruments {
fn observe_simulation(&self, _duration: std::time::Duration) {}
}
#[derive(Debug, PartialEq, Eq, Clone, Default)]
struct Statistics {
/// Number of times this node was simulated
played: usize,
/// Number of times this node was simulated and the agent has won.
won: HashMap<SnakeId, usize>,
}
#[derive(Debug, PartialEq, Eq, Clone, Default)]
struct ActionStatistic {
played: usize,
won: usize,
}
#[derive(Debug, PartialEq, Eq, Clone, Default)]
struct Node {
statistic: Statistics,
child_statistics: HashMap<SnakeId, HashMap<Move, ActionStatistic>>,
childs: HashMap<[Option<(Move, u16)>; 4], Node>,
}
impl Node {
/// Performs one monte carlo simulation step
///
/// Returns the snake that has won the simulation
fn monte_carlo_step(&mut self, board: &CellBoard4Snakes11x11) -> Option<SnakeId> {
let stop_condition = CellBoard4Snakes11x11::is_over;
let winner = if stop_condition(board) {
board.get_winner()
} else if self.statistic.played == 0 {
// We didn't simulate a game for this node yet. Do that
let mut board = *board;
while !stop_condition(&board) {
let rng = &mut thread_rng();
let moves = board.random_reasonable_move_for_each_snake(rng);
let (_, new_board) = board
.simulate_with_moves(
&Instruments,
moves.map(|(snake_id, direction)| (snake_id, [direction])),
)
.next()
.unwrap();
board = new_board;
}
board.get_winner()
} else {
// select a node to simulate
let possible_actions = board.reasonable_moves_for_each_snake();
let actions = possible_actions
.filter_map(|(token, actions)| {
let statistics = self.child_statistics.entry(token).or_default();
let selected = actions.iter().copied().max_by_key(|direction| {
let statistics = statistics.entry(*direction).or_default();
if statistics.played == 0 {
return OrderedFloat(f64::INFINITY);
}
#[allow(clippy::cast_precision_loss)]
let exploitation = statistics.won as f64 / statistics.played as f64;
#[allow(clippy::cast_precision_loss)]
let exploration = f64::consts::SQRT_2
* f64::sqrt(
f64::ln(self.statistic.played as f64) / statistics.played as f64,
);
OrderedFloat(exploitation + exploration)
})?;
Some((token, [selected]))
})
.collect::<Vec<_>>();
let (_, board) = board
.simulate_with_moves(&Instruments, actions.iter().copied())
.next()
.unwrap();
let mut map_actions = [None; 4];
for (i, action) in map_actions.iter_mut().enumerate() {
*action = actions
.iter()
.find(|(snake_id, _)| snake_id.as_usize() == i)
.and_then(|(snake_id, moves)| {
Some((*moves.first()?, board.get_length(snake_id)))
});
}
let winner = self
.childs
.entry(map_actions)
.or_default()
.monte_carlo_step(&board);
// update child statistics
for (token, action) in &actions {
let entry = self
.child_statistics
.entry(*token)
.or_default()
.entry(action[0])
.or_default();
entry.played += 1;
if Some(*token) == winner {
entry.won += 1;
}
}
winner
};
self.statistic.played += 1;
if let Some(token) = winner {
self.statistic
.won
.entry(token)
.and_modify(|won| *won += 1)
.or_insert(1);
}
winner
}
/// Performs one monte carlo simulation step for a solo game
///
/// Returns the lengths before death
fn monte_carlo_solo_step(&mut self, board: &CellBoard4Snakes11x11) -> u16 {
let stop_condition = |board: &CellBoard4Snakes11x11| board.alive_snake_count() == 0;
let winner = if self.statistic.played == 0 {
// We didn't simulate a game for this node yet. Do that
let mut board = *board;
while !stop_condition(&board) {
let moves =
board
.reasonable_moves_for_each_snake()
.filter_map(|(snake_id, moves)| {
Some((snake_id, [*moves.choose(&mut thread_rng())?]))
});
let Some((_, new_board)) = board.simulate_with_moves(&Instruments, moves).next()
else {
break;
};
if stop_condition(&new_board) {
break;
}
board = new_board;
}
let winner = board.get_length(board.you_id());
winner
} else {
// select a node to simulate
let possible_actions = board.reasonable_moves_for_each_snake();
let actions = possible_actions
.filter_map(|(token, actions)| {
let statistics = self.child_statistics.entry(token).or_default();
let selected = actions.iter().copied().max_by_key(|direction| {
let statistics = statistics.entry(*direction).or_default();
if statistics.played == 0 {
return OrderedFloat(f64::INFINITY);
}
#[allow(clippy::cast_precision_loss)]
let exploitation = statistics.won as f64 / statistics.played as f64;
#[allow(clippy::cast_precision_loss)]
let exploration = f64::consts::SQRT_2
* f64::sqrt(
f64::ln(self.statistic.played as f64) / statistics.played as f64,
)
* 11.0
* 11.0;
OrderedFloat(exploitation + exploration)
})?;
Some((token, [selected]))
})
.collect::<Vec<_>>();
let (_, new_board) = board
.simulate_with_moves(&Instruments, actions.iter().copied())
.next()
.unwrap();
let mut map_actions = [None; 4];
for (i, action) in map_actions.iter_mut().enumerate() {
*action = actions
.iter()
.find(|(snake_id, _)| snake_id.as_usize() == i)
.and_then(|(snake_id, moves)| {
Some((*moves.first()?, new_board.get_length(snake_id)))
});
}
let winner = if stop_condition(&new_board) {
board.get_length(board.you_id())
} else {
self.childs
.entry(map_actions)
.or_default()
.monte_carlo_solo_step(&new_board)
};
// update child statistics
let entry = self
.child_statistics
.entry(*new_board.you_id())
.or_default()
.entry(
actions
.iter()
.find(|(snake_id, _)| snake_id == new_board.you_id())
.map(|(_, action)| action[0])
.unwrap(),
)
.or_default();
entry.played += 1;
entry.won += usize::from(winner);
winner
};
self.statistic.played += 1;
self.statistic
.won
.entry(*board.you_id())
.and_modify(|won| *won += usize::from(winner))
.or_insert_with(|| usize::from(winner));
winner
}
}

View File

@ -1,109 +1,313 @@
#![allow(clippy::needless_pass_by_value)] use std::env;
use std::{env, sync::Arc, time::Instant};
use battlesnake::{ use axum::{
logic::{self, GameState}, extract::Json,
Response, response,
routing::{get, post},
Router,
}; };
use battlesnake_game_types::{types::Move, wire_representation::Game}; use battlesnake::types::{
use dashmap::DashMap; simulation::Board,
use log::{error, info}; wire::{Request, Response},
use rocket::{ Direction,
fairing::AdHoc, get, http::Status, launch, post, routes, serde::json::Json, tokio::task, State,
}; };
use serde_json::Value; use float_ord::FloatOrd;
use log::{debug, error, info, trace, warn};
type States = Arc<DashMap<(String, String), GameState>>; use rand::prelude::*;
use serde::Serialize;
#[get("/")] use tokio::{
fn handle_index() -> Json<Value> { net::TcpListener,
Json(logic::info()) time::{Duration, Instant},
}
#[post("/start", format = "json", data = "<game>")]
fn handle_start(state: &State<States>, game: Json<Game>) -> Status {
if state
.insert(
(game.game.id.clone(), game.you.id.clone()),
logic::start(&game),
)
.is_some()
{
error!("re-started game");
}
Status::Ok
}
#[post("/move", format = "json", data = "<game>")]
async fn handle_move(state: &State<States>, game: Json<Game>) -> Json<Response> {
let start = Instant::now();
let state = (*state).clone();
let action = task::spawn_blocking(move || {
let mut game_state = state.get_mut(&(game.game.id.clone(), game.you.id.clone()));
while game_state.is_none() {
error!("move request without previous start");
if state
.insert(
(game.game.id.clone(), game.you.id.clone()),
logic::start(&game),
)
.is_some()
{
error!("re-started game");
}
game_state = state.get_mut(&(game.game.id.clone(), game.you.id.clone()));
}
let Some(mut game_state) = game_state else {
std::mem::drop(game_state);
unreachable!()
}; };
logic::get_move(game.0, &mut game_state, &start)
})
.await
.unwrap_or(Move::Up);
Json(Response::new(action))
}
#[post("/end", format = "json", data = "<game>")]
fn handle_end(state: &State<States>, game: Json<Game>) -> Status {
if let Some((_key, game_state)) = state.remove(&(game.game.id.clone(), game.you.id.clone())) {
logic::end(&game, game_state);
} else {
error!("ended game without state");
}
Status::Ok
}
#[launch]
fn rocket() -> _ {
// Lots of web hosting services expect you to bind to the port specified by the `PORT`
// environment variable. However, Rocket looks at the `ROCKET_PORT` environment variable.
// If we find a value for `PORT`, we set `ROCKET_PORT` to that value.
if let Ok(port) = env::var("PORT") {
env::set_var("ROCKET_PORT", &port);
}
// We default to 'info' level logging. But if the `RUST_LOG` environment variable is set,
// we keep that value instead.
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info");
}
#[tokio::main]
async fn main() {
env_logger::init(); env_logger::init();
info!("Starting Battlesnake Server..."); debug!("Creating routes");
let app = Router::new()
.route("/", get(info))
.route("/start", post(start))
.route("/move", post(get_move))
.route("/end", post(end));
rocket::build() debug!("Creating listener");
.attach(AdHoc::on_response("Server ID Middleware", |_, res| { let port = env::var("PORT").unwrap_or_else(|_| "8000".into());
Box::pin(async move { let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();
res.set_raw_header("Server", "battlesnake/github/starter-snake-rust"); debug!("Starting server");
}) axum::serve(listener, app).await.unwrap();
})) }
.mount(
"/", async fn info() -> response::Json<Info> {
routes![handle_index, handle_start, handle_move, handle_end], info!("got info request");
) response::Json(Info {
.manage(States::new(DashMap::new())) apiversion: "1",
author: "der-informatiker",
color: "#00FFEE",
head: "smart-caterpillar",
tail: "mouse",
version: env!("CARGO_PKG_VERSION"),
})
}
#[derive(Debug, Clone, Serialize)]
struct Info {
apiversion: &'static str,
author: &'static str,
color: &'static str,
head: &'static str,
tail: &'static str,
version: &'static str,
}
async fn start(request: Json<Request>) {
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> {
let start = Instant::now();
let board = Board::from(&*request);
let id = 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<_>>();
if actions.len() <= 1 {
info!(
"only one possible action. Fast forwarding {:?}",
actions.first()
);
return response::Json(Response {
direction: actions.first().copied().unwrap_or(Direction::Up),
shout: None,
});
}
info!("valid actions: {:?}", actions);
tokio::task::spawn_blocking(move || {
let base_turns = board.turn();
let end_condition: &dyn Fn(&Board) -> Option<_> = match &*request.game.ruleset.name {
"solo" => &|board| {
if board.num_snakes() == 0
|| board.turn() > base_turns + (u32::from(request.you.length) * 3).min(32)
{
Some(())
} else {
None
}
},
_ => &|board| {
if board.num_snakes() <= 1
|| board.turn() > base_turns + (u32::from(request.you.length) * 3).min(32)
{
Some(())
} else {
None
}
},
};
let start_snakes = u32::try_from(board.num_snakes()).unwrap_or(0);
let score_fn: &dyn Fn(&Board, u8) -> u32 = match &*request.game.ruleset.name {
"solo" => &|board, _| board.turn(),
_ => &|board, id| {
if board.alive(id) {
1 + start_snakes - u32::try_from(board.num_snakes()).unwrap_or(start_snakes)
} else {
0
}
},
};
let mut mcts_managers: Vec<_> = (0..request.board.snakes.len())
.map(|id| MctsManager::new(u8::try_from(id).unwrap()))
.collect();
let c = f32::sqrt(2.0);
while start.elapsed() < Duration::from_millis(250) {
let mut board = board.clone();
while end_condition(&board).is_none() {
let actions: Vec<_> = mcts_managers
.iter_mut()
.filter_map(|mcts_manager| {
mcts_manager
.next_action(&board, c)
.map(|action| (mcts_manager.snake, action))
})
.collect();
board.next_turn(&actions);
if actions.is_empty() {
break;
}
}
board.simulate_random(end_condition);
for mcts_manager in &mut mcts_managers {
let id = mcts_manager.snake;
let score = score_fn(&board, id);
mcts_manager.apply_score(score);
}
}
let my_mcts_manager = &mcts_managers[usize::from(id)];
for action in actions {
let score = my_mcts_manager.base.next[usize::from(action)]
.as_ref()
.map(|info| info.score as f32 / info.played as f32);
if let Some(score) = score {
info!("{action:?} -> {score}");
} else {
info!("{action:?} -> None");
}
}
let action = my_mcts_manager
.base
.next
.iter()
.enumerate()
.filter_map(|(index, info)| {
info.as_ref().map(|info| {
(
match index {
0 => Direction::Up,
1 => Direction::Down,
2 => Direction::Left,
3 => Direction::Right,
_ => unreachable!(),
},
info,
)
})
})
.max_by_key(|(_, info)| FloatOrd(info.score as f32 / info.played as f32))
.map(|(action, _)| action);
if let Some(action) = action {
info!(
"found action {action:?} after {} simulations.",
my_mcts_manager.base.played
);
} else {
warn!("unable to find a valid action");
}
info!("chose {action:?}");
response::Json(Response {
direction: action.unwrap_or(Direction::Up),
shout: None,
})
})
.await
.unwrap()
}
async fn end(request: Json<Request>) {
let board = Board::from(&*request);
info!("got end request: {board}");
}
#[derive(Debug)]
struct ActionInfo {
score: u32,
played: u32,
next: Box<[Option<ActionInfo>; 4]>,
}
impl ActionInfo {
fn new() -> Self {
Self {
score: 0,
played: 0,
next: Box::new([None, None, None, None]),
}
}
fn uct(&self, c: f32) -> [Option<f32>; 4] {
let mut ucts = [None; 4];
for (action, uct) in self.next.iter().zip(ucts.iter_mut()) {
if let Some(action) = action {
let exploitation = action.score as f32 / action.played as f32;
let exploration = f32::sqrt(f32::ln(self.played as f32) / action.played as f32);
uct.replace(c.mul_add(exploration, exploitation));
}
}
ucts
}
}
#[derive(Debug)]
struct MctsManager {
base: ActionInfo,
actions: Vec<Direction>,
expanded: bool,
snake: u8,
}
impl MctsManager {
fn new(snake: u8) -> Self {
Self {
base: ActionInfo::new(),
actions: Vec::new(),
expanded: false,
snake,
}
}
fn apply_score(&mut self, score: u32) {
self.base.played += 1;
self.base.score += score;
let mut current = &mut self.base;
for action in &self.actions {
let Some(ref mut new_current) = &mut current.next[usize::from(*action)] else {
error!("got action without actioninfo");
break;
};
current = new_current;
current.played += 1;
current.score += score;
}
self.actions.clear();
self.expanded = false;
}
fn next_action(&mut self, board: &Board, c: f32) -> Option<Direction> {
if self.expanded {
return None;
}
let mut current = &mut self.base;
for action in &self.actions {
let Some(ref mut new_current) = &mut current.next[usize::from(*action)] else {
error!("got action without actioninfo");
return None;
};
current = new_current;
}
let ucts = current.uct(c);
let valid_actions = board.valid_actions(self.snake);
let ucts: Vec<_> = valid_actions
.map(|action| (action, ucts[usize::from(action)]))
.collect();
trace!("got actions: {ucts:?}");
if ucts.iter().any(|(_, uct)| uct.is_none()) {
let action = ucts
.iter()
.filter(|(_, uct)| uct.is_none())
.choose(&mut thread_rng())?
.0;
self.expanded = true;
current.next[usize::from(action)].replace(ActionInfo::new());
self.actions.push(action);
return Some(action);
}
let action = ucts
.iter()
.max_by_key(|(_, uct)| FloatOrd(uct.unwrap_or(f32::NEG_INFINITY)))
.map(|(action, _)| *action);
if let Some(action) = action {
self.actions.push(action);
}
action
}
} }

View File

@ -0,0 +1,58 @@
use enum_iterator::Sequence;
use serde::{Deserialize, Serialize};
pub mod simulation;
pub mod wire;
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
pub struct Coord {
pub x: u8,
pub y: u8,
}
impl Coord {
#[must_use]
pub fn apply(self, direction: Direction) -> Option<Self> {
match direction {
Direction::Up => self.y.checked_add(1).map(|y| Self { y, x: self.x }),
Direction::Down => self.y.checked_sub(1).map(|y| Self { y, x: self.x }),
Direction::Left => self.x.checked_sub(1).map(|x| Self { x, y: self.y }),
Direction::Right => self.x.checked_add(1).map(|x| Self { x, y: self.y }),
}
}
#[must_use]
pub const fn wrapping_apply(mut self, direction: Direction) -> Self {
match direction {
Direction::Up => self.y = self.y.wrapping_add(1),
Direction::Down => self.y = self.y.wrapping_sub(1),
Direction::Left => self.x = self.x.wrapping_sub(1),
Direction::Right => self.x = self.x.wrapping_add(1),
}
self
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Sequence)]
#[serde(rename_all = "lowercase")]
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,
}
impl From<Direction> for usize {
fn from(value: Direction) -> Self {
match value {
Direction::Up => 0,
Direction::Down => 1,
Direction::Left => 2,
Direction::Right => 3,
}
}
}

View File

@ -0,0 +1,519 @@
use std::{
collections::VecDeque,
fmt::Display,
num::NonZeroUsize,
ops::{Deref, DerefMut},
sync::Arc,
};
use bitvec::prelude::*;
use log::{error, warn};
use rand::prelude::*;
use super::{wire::Request, Coord, Direction};
#[derive(Debug, PartialEq, Eq, Clone)]
enum SmallBitBox {
Stack {
storage: BitArr!(for Self::STACK_BITS),
len: NonZeroUsize,
},
Heap(BitBox),
}
impl SmallBitBox {
const STACK_BITS: usize = usize::BITS as usize * 3;
fn new(initial: bool, len: usize) -> Self {
if let len @ 1..Self::STACK_BITS = len {
let Some(len) = NonZeroUsize::new(len) else {
unreachable!()
};
let mut storage = BitArray::ZERO;
storage.fill(initial);
Self::Stack { storage, len }
} else {
Self::Heap(bitbox![u8::from(initial); len])
}
}
}
impl Deref for SmallBitBox {
type Target = BitSlice;
fn deref(&self) -> &Self::Target {
match self {
Self::Stack { storage, len } => &storage[..len.get()],
Self::Heap(bit_box) => bit_box,
}
}
}
impl DerefMut for SmallBitBox {
fn deref_mut(&mut self) -> &mut Self::Target {
match self {
Self::Stack { storage, len } => &mut storage[..len.get()],
Self::Heap(bit_box) => bit_box,
}
}
}
#[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>)]>,
}
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 id_map = value
.board
.snakes
.iter()
.enumerate()
.filter_map(|(i, snake)| {
u8::try_from(i)
.inspect_err(|e| warn!("unable to convert id to u8: {e}"))
.ok()
.map(|i| (i, snake.id.clone()))
})
.collect::<Vec<_>>();
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(),
};
for &food in &value.board.food {
let index = board.coord_to_linear(food);
board.food.set(index, true);
}
for &hazard in &value.board.hazards {
let index = board.coord_to_linear(hazard);
board.hazard.set(index, true);
}
for (id, snake) in value.board.snakes.iter().enumerate() {
for &tile in snake
.body
.iter()
.take(snake.body.len() - usize::from(!board.constrictor))
{
let index = board.coord_to_linear(tile);
board.free.set(index, false);
}
let snake = Snake {
body: snake.body.iter().rev().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 @ {}",
if self.constrictor { "constrictor" } else { "" },
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 = 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 {
#[must_use]
pub fn get_id(&self, str_id: &str) -> Option<u8> {
self.id_map
.iter()
.find(|(_, str_id2)| *str_id == **str_id2)
.map(|(id, _)| *id)
}
#[must_use]
pub const fn turn(&self) -> u32 {
self.turn
}
#[must_use]
pub fn num_snakes(&self) -> usize {
self.snakes.len()
}
#[must_use]
pub fn alive(&self, id: u8) -> bool {
self.id_to_index(id).is_some()
}
#[must_use]
pub fn is_food(&self, tile: Coord) -> bool {
let index = self.coord_to_linear(tile);
self.food[index]
}
#[must_use]
pub fn is_hazard(&self, tile: Coord) -> bool {
let index = self.coord_to_linear(tile);
self.hazard[index]
}
#[must_use]
pub fn is_free(&self, tile: Coord) -> bool {
if !(tile.x < self.width && tile.y < self.height) {
return false;
}
let index = self.coord_to_linear(tile);
self.free[index]
}
pub fn valid_actions(&self, id: u8) -> impl Iterator<Item = Direction> + use<'_> {
let index = self.id_to_index(id);
index
.into_iter()
.flat_map(|index| self.valid_actions_index(index))
}
#[must_use]
pub fn random_action(&self, id: u8) -> Direction {
let Some(index) = self.id_to_index(id) else {
return Direction::Up;
};
self.valid_actions_index(index)
.choose(&mut thread_rng())
.unwrap_or(Direction::Up)
}
pub fn random_actions(&self) -> impl Iterator<Item = (u8, Direction)> + use<'_> {
(0..self.snakes.len()).map(|index| {
(
self.snakes[index].id,
self.valid_actions_index(index)
.choose(&mut thread_rng())
.unwrap_or(Direction::Up),
)
})
}
pub fn simulate_random<T>(&mut self, stop: impl Fn(&Self) -> Option<T>) -> T {
loop {
if let Some(score) = stop(self) {
break score;
}
self.next_turn(&[]);
}
}
pub fn next_turn(&mut self, actions: &[(u8, Direction)]) {
self.move_standard(actions);
self.starvation_standard();
self.hazard_damage_standard();
self.feed_snakes_standard();
self.eliminate_snake_standard();
self.update_free_map();
self.spawn_food();
self.turn += 1;
}
fn id_to_index(&self, id: u8) -> Option<usize> {
self.snakes.binary_search_by_key(&id, |snake| snake.id).ok()
}
fn valid_actions_index(&self, index: usize) -> impl Iterator<Item = Direction> + use<'_> {
let head = self.snakes[index].head();
enum_iterator::all::<Direction>()
.map(move |direction| (direction, head.wrapping_apply(direction)))
.filter(|(_, tile)| self.is_in_bounds(*tile))
.filter(|(_, tile)| self.is_free(*tile))
.map(|(direction, _)| direction)
}
fn move_standard(&mut self, actions: &[(u8, Direction)]) {
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(&mut thread_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 {
let tail = snake.tail();
let pre_tail = snake.body[snake.body.len() - 2];
if tail != pre_tail {
let tail_index = self.coord_to_linear(tail);
self.free.set(tail_index, true);
}
}
// block heads
for snake in &self.snakes {
let head = snake.head();
let head_index = self.coord_to_linear(head);
self.free.set(head_index, false);
}
}
fn spawn_food(&mut self) {
let num_food = self.food.count_ones();
let needed_food = if num_food < usize::from(self.min_food) {
usize::from(self.min_food) - num_food
} else {
usize::from(
self.food_spawn_chance > 0
&& thread_rng().gen_range(0..100) < self.food_spawn_chance,
)
};
if needed_food == 0 {
return;
}
let food_spots = self
.free
.iter()
.enumerate()
.filter_map(|(i, free)| free.then_some(i))
.filter(|i| {
self.snakes
.iter()
.all(|snake| self.coord_to_linear(snake.tail()) != *i)
})
.filter(|i| {
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))
})
.all(|action| *i != self.coord_to_linear(action))
})
.filter(|i| !self.food[*i])
.choose_multiple(&mut thread_rng(), needed_food);
for index in food_spots {
self.food.set(index, true);
}
}
fn coord_to_linear(&self, coord: Coord) -> usize {
usize::from(coord.x) + usize::from(coord.y) * usize::from(self.width)
}
const fn is_in_bounds(&self, coord: Coord) -> bool {
coord.x < self.width && coord.y < self.height
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
struct Snake {
id: u8,
health: u8,
body: VecDeque<Coord>,
}
impl Snake {
pub fn head(&self) -> Coord {
self.body.back().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 }
})
}
pub fn advance(&mut self, head: Coord) {
self.body.push_back(head);
self.body.pop_front();
}
pub fn feed(&mut self) {
if let Some(tail) = self.body.front() {
self.body.push_front(*tail);
self.health = 100;
}
}
}

View File

@ -0,0 +1,121 @@
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use super::{Coord, Direction};
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
pub struct Request {
/// Game object describing the game being played.
pub game: Game,
/// Turn number for this move.
pub turn: u32,
/// Board object describing the initial state of the game board.
pub board: Board,
/// Battlesnake Object describing your Battlesnake.
pub you: Battlesnake,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
pub struct Game {
/// A unique identifier for this Game.
pub id: Arc<str>,
/// Information about the ruleset being used to run this Game.
pub ruleset: Ruleset,
/// The name of the map being played on.
pub map: Arc<str>,
/// How much time your snake has to respond to requests for this Game.
pub timeout: u16,
/// The source of this Game.
/// One of:
/// - tournament
/// - league
/// - arena
/// - challenge
/// - custom
pub source: Arc<str>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
pub struct Ruleset {
/// Name of the ruleset being used to run this game.
pub name: Arc<str>,
/// The release version of the Rules module used in this game.
pub version: Arc<str>,
/// A collection of specific settings being used by the current game that control how the rules
/// are applied.
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.
pub food_spawn_chance: u8,
/// Minimum food to keep on the board every turn.
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.
pub hazard_damage_per_turn: u8,
/// Settings for the royale game mode
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).
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.
pub height: u8,
/// The number of rows in the x-axis of the game board.
pub width: u8,
/// Array of coordinates representing food locations on the game board.
pub food: Vec<Coord>,
/// Array of coordinates representing hazardous locations on the game board.
pub hazards: Vec<Coord>,
/// Array of Battlesnake objects representing all Battlesnakes remaining on the game board
/// (including yourself if you haven't been eliminated).
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.
pub id: Arc<str>,
/// Name given to this Battlesnake by its author
pub name: Arc<str>,
/// Health value of this Battlesnake, between 0 and 100
pub health: u8,
/// Array of coordinates representing the Battlesnake's location on the game board.
/// This array is ordered from head to tail.
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
pub latency: Arc<str>,
/// 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: u16,
/// Message shouted by this Battlesnake on the previous turn
pub shout: Option<Arc<str>>,
/// The squad that the Battlesnake belongs to.
/// Used to identify squad members in Squad Mode games.
pub squad: Arc<str>,
}
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
pub struct Response {
/// Your Battlesnake's move for this turn.
#[serde(rename = "move")]
pub direction: Direction,
/// An optional message sent to all other Battlesnakes on the next turn.
/// Must be 256 characters or less.
pub shout: Option<String>,
}

View File

@ -366,7 +366,7 @@ fn run_local_docker(port: u16) -> Result<Child, DynError> {
"--env", "--env",
"RUST_LOG=error", "RUST_LOG=error",
"--env", "--env",
format!("ROCKET_PORT={}", port).as_str(), format!("PORT={}", port).as_str(),
"--network=host", "--network=host",
"--rm", "--rm",
"local_snake", "local_snake",
@ -396,7 +396,7 @@ fn run_production(port: u16) -> Result<Child, DynError> {
"--env", "--env",
"RUST_LOG=error", "RUST_LOG=error",
"--env", "--env",
format!("ROCKET_PORT={}", port).as_str(), format!("PORT={}", port).as_str(),
"--network=host", "--network=host",
"--rm", "--rm",
"docker.mkaenner.de/snake:latest", "docker.mkaenner.de/snake:latest",