#![allow(clippy::needless_pass_by_value)] use enum_iterator::Sequence; use log::info; use rocket::fairing::AdHoc; use rocket::http::Status; use rocket::serde::{json::Json, Deserialize}; use rocket::{get, launch, post, routes}; use serde::Serialize; use serde_json::Value; use std::env; mod logic; mod simulation; const MAX_HEALTH: i32 = 100; // API and Response Objects // See https://docs.battlesnake.com/api #[derive( Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Deserialize, Serialize, Sequence, Default, )] #[serde(rename_all = "lowercase")] pub enum Direction { /// Move left (-x) #[default] Left, /// Move up (+y) Up, /// Move right (+x) Right, /// Move down (-y) Down, } #[derive(Debug, Deserialize, Serialize)] pub struct Action { /// In which direction the snake should move r#move: Direction, /// Say something to the other snakes #[serde(default, skip_serializing_if = "is_default")] shout: Option, } fn is_default(value: &T) -> bool { *value == T::default() } #[derive(Deserialize, Serialize, Debug)] 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: u32, /// The source of this game. /// /// One of: /// - "tournament" /// - "league" /// - "arena" /// - "challenge" /// - "custom" /// /// The values may change. source: String, } #[derive(Debug, Deserialize, Serialize)] pub struct Ruleset { /// Name of the ruleset being used to run this game. name: String, /// The release version of the [Rules](https://github.com/BattlesnakeOfficial/rules) module used in this game. version: String, /// A collection of specific settings being used by the current game that control how the rules /// are applied. settings: RulesetSettings, } #[derive(Debug, Deserialize, Serialize)] pub struct RulesetSettings { /// Percentage chance of spawning a new food every round. #[serde(rename = "foodSpawnChance")] food_spawn_chance: u8, /// Minimum food to keep on the board every turn. #[serde(rename = "minimumFood")] minimum_food: u8, /// Health damage a snake will take when ending its turn in a hazard. This stacks on top of the /// regular 1 damage a snake takes per turn. #[serde(rename = "hazardDamagePerTurn")] hazard_damage_per_turn: u8, /// rules for the royale mode royale: RulesetRoyale, /// rules for the squad mode squad: RulesetSquad, } #[derive(Debug, Deserialize, Serialize)] pub struct RulesetRoyale { /// The number of turns between generating new hazards (shrinking the safe board space). #[serde(rename = "shrinkEveryNTurns")] shrink_every_n_turns: i32, } #[allow(clippy::struct_excessive_bools)] #[derive(Debug, Deserialize, Serialize)] pub struct RulesetSquad { /// Allow members of the same squad to move over each other without dying. #[serde(rename = "allowBodyCollisions")] allow_body_collisions: bool, /// All squad members are eliminated when one is eliminated. #[serde(rename = "sharedElimination")] shared_elimination: bool, /// All squad members share health. #[serde(rename = "sharedHealth")] shared_health: bool, /// All squad members share length. #[serde(rename = "sharedLength")] shared_length: bool, } #[derive(Deserialize, Serialize, Debug, Clone)] pub struct Board { /// The number of rows in the y-axis of the game board. height: i32, /// The number of columns in the x-axis of the game board. width: i32, /// Array of coordinates representing food locations on the game board. food: Vec, /// Array of Battlesnakes representing all Battlesnakes remaining on the game board (including /// yourself if you haven't been eliminated). snakes: Vec, /// Array of coordinates representing hazardous locations on the game board. hazards: Vec, } #[derive(Deserialize, Serialize, Debug, Clone)] 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: i32, /// Array of coordinates representing this Battlesnake's location on the game board. This array /// is ordered from head to tail body: Vec, /// Coordinates for this Battlesnake's head. Equivalent to the first element of the body array. head: Coord, /// Length of this Battlesnake from head to tail. Equivalent to the length of the body array. length: i32, /// The previous response time of this Battlesnake, in milliseconds. If the Battlesnake timed /// out and failed to respond, the game timeout will be returned. latency: String, /// Message shouted by this Battlesnake on the previous turn. shout: Option, /// The squad that the Battlesnake belongs to. Used to identify squad members in Squad Mode /// games. squad: String, // /// The collection of customizations that control how this Battlesnake is displayed. // customizations: {color, head, tail} } #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Deserialize, Serialize)] pub struct Coord { x: i32, y: i32, } impl Coord { const fn move_to(mut self, direction: Direction) -> Self { match direction { Direction::Left => self.x -= 1, Direction::Up => self.y += 1, Direction::Right => self.x += 1, Direction::Down => self.y -= 1, } self } } #[derive(Deserialize, Serialize, Debug)] pub struct GameState { game: Game, turn: i32, board: Board, you: Battlesnake, } #[get("/")] fn handle_index() -> Json { Json(logic::info()) } #[post("/start", format = "json", data = "")] fn handle_start(start_req: Json) -> Status { logic::start( &start_req.game, start_req.turn, &start_req.board, &start_req.you, ); Status::Ok } #[post("/move", format = "json", data = "")] fn handle_move(move_req: Json) -> Option> { let response = logic::get_move( &move_req.game, move_req.turn, &move_req.board, &move_req.you, )?; Some(Json(response)) } #[post("/end", format = "json", data = "")] fn handle_end(end_req: Json) -> Status { logic::end(&end_req.game, end_req.turn, &end_req.board, &end_req.you); 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"); } env_logger::init(); info!("Starting Battlesnake Server..."); 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], ) }