accept battlesnake requests
This commit is contained in:
		
							
								
								
									
										1473
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1473
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,7 +1,7 @@ | ||||
| [package] | ||||
| authors = ["Max Känner"] | ||||
| name = "battlesnake" | ||||
| version = "1.0.0" | ||||
| version = "2.0.0" | ||||
| edition = "2021" | ||||
|  | ||||
| readme = "README.md" | ||||
| @@ -16,22 +16,8 @@ pedantic = "warn" | ||||
| nursery = "warn" | ||||
|  | ||||
| [dependencies] | ||||
| rocket = { version = "0.5.0", features = ["json"] } | ||||
| serde = { version = "1.0.117", features = ["derive"] } | ||||
| serde_json = "1.0.59" | ||||
| log = "0.4.0" | ||||
| env_logger = "0.11.5" | ||||
| rand = "0.8.4" | ||||
| enum-iterator = "2.1" | ||||
| iter_tools = "0.24" | ||||
| 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" | ||||
| tokio = { version = "1.43", features = ["net", "macros", "rt-multi-thread"] } | ||||
| axum = { version = "0.8", features = ["http2", "multipart", "ws"] } | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| log = "0.4" | ||||
| env_logger = "0.11" | ||||
|   | ||||
| @@ -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,67 @@ | ||||
| #![allow(clippy::needless_pass_by_value)] | ||||
| use std::{env, sync::Arc, time::Instant}; | ||||
| mod types; | ||||
|  | ||||
| use battlesnake::{ | ||||
|     logic::{self, GameState}, | ||||
|     Response, | ||||
| use axum::{ | ||||
|     extract::Json, | ||||
|     response, | ||||
|     routing::{get, post}, | ||||
|     Router, | ||||
| }; | ||||
| use battlesnake_game_types::{types::Move, wire_representation::Game}; | ||||
| use dashmap::DashMap; | ||||
| use log::{error, info}; | ||||
| use rocket::{ | ||||
|     fairing::AdHoc, get, http::Status, launch, post, routes, serde::json::Json, tokio::task, State, | ||||
| }; | ||||
| 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"); | ||||
|     } | ||||
| use log::{debug, info}; | ||||
| use serde::Serialize; | ||||
| use tokio::net::TcpListener; | ||||
| use types::wire::{Direction, Request, Response}; | ||||
|  | ||||
| #[tokio::main] | ||||
| async fn main() { | ||||
|     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() | ||||
|         .attach(AdHoc::on_response("Server ID Middleware", |_, res| { | ||||
|             Box::pin(async move { | ||||
|                 res.set_raw_header("Server", "battlesnake/github/starter-snake-rust"); | ||||
|             }) | ||||
|         })) | ||||
|         .mount( | ||||
|             "/", | ||||
|             routes![handle_index, handle_start, handle_move, handle_end], | ||||
|         ) | ||||
|         .manage(States::new(DashMap::new())) | ||||
|     debug!("Creating listener"); | ||||
|     let listener = TcpListener::bind("0.0.0.0:8000").await.unwrap(); | ||||
|     debug!("Starting server"); | ||||
|     axum::serve(listener, app).await.unwrap(); | ||||
| } | ||||
|  | ||||
| async fn info() -> response::Json<Info> { | ||||
|     info!("got info request"); | ||||
|     response::Json(Info { | ||||
|         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>) { | ||||
|     info!("got start request: {request:#?}"); | ||||
| } | ||||
|  | ||||
| async fn get_move(request: Json<Request>) -> response::Json<Response> { | ||||
|     info!("got move request: {request:#?}"); | ||||
|     response::Json(Response { | ||||
|         direction: Direction::Up, | ||||
|         shout: None, | ||||
|     }) | ||||
| } | ||||
|  | ||||
| async fn end(request: Json<Request>) { | ||||
|     info!("got end request: {request:#?}"); | ||||
| } | ||||
|   | ||||
							
								
								
									
										1
									
								
								battlesnake/src/types/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								battlesnake/src/types/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| pub mod wire; | ||||
							
								
								
									
										135
									
								
								battlesnake/src/types/wire.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								battlesnake/src/types/wire.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| pub struct Request { | ||||
|     /// Game object describing the game being played. | ||||
|     game: Game, | ||||
|     /// Turn number for this move. | ||||
|     turn: u32, | ||||
|     /// Board object describing the initial state of the game board. | ||||
|     board: Board, | ||||
|     /// Battlesnake Object describing your Battlesnake. | ||||
|     you: Battlesnake, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| pub 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: u16, | ||||
|     /// The source of this Game. | ||||
|     /// One of: | ||||
|     /// - tournament | ||||
|     /// - league | ||||
|     /// - arena | ||||
|     /// - challenge | ||||
|     /// - custom | ||||
|     source: String, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| pub struct Ruleset { | ||||
|     /// Name of the ruleset being used to run this game. | ||||
|     name: String, | ||||
|     /// The release version of the 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: Settings, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct Settings { | ||||
|     /// Percentage chance of spawning a new food every round. | ||||
|     food_spawn_chance: u8, | ||||
|     /// Minimum food to keep on the board every turn. | ||||
|     minimum_food: u16, | ||||
|     /// Health damage a snake will take when ending its turn in a hazard. This stacks on top of the | ||||
|     /// regular 1 damage a snake takes per turn. | ||||
|     hazard_damage_per_turn: u8, | ||||
|     /// Settings for the royale game mode | ||||
|     royale: RoyaleSettings, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] | ||||
| #[serde(rename_all = "camelCase")] | ||||
| pub struct RoyaleSettings { | ||||
|     /// The number of turns between generating new hazards (shrinking the safe board space). | ||||
|     shrink_every_n_turns: u8, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| pub struct Board { | ||||
|     /// The number of rows in the y-axis of the game board. | ||||
|     height: u16, | ||||
|     /// The number of rows in the x-axis of the game board. | ||||
|     width: u16, | ||||
|     /// Array of coordinates representing food locations on the game board. | ||||
|     food: Vec<Coord>, | ||||
|     /// Array of coordinates representing hazardous locations on the game board. | ||||
|     hazards: Vec<Coord>, | ||||
|     /// Array of Battlesnake objects representing all Battlesnakes remaining on the game board | ||||
|     /// (including yourself if you haven't been eliminated). | ||||
|     snakes: Vec<Battlesnake>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)] | ||||
| pub struct Coord { | ||||
|     x: u16, | ||||
|     y: u16, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Deserialize)] | ||||
| pub 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: u8, | ||||
|     /// Array of coordinates representing the Battlesnake's location on the game board. | ||||
|     /// This array is ordered from head to tail. | ||||
|     body: Vec<Coord>, | ||||
|     /// The previous response time of this Battlesnake, in milliseconds. | ||||
|     /// If the Battlesnake timed out and failed to respond, the game timeout will be returned | ||||
|     latency: String, | ||||
|     /// 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: u32, | ||||
|     /// Message shouted by this Battlesnake on the previous turn | ||||
|     shout: String, | ||||
|     /// The squad that the Battlesnake belongs to. | ||||
|     /// Used to identify squad members in Squad Mode games. | ||||
|     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>, | ||||
| } | ||||
|  | ||||
| #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize)] | ||||
| pub enum Direction { | ||||
|     /// Move in positive y direction | ||||
|     Up, | ||||
|     /// Move in negative y direction | ||||
|     Down, | ||||
|     /// Move in negative x direction | ||||
|     Left, | ||||
|     /// Move in positive x direction | ||||
|     Right, | ||||
| } | ||||
		Reference in New Issue
	
	Block a user