initial commit
This commit is contained in:
		
							
								
								
									
										2
									
								
								.cargo/config.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.cargo/config.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| [alias] | ||||
| xtask = "run --package xtask --" | ||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| target | ||||
							
								
								
									
										1704
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										1704
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										4
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| [workspace] | ||||
| members = ["battlesnake", "xtask"] | ||||
|  | ||||
| resolver = "2" | ||||
							
								
								
									
										8
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | ||||
| FROM rust:1.80 | ||||
|  | ||||
| COPY . /usr/app | ||||
| WORKDIR /usr/app | ||||
|  | ||||
| RUN cargo install --path battlesnake | ||||
|  | ||||
| CMD ["battlesnake"] | ||||
							
								
								
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								LICENSE
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| MIT License | ||||
|  | ||||
| Copyright (c) 2022 Battlesnake Inc. | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
							
								
								
									
										
											BIN
										
									
								
								battlesnake-cli
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								battlesnake-cli
									
									
									
									
									
										Executable file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										19
									
								
								battlesnake/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								battlesnake/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| [package] | ||||
| authors = ["Max Känner"] | ||||
| name = "battlesnake" | ||||
| version = "1.0.0" | ||||
| edition = "2021" | ||||
|  | ||||
| readme = "README.md" | ||||
| keywords = ["battlesnake"] | ||||
| description = """ | ||||
| A simple Battlesnake written in Rust | ||||
| """ | ||||
|  | ||||
| [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" | ||||
							
								
								
									
										49
									
								
								battlesnake/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								battlesnake/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| # Battlesnake Rust Starter Project | ||||
|  | ||||
| An official Battlesnake template written in Rust. Get started at [play.battlesnake.com](https://play.battlesnake.com). | ||||
|  | ||||
|  | ||||
|  | ||||
| This project is a great starting point for anyone wanting to program their first Battlesnake in Rust. It can be run locally or easily deployed to a cloud provider of your choosing. See the [Battlesnake API Docs](https://docs.battlesnake.com/api) for more detail.  | ||||
|  | ||||
| [](https://replit.com/@Battlesnake/starter-snake-rust) | ||||
|  | ||||
| ## Technologies Used | ||||
|  | ||||
| This project uses [Rust](https://www.rust-lang.org/) and [Rocket](https://rocket.rs). It also comes with an optional [Dockerfile](https://docs.docker.com/engine/reference/builder/) to help with deployment. | ||||
|  | ||||
| ## Run Your Battlesnake | ||||
|  | ||||
| ```sh | ||||
| cargo run | ||||
| ``` | ||||
|  | ||||
| You should see the following output once it is running | ||||
|  | ||||
| ```sh | ||||
| 🚀 Rocket has launched from http://0.0.0.0:8000 | ||||
| ``` | ||||
|  | ||||
| Open [localhost:8000](http://localhost:8000) in your browser and you should see | ||||
|  | ||||
| ```json | ||||
| {"apiversion":"1","author":"","color":"#888888","head":"default","tail":"default"} | ||||
| ``` | ||||
|  | ||||
| ## Play a Game Locally | ||||
|  | ||||
| Install the [Battlesnake CLI](https://github.com/BattlesnakeOfficial/rules/tree/main/cli) | ||||
| * You can [download compiled binaries here](https://github.com/BattlesnakeOfficial/rules/releases) | ||||
| * or [install as a go package](https://github.com/BattlesnakeOfficial/rules/tree/main/cli#installation) (requires Go 1.18 or higher) | ||||
|  | ||||
| Command to run a local game | ||||
|  | ||||
| ```sh | ||||
| battlesnake play -W 11 -H 11 --name 'Rust Starter Project' --url http://localhost:8000 -g solo --browser | ||||
| ``` | ||||
|  | ||||
| ## Next Steps | ||||
|  | ||||
| Continue with the [Battlesnake Quickstart Guide](https://docs.battlesnake.com/quickstart) to customize and improve your Battlesnake's behavior. | ||||
|  | ||||
| **Note:** To play games on [play.battlesnake.com](https://play.battlesnake.com) you'll need to deploy your Battlesnake to a live web server OR use a port forwarding tool like [ngrok](https://ngrok.com/) to access your server locally. | ||||
							
								
								
									
										4
									
								
								battlesnake/Rocket.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								battlesnake/Rocket.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| [default] | ||||
| address = "0.0.0.0" | ||||
| port = 8000 | ||||
| keep_alive = 0 | ||||
							
								
								
									
										101
									
								
								battlesnake/src/logic.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								battlesnake/src/logic.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | ||||
| // 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 log::info; | ||||
| use rand::seq::SliceRandom; | ||||
| use serde_json::{json, Value}; | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use crate::{Battlesnake, Board, Game}; | ||||
|  | ||||
| // 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 | ||||
| pub fn info() -> Value { | ||||
|     info!("INFO"); | ||||
|  | ||||
|     json!({ | ||||
|         "apiversion": "1", | ||||
|         "author": "Der Informatiker", | ||||
|         "color": "#00FFEE", | ||||
|         "head": "default", // TODO: Choose head | ||||
|         "tail": "default", // TODO: Choose tail | ||||
|     }) | ||||
| } | ||||
|  | ||||
| // start is called when your Battlesnake begins a game | ||||
| pub fn start(_game: &Game, _turn: &i32, _board: &Board, _you: &Battlesnake) { | ||||
|     info!("GAME START"); | ||||
| } | ||||
|  | ||||
| // end is called when your Battlesnake finishes a game | ||||
| pub fn end(_game: &Game, _turn: &i32, _board: &Board, _you: &Battlesnake) { | ||||
|     info!("GAME OVER"); | ||||
| } | ||||
|  | ||||
| // 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, turn: &i32, _board: &Board, you: &Battlesnake) -> Value { | ||||
|     let mut is_move_safe: HashMap<_, _> = vec![ | ||||
|         ("up", true), | ||||
|         ("down", true), | ||||
|         ("left", true), | ||||
|         ("right", true), | ||||
|     ] | ||||
|     .into_iter() | ||||
|     .collect(); | ||||
|  | ||||
|     // We've included code to prevent your Battlesnake from moving backwards | ||||
|     let my_head = &you.body[0]; // Coordinates of your head | ||||
|     let my_neck = &you.body[1]; // Coordinates of your "neck" | ||||
|  | ||||
|     if my_neck.x < my_head.x { | ||||
|         // Neck is left of head, don't move left | ||||
|         is_move_safe.insert("left", false); | ||||
|     } else if my_neck.x > my_head.x { | ||||
|         // Neck is right of head, don't move right | ||||
|         is_move_safe.insert("right", false); | ||||
|     } else if my_neck.y < my_head.y { | ||||
|         // Neck is below head, don't move down | ||||
|         is_move_safe.insert("down", false); | ||||
|     } else if my_neck.y > my_head.y { | ||||
|         // Neck is above head, don't move up | ||||
|         is_move_safe.insert("up", false); | ||||
|     } | ||||
|  | ||||
|     // TODO: Step 1 - Prevent your Battlesnake from moving out of bounds | ||||
|     // let board_width = &board.width; | ||||
|     // let board_height = &board.height; | ||||
|  | ||||
|     // TODO: Step 2 - Prevent your Battlesnake from colliding with itself | ||||
|     // let my_body = &you.body; | ||||
|  | ||||
|     // TODO: Step 3 - Prevent your Battlesnake from colliding with other Battlesnakes | ||||
|     // let opponents = &board.snakes; | ||||
|  | ||||
|     // Are there any safe moves left? | ||||
|     let safe_moves = is_move_safe | ||||
|         .into_iter() | ||||
|         .filter(|&(_, v)| v) | ||||
|         .map(|(k, _)| k) | ||||
|         .collect::<Vec<_>>(); | ||||
|  | ||||
|     // Choose a random move from the safe ones | ||||
|     let chosen = safe_moves.choose(&mut rand::thread_rng()).unwrap(); | ||||
|  | ||||
|     // TODO: Step 4 - Move towards food instead of random, to regain health and survive longer | ||||
|     // let food = &board.food; | ||||
|  | ||||
|     info!("MOVE {}: {}", turn, chosen); | ||||
|     return json!({ "move": chosen }); | ||||
| } | ||||
							
								
								
									
										123
									
								
								battlesnake/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								battlesnake/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| 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::collections::HashMap; | ||||
| use std::env; | ||||
|  | ||||
| mod logic; | ||||
|  | ||||
| // API and Response Objects | ||||
| // See https://docs.battlesnake.com/api | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| pub struct Game { | ||||
|     id: String, | ||||
|     ruleset: HashMap<String, Value>, | ||||
|     timeout: u32, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| pub struct Board { | ||||
|     height: u32, | ||||
|     width: i32, | ||||
|     food: Vec<Coord>, | ||||
|     snakes: Vec<Battlesnake>, | ||||
|     hazards: Vec<Coord>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| pub struct Battlesnake { | ||||
|     id: String, | ||||
|     name: String, | ||||
|     health: i32, | ||||
|     body: Vec<Coord>, | ||||
|     head: Coord, | ||||
|     length: i32, | ||||
|     latency: String, | ||||
|     shout: Option<String>, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| pub struct Coord { | ||||
|     x: i32, | ||||
|     y: i32, | ||||
| } | ||||
|  | ||||
| #[derive(Deserialize, Serialize, Debug)] | ||||
| pub struct GameState { | ||||
|     game: Game, | ||||
|     turn: i32, | ||||
|     board: Board, | ||||
|     you: Battlesnake, | ||||
| } | ||||
|  | ||||
| #[get("/")] | ||||
| fn handle_index() -> Json<Value> { | ||||
|     Json(logic::info()) | ||||
| } | ||||
|  | ||||
| #[post("/start", format = "json", data = "<start_req>")] | ||||
| fn handle_start(start_req: Json<GameState>) -> Status { | ||||
|     logic::start( | ||||
|         &start_req.game, | ||||
|         &start_req.turn, | ||||
|         &start_req.board, | ||||
|         &start_req.you, | ||||
|     ); | ||||
|  | ||||
|     Status::Ok | ||||
| } | ||||
|  | ||||
| #[post("/move", format = "json", data = "<move_req>")] | ||||
| fn handle_move(move_req: Json<GameState>) -> Json<Value> { | ||||
|     let response = logic::get_move( | ||||
|         &move_req.game, | ||||
|         &move_req.turn, | ||||
|         &move_req.board, | ||||
|         &move_req.you, | ||||
|     ); | ||||
|  | ||||
|     Json(response) | ||||
| } | ||||
|  | ||||
| #[post("/end", format = "json", data = "<end_req>")] | ||||
| fn handle_end(end_req: Json<GameState>) -> 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], | ||||
|         ) | ||||
| } | ||||
							
								
								
									
										6
									
								
								xtask/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								xtask/Cargo.toml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| [package] | ||||
| name = "xtask" | ||||
| version = "0.1.0" | ||||
| edition = "2021" | ||||
|  | ||||
| [dependencies] | ||||
							
								
								
									
										86
									
								
								xtask/src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								xtask/src/main.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| use std::{ | ||||
|     env, | ||||
|     net::TcpStream, | ||||
|     path::{Path, PathBuf}, | ||||
|     process::Command, | ||||
|     thread::sleep, | ||||
|     time::Duration, | ||||
| }; | ||||
|  | ||||
| type DynError = Box<dyn std::error::Error>; | ||||
|  | ||||
| fn main() { | ||||
|     if let Err(e) = try_main() { | ||||
|         eprintln!("{e}"); | ||||
|         std::process::exit(-1) | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn try_main() -> Result<(), DynError> { | ||||
|     let task = env::args().nth(1); | ||||
|     match task.as_deref() { | ||||
|         Some("local") => local()?, | ||||
|         _ => print_help(), | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn print_help() { | ||||
|     eprintln!( | ||||
|         "Tasks: | ||||
|  | ||||
| local       runs the snake on a local game | ||||
| " | ||||
|     ) | ||||
| } | ||||
|  | ||||
| fn local() -> Result<(), DynError> { | ||||
|     let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); | ||||
|  | ||||
|     let mut snake = Command::new(cargo) | ||||
|         .current_dir(project_root()) | ||||
|         .args( | ||||
|             ["run", "--bin", "battlesnake"] | ||||
|                 .map(str::to_string) | ||||
|                 .into_iter() | ||||
|                 .chain(env::args().skip(1)), | ||||
|         ) | ||||
|         .spawn()?; | ||||
|  | ||||
|     while snake.try_wait()?.is_none() { | ||||
|         // check if port 8000 has been opened. Only then the game can be started | ||||
|         if TcpStream::connect(("127.0.0.1", 8000)).is_ok() { | ||||
|             let _game = Command::new("./battlesnake-cli") | ||||
|                 .current_dir(project_root()) | ||||
|                 .args([ | ||||
|                     "play", | ||||
|                     "-W", | ||||
|                     "11", | ||||
|                     "-H", | ||||
|                     "11", | ||||
|                     "--name", | ||||
|                     "local test", | ||||
|                     "--url", | ||||
|                     "http://localhost:8000", | ||||
|                     "-g", | ||||
|                     "solo", | ||||
|                     "--browser", | ||||
|                 ]) | ||||
|                 .status(); | ||||
|             break; | ||||
|         } else { | ||||
|             sleep(Duration::from_secs(1)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     snake.kill()?; | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn project_root() -> PathBuf { | ||||
|     Path::new(&env!("CARGO_MANIFEST_DIR")) | ||||
|         .ancestors() | ||||
|         .nth(1) | ||||
|         .unwrap() | ||||
|         .to_path_buf() | ||||
| } | ||||
		Reference in New Issue
	
	Block a user