initial commit

This commit is contained in:
Max Känner 2024-09-02 15:16:32 +02:00
commit c605d2887a
14 changed files with 2128 additions and 0 deletions

2
.cargo/config.toml Normal file
View File

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
target

1704
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

4
Cargo.toml Normal file
View File

@ -0,0 +1,4 @@
[workspace]
members = ["battlesnake", "xtask"]
resolver = "2"

8
Dockerfile Normal file
View 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
View 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

Binary file not shown.

19
battlesnake/Cargo.toml Normal file
View 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
View 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
View File

@ -0,0 +1,4 @@
[default]
address = "0.0.0.0"
port = 8000
keep_alive = 0

101
battlesnake/src/logic.rs Normal file
View 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
View 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
View File

@ -0,0 +1,6 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"
[dependencies]

86
xtask/src/main.rs Normal file
View 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()
}