initial commit
This commit is contained in:
commit
c605d2887a
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).
|
||||||
|
|
||||||
|
![Battlesnake Logo](https://media.battlesnake.com/social/StarterSnakeGitHubRepos_Rust.png)
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
[![Run on Replit](https://repl.it/badge/github/BattlesnakeOfficial/starter-snake-rust)](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()
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user