Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
1410602c6c | |||
5e81a7952f | |||
9c76df1f69 | |||
d91d0e0a82 | |||
f9e34d119a | |||
d9ee6ae2f7 | |||
f3eba2ba75 | |||
932023451a |
17
.github/workflows/build_docker.yaml
vendored
Normal file
17
.github/workflows/build_docker.yaml
vendored
Normal 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
|
1471
Cargo.lock
generated
1471
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
@ -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,16 @@ 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 = ["net", "macros", "rt-multi-thread"] }
|
||||||
serde_json = "1.0.59"
|
axum = { version = "0.8", features = ["http2", "multipart", "ws"] }
|
||||||
log = "0.4.0"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
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"
|
|
||||||
dashmap = "6.1.0"
|
|
||||||
nalgebra = "0.33.2"
|
|
||||||
|
|
||||||
battlesnake-game-types = "0.17.0"
|
|
||||||
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = "fat"
|
|
||||||
codegen-units = 1
|
|
||||||
panic = "abort"
|
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
[default]
|
|
||||||
address = "0.0.0.0"
|
|
||||||
port = 8000
|
|
||||||
keep_alive = 0
|
|
@ -1,24 +0,0 @@
|
|||||||
use battlesnake_game_types::types::Move;
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +1,90 @@
|
|||||||
#![allow(clippy::needless_pass_by_value)]
|
mod types;
|
||||||
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 log::{debug, info, warn};
|
||||||
use dashmap::DashMap;
|
use rand::prelude::*;
|
||||||
use log::{error, info};
|
use serde::Serialize;
|
||||||
use rocket::{
|
use tokio::{net::TcpListener, time::Instant};
|
||||||
fairing::AdHoc, get, http::Status, launch, post, routes, serde::json::Json, tokio::task, State,
|
use types::{
|
||||||
|
simulation::Board,
|
||||||
|
wire::{Request, Response},
|
||||||
|
Direction,
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
|
||||||
|
|
||||||
type States = Arc<DashMap<(String, String), GameState>>;
|
|
||||||
|
|
||||||
#[get("/")]
|
|
||||||
fn handle_index() -> Json<Value> {
|
|
||||||
Json(logic::info())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 listener = TcpListener::bind("0.0.0.0:8000").await.unwrap();
|
||||||
Box::pin(async move {
|
debug!("Starting server");
|
||||||
res.set_raw_header("Server", "battlesnake/github/starter-snake-rust");
|
axum::serve(listener, app).await.unwrap();
|
||||||
})
|
}
|
||||||
}))
|
|
||||||
.mount(
|
async fn info() -> response::Json<Info> {
|
||||||
"/",
|
info!("got info request");
|
||||||
routes![handle_index, handle_start, handle_move, handle_end],
|
response::Json(Info {
|
||||||
)
|
apiversion: "1",
|
||||||
.manage(States::new(DashMap::new()))
|
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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_move(request: Json<Request>) -> response::Json<Response> {
|
||||||
|
let board = Board::from(&*request);
|
||||||
|
info!("got move request: {board}");
|
||||||
|
let actions = board.valid_actions(0).collect::<Vec<_>>();
|
||||||
|
info!("valid actions: {:?}", actions);
|
||||||
|
let start = Instant::now();
|
||||||
|
for _ in 0..100 {
|
||||||
|
let mut board = board.clone();
|
||||||
|
let score = board.simulate_random(|board| (board.num_snakes() <= 1).then_some(1));
|
||||||
|
std::hint::black_box(score);
|
||||||
|
}
|
||||||
|
let elapsed = start.elapsed();
|
||||||
|
debug!("simulated 100 random games in {elapsed:?}");
|
||||||
|
let action = actions.choose(&mut thread_rng()).copied();
|
||||||
|
if action.is_none() {
|
||||||
|
warn!("unable to find a valid action");
|
||||||
|
}
|
||||||
|
info!("chose {action:?}");
|
||||||
|
response::Json(Response {
|
||||||
|
direction: action.unwrap_or(Direction::Up),
|
||||||
|
shout: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn end(request: Json<Request>) {
|
||||||
|
let board = Board::from(&*request);
|
||||||
|
info!("got end request: {board}");
|
||||||
}
|
}
|
||||||
|
45
battlesnake/src/types/mod.rs
Normal file
45
battlesnake/src/types/mod.rs
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
use enum_iterator::Sequence;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod simulation;
|
||||||
|
pub mod wire;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
|
||||||
|
pub struct Coord {
|
||||||
|
x: u8,
|
||||||
|
y: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coord {
|
||||||
|
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 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub 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,
|
||||||
|
}
|
377
battlesnake/src/types/simulation.rs
Normal file
377
battlesnake/src/types/simulation.rs
Normal file
@ -0,0 +1,377 @@
|
|||||||
|
use std::{collections::VecDeque, fmt::Display};
|
||||||
|
|
||||||
|
use bitvec::prelude::*;
|
||||||
|
use log::{error, warn};
|
||||||
|
use rand::prelude::*;
|
||||||
|
|
||||||
|
use super::{wire::Request, Coord, Direction};
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
constrictor: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
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()),
|
||||||
|
constrictor: value.game.ruleset.name == "constrictor",
|
||||||
|
};
|
||||||
|
|
||||||
|
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().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 {
|
||||||
|
pub fn num_snakes(&self) -> usize {
|
||||||
|
self.snakes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_food(&self, tile: Coord) -> bool {
|
||||||
|
let index = self.coord_to_linear(tile);
|
||||||
|
self.food[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_hazard(&self, tile: Coord) -> bool {
|
||||||
|
let index = self.coord_to_linear(tile);
|
||||||
|
self.hazard[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
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 head = self
|
||||||
|
.snakes
|
||||||
|
.binary_search_by_key(&id, |snake| snake.id)
|
||||||
|
.ok()
|
||||||
|
.map(|index| self.snakes[index].head());
|
||||||
|
if head.is_none() {
|
||||||
|
warn!(
|
||||||
|
"Asked for an action for a snake that doesn't exist: {id} not in {:?}",
|
||||||
|
self.snakes
|
||||||
|
);
|
||||||
|
}
|
||||||
|
enum_iterator::all::<Direction>()
|
||||||
|
.filter_map(move |direction| {
|
||||||
|
head.and_then(|head| head.apply(direction))
|
||||||
|
.map(|tile| (direction, tile))
|
||||||
|
})
|
||||||
|
.filter(|(_, tile)| tile.x < self.width && tile.y < self.height)
|
||||||
|
.filter(|(_, tile)| self.is_free(*tile))
|
||||||
|
.map(|(direction, _)| direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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(snake.id)
|
||||||
|
.choose(&mut thread_rng())
|
||||||
|
.unwrap_or(Direction::Up)
|
||||||
|
},
|
||||||
|
|(_, action)| *action,
|
||||||
|
);
|
||||||
|
let new_head = snake.head().wrapping_apply(action);
|
||||||
|
let snake = &mut self.snakes[i];
|
||||||
|
snake.body.push_front(new_head);
|
||||||
|
snake.body.pop_back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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);
|
||||||
|
let snake = &mut self.snakes[i];
|
||||||
|
snake.health = 100;
|
||||||
|
let tail = snake.tail();
|
||||||
|
snake.body.push_back(tail);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let food_spots = self
|
||||||
|
.free
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, free)| free.then_some(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.front().copied().unwrap_or_else(|| {
|
||||||
|
error!("Snake without a head: {self:?}");
|
||||||
|
Coord { x: 0, y: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tail(&self) -> Coord {
|
||||||
|
self.body.back().copied().unwrap_or_else(|| {
|
||||||
|
error!("Snake without a tail: {self:?}");
|
||||||
|
Coord { x: 0, y: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
119
battlesnake/src/types/wire.rs
Normal file
119
battlesnake/src/types/wire.rs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
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: 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: u16,
|
||||||
|
/// The source of this Game.
|
||||||
|
/// One of:
|
||||||
|
/// - tournament
|
||||||
|
/// - league
|
||||||
|
/// - arena
|
||||||
|
/// - challenge
|
||||||
|
/// - custom
|
||||||
|
pub source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
||||||
|
pub struct Ruleset {
|
||||||
|
/// Name of the ruleset being used to run this game.
|
||||||
|
pub name: String,
|
||||||
|
/// The release version of the 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: 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: String,
|
||||||
|
/// Name given to this Battlesnake by its author
|
||||||
|
pub name: String,
|
||||||
|
/// 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: String,
|
||||||
|
/// 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: String,
|
||||||
|
/// The squad that the Battlesnake belongs to.
|
||||||
|
/// Used to identify squad members in Squad Mode games.
|
||||||
|
pub squad: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user