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