Compare commits

...

24 Commits

Author SHA1 Message Date
f0156ccec4 add multithreading for seed cracking
All checks were successful
Build / build (push) Successful in 1m55s
2025-06-24 20:59:32 +02:00
2c2bfd3489 fix fast validation
All checks were successful
Build / build (push) Successful in 1m51s
2025-06-24 20:49:56 +02:00
263b0642f4 calculate less
All checks were successful
Build / build (push) Successful in 2m38s
Speed up validation by precalculating some stuff and toing a fast return
wherever possible
2025-06-24 20:07:25 +02:00
081c41b753 try some seedcracking
All checks were successful
Build / build (push) Successful in 3m55s
2025-06-24 17:28:11 +02:00
549d4fe36d modularize simulation 2025-06-24 17:27:56 +02:00
6f54282c9a improve xtask 2025-06-24 17:27:29 +02:00
6f3a1d138a ignore assets and games 2025-06-24 17:27:13 +02:00
0876f1ed3c explicitly select rust toolchain 2025-06-24 17:27:01 +02:00
87fc6cccd2 update dependencies
All checks were successful
Build / build (push) Successful in 1m12s
2025-06-07 21:17:38 +02:00
2f87e2aa60 improve log output
All checks were successful
Build / build (push) Successful in 1m7s
2025-06-06 21:38:34 +02:00
5f1d3dfc4f build local snakes in release
All checks were successful
Build / build (push) Successful in 1m10s
2025-06-06 21:29:59 +02:00
d44538b749 make number of compute threads configurable
All checks were successful
Build / build (push) Successful in 1m15s
2025-06-06 21:24:18 +02:00
b4b332bdbb reduce regression parallelism
All checks were successful
Build / build (push) Successful in 2m52s
2025-06-06 21:16:22 +02:00
b97d7c895a add multithreading 2025-06-06 21:15:26 +02:00
e5600fe038 use faster rng
All checks were successful
Build / build (push) Successful in 1m54s
2025-04-25 19:55:42 +02:00
15d90357ec remove unused import
All checks were successful
Build / build (push) Successful in 2m6s
2025-04-25 15:04:09 +02:00
99acd4ad1f improve score condition
Some checks failed
Build / build (push) Has been cancelled
2025-04-25 14:59:21 +02:00
bad4d916b8 make battlesnake the default
All checks were successful
Build / build (push) Successful in 3m44s
2025-04-24 21:22:12 +02:00
caa6eed783 improve progress bar 2025-04-24 21:22:04 +02:00
9333f6c6fb reduce regression parallelism 2025-04-22 19:48:38 +02:00
1d527a89cd make simulation faster
All checks were successful
Build / build (push) Successful in 2m43s
2025-04-21 21:48:11 +02:00
c5097ec417 update
All checks were successful
Build / build (push) Successful in 3m51s
2025-04-21 20:56:18 +02:00
879f99e23f multi agent mcts
All checks were successful
Build / build (push) Successful in 2m10s
2025-01-26 19:29:45 +01:00
302f5cac50 fix regression test 2025-01-26 19:29:24 +01:00
23 changed files with 3964 additions and 536 deletions

View File

@ -1,5 +1,2 @@
[alias]
xtask = "run --package xtask --"
[build]
rustflags = "-Ctarget-cpu=core-avx2"

2
.gitignore vendored
View File

@ -1 +1,3 @@
target
assets
games

1669
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,9 @@
[workspace]
members = ["battlesnake", "xtask"]
resolver = "3"
resolver = "2"
default-members = ["battlesnake"]
[profile.release]
lto = "fat"
codegen-units = 1

View File

@ -1,11 +1,11 @@
FROM rust:1.84-bookworm AS build
FROM rust:1.86-bullseye AS build
COPY battlesnake/ /usr/app
WORKDIR /usr/app
RUN cargo install --path .
FROM debian:bookworm
FROM debian:bullseye-slim
COPY --from=build /usr/local/cargo/bin/battlesnake /bin/battlesnake
CMD ["battlesnake"]

View File

@ -2,7 +2,8 @@
authors = ["Max Känner"]
name = "battlesnake"
version = "2.0.0"
edition = "2021"
edition = "2024"
rust-version = "1.86"
readme = "README.md"
repository = "https://git.mkaenner.de/max/battlesnake"
@ -10,11 +11,21 @@ keywords = ["battlesnake"]
description = """
A simple Battlesnake written in Rust
"""
default-run = "battlesnake"
[lints.clippy]
pedantic = "warn"
nursery = "warn"
[[bin]]
name = "battlesnake"
[[bin]]
name = "seed-cracker"
[[bin]]
name = "generate"
[dependencies]
# server
tokio = { version = "1.43", features = ["full"] }
@ -28,8 +39,23 @@ env_logger = "0.11"
# other
bitvec = "1.0"
enum-iterator = "2.1"
rand = "0.8"
rand = "0.9"
float-ord = "0.3"
futures-util = "0.3.31"
az = "1.2.1"
blanket = "0.4.0"
hashbrown = "0.15.4"
serde_json = "1.0.140"
clap = { version = "4.5.39", features = ["derive"] }
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
enum_dispatch = "0.3.13"
lru = "0.14.0"
lfu_cache = "1.3.0"
memmap2 = "0.9.5"
bytemuck = "1.23.1"
flame = "0.2.2"
flamer = "0.5.0"
rayon = "1.10.0"
[dev-dependencies]
criterion = "0.5"

View File

@ -1,15 +1,19 @@
use std::sync::{
atomic::{AtomicU32, Ordering},
use std::{
hint::black_box,
sync::{
Arc,
atomic::{AtomicU32, Ordering},
},
};
use criterion::{black_box, criterion_group, criterion_main, Bencher, BenchmarkId, Criterion};
use criterion::{Bencher, BenchmarkId, Criterion, criterion_group, criterion_main};
use battlesnake::types::{
simulation::Board,
wire::{Battlesnake, Board as WireBoard, Game, Request, RoyaleSettings, Ruleset, Settings},
Coord,
simulation::Game as Board,
wire::{Battlesnake, Board as WireBoard, Game, Request, RoyaleSettings, Ruleset, Settings},
};
use rand::{SeedableRng, rngs::SmallRng};
fn create_start_snake(coord: Coord) -> Battlesnake {
let id: Arc<str> = format!("{coord:?}").into();
@ -74,13 +78,8 @@ fn standard(c: &mut Criterion) {
let benchmark = |b: &mut Bencher, board: &Board| {
b.iter(|| {
let mut board = board.clone();
let turn = board.simulate_random(|board| {
if board.num_snakes() <= 1 {
Some(board.turn())
} else {
None
}
});
board.simulate_random(&mut SmallRng::from_os_rng());
let turn = board.board.turn();
if turn < turns_min.load(Ordering::Relaxed) {
turns_min.store(turn, Ordering::Relaxed);
@ -151,13 +150,8 @@ fn constrictor(c: &mut Criterion) {
let benchmark = |b: &mut Bencher, board: &Board| {
b.iter(|| {
let mut board = board.clone();
let turn = board.simulate_random(|board| {
if board.num_snakes() <= 1 {
Some(board.turn())
} else {
None
}
});
board.simulate_random(&mut SmallRng::from_os_rng());
let turn = board.board.turn();
if turn < turns_min.load(Ordering::Relaxed) {
turns_min.store(turn, Ordering::Relaxed);

View File

@ -0,0 +1,47 @@
use std::{
fs::File,
io::{self, BufWriter, Write},
};
use az::Az;
use bytemuck::cast_slice;
fn main() -> Result<(), io::Error> {
let mut chain = BufWriter::new(File::create("assets/chain")?);
let mut indices = vec![0u32; (i32::MAX - 1).az::<usize>()];
let mut x = 1;
let mut count = 0;
loop {
if count % 1_000_000 == 0 {
println!("current: {count:0>10}");
}
let next = seedrand(x);
indices[x.az::<usize>() - 1] = count;
chain.write_all(&next.to_ne_bytes())?;
x = next;
count += 1;
if x == 1 {
break;
}
}
File::create("assets/table")?.write_all(cast_slice(&indices))?;
Ok(())
}
const fn seedrand(x: i32) -> i32 {
const A: i32 = 48_271;
const Q: i32 = 44_488;
const R: i32 = 3_399;
let hi = x / Q;
let lo = x % Q;
let x = A * lo - R * hi;
if x < 0 { x + i32::MAX } else { x }
}

File diff suppressed because it is too large Load Diff

View File

@ -1,39 +1,110 @@
use std::env;
use std::{
env,
sync::atomic::{AtomicUsize, Ordering},
};
use axum::{
extract::Json,
Router,
extract::{Json, State},
response,
routing::{get, post},
Router,
};
use battlesnake::types::{
simulation::Board,
wire::{Request, Response},
Direction,
simulation::{Board, Game},
wire::{Request, Response},
};
use float_ord::FloatOrd;
use futures_util::future::join_all;
use hashbrown::HashMap;
use log::{debug, error, info, trace, warn};
use rand::prelude::*;
use serde::Serialize;
use tokio::{
fs::File,
io::AsyncWriteExt,
net::TcpListener,
sync::mpsc::{UnboundedSender, unbounded_channel},
time::{Duration, Instant},
};
static THREADS: AtomicUsize = AtomicUsize::new(1);
#[tokio::main]
async fn main() {
env_logger::init();
let (sender, mut receiver) = unbounded_channel();
debug!("Creating routes");
let app = Router::new()
.route("/", get(info))
.route("/start", post(start))
.route("/move", post(get_move))
.route("/end", post(end));
.route("/end", post(end))
.with_state(sender);
let threads = env::var("THREADS")
.ok()
.and_then(|threads| {
threads
.parse()
.inspect_err(|err| error!("Unable to parse number of threads: {err}"))
.ok()
})
.unwrap_or(1);
THREADS.store(threads, Ordering::Relaxed);
debug!("Creating listener");
let port = env::var("PORT").unwrap_or_else(|_| "8000".into());
let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();
debug!("Starting observer");
tokio::spawn(async move {
let mut games = HashMap::new();
while let Some((request_type, request)) = receiver.recv().await {
match request_type {
RequestType::Start => {
let game_id = request.game.id.clone();
info!("Got start request {game_id}");
if let Some(old_requests) = games.insert(game_id, vec![request]) {
warn!("evicted duplicate game: {old_requests:?}");
}
}
RequestType::GetMove => {
let game_id = request.game.id.clone();
info!("Got move request {game_id}");
games.entry(game_id).or_default().push(request);
}
RequestType::End => {
let game_id = request.game.id.clone();
info!("Got end request {game_id}");
if let Some(mut requests) = games.remove(&game_id) {
requests.push(request);
let json = match serde_json::to_vec_pretty(&requests) {
Ok(json) => json,
Err(e) => {
error!("Unable to serealize json: {e}");
continue;
}
};
match File::create_new(format!("games/{game_id}.json")).await {
Ok(mut file) => {
if let Err(e) = file.write_all(&json).await {
error!("Unable to write jsone: {e}");
}
}
Err(e) => error!("Unable to open file: {e}"),
}
} else {
warn!("end of game without game: {request:?}");
}
}
}
}
warn!("Observer stopped");
});
debug!("Starting server");
axum::serve(listener, app).await.unwrap();
}
@ -60,20 +131,41 @@ struct Info {
version: &'static str,
}
async fn start(request: Json<Request>) {
let board = Board::from(&*request);
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
enum RequestType {
Start,
GetMove,
End,
}
async fn start(
State(sender): State<UnboundedSender<(RequestType, Request)>>,
Json(request): Json<Request>,
) {
if let Err(e) = sender.send((RequestType::Start, request.clone())) {
warn!("Unable to observe request: {e}");
}
let board = Board::from(&request);
info!("got start request: {board}");
}
async fn get_move(request: Json<Request>) -> response::Json<Response> {
#[allow(clippy::too_many_lines)]
async fn get_move(
State(sender): State<UnboundedSender<(RequestType, Request)>>,
Json(request): Json<Request>,
) -> response::Json<Response> {
let start = Instant::now();
let board = Board::from(&*request);
let id = board.get_id(&request.you.id).unwrap_or_else(|| {
if let Err(e) = sender.send((RequestType::GetMove, request.clone())) {
warn!("Unable to observe request: {e}");
}
let board = Game::from(&request);
let timeout = Duration::from_millis(u64::from(request.game.timeout));
let id = board.board.get_id(&request.you.id).unwrap_or_else(|| {
error!("My id is not in the simulation board");
0
});
debug!("got move request: {board}");
let actions = board.valid_actions(id).collect::<Vec<_>>();
debug!("got move request: {}", board.board);
let actions = board.board.valid_actions(id).collect::<Vec<_>>();
if actions.len() <= 1 {
info!(
"only one possible action. Fast forwarding {:?}",
@ -84,83 +176,92 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> {
shout: None,
});
}
info!("valid actions: {:?}", actions);
info!("valid actions: {actions:?}");
tokio::task::spawn_blocking(move || {
let base_turns = board.turn();
let end_condition: &dyn Fn(&Board) -> Option<u32> = match &*request.game.ruleset.name {
"solo" => &|board: &Board| {
if board.num_snakes() == 0
|| board.turn() > base_turns + (u32::from(request.you.length) * 3).min(32)
{
Some(board.turn())
} else {
None
if start.elapsed() > Duration::from_millis(10) {
error!(
"The calculation started late ({}ms)",
start.elapsed().as_millis()
);
}
},
_ => &|board: &Board| {
if board.num_snakes() <= 1
|| board.turn() > base_turns + (u32::from(request.you.length) * 3).min(32)
{
Some(u32::from(board.alive(id)))
} else {
None
}
},
let score_fn: fn(&Board, u8) -> u32 = match &*request.game.ruleset.name {
"solo" => score_solo,
_ => score_standard,
};
let mut mcts_manager = MctsManager::new(id);
let action_futures = (0..THREADS.load(Ordering::Relaxed)).map(|_| {
let request = request.clone();
let board = board.clone();
let mut rng = SmallRng::from_os_rng();
tokio::task::spawn_blocking(move || {
let mut mcts_managers: Vec<_> = (0..request.board.snakes.len())
.map(|id| MctsManager::new(u8::try_from(id).unwrap()))
.collect();
let c = f32::sqrt(2.0);
'outer: while start.elapsed() < Duration::from_millis(250) {
let mut mcts_actions = Vec::new();
while start.elapsed() < timeout * 4 / 5 {
let mut board = board.clone();
while let Some(action) = mcts_manager.next_action(&board, c) {
board.next_turn(&[(id, action)]);
if let Some(score) = end_condition(&board) {
mcts_manager.apply_score(score);
continue 'outer;
let mut game_over = false;
while !game_over {
mcts_actions.clear();
mcts_actions.extend(mcts_managers.iter_mut().filter_map(|mcts_manager| {
mcts_manager
.next_action(&board.board, c, &mut rng)
.map(|action| (mcts_manager.snake, action))
}));
game_over = board.next_turn_random(&mcts_actions, &mut rng);
if mcts_actions.is_empty() {
break;
}
}
let score = board.simulate_random(end_condition);
if !game_over {
board.simulate_random(&mut rng);
}
for mcts_manager in &mut mcts_managers {
let id = mcts_manager.snake;
let score = score_fn(&board.board, id);
mcts_manager.apply_score(score);
}
for action in actions {
let score = mcts_manager.base.next[usize::from(action)]
.as_ref()
.map(|info| info.score as f32 / info.played as f32);
if let Some(score) = score {
info!("{action:?} -> {score}");
} else {
info!("{action:?} -> None");
}
}
let my_mcts_manager = mcts_managers.into_iter().nth(usize::from(id)).unwrap();
let action = mcts_manager
.base
.next
.iter()
let actions = my_mcts_manager.base.next.map(|action| {
action.map_or(0.0, |action| action.score as f32 / action.played as f32)
});
(actions, my_mcts_manager.base.played)
})
});
let (scores, played) = join_all(action_futures).await.into_iter().fold(
([0.0; 4], 0),
|(mut total, mut games), actions| {
if let Ok((actions, new_games)) = actions {
for i in 0..total.len() {
total[i] += actions[i];
}
games += new_games;
}
(total, games)
},
);
for action in actions {
let score = scores[usize::from(action)];
info!("{action:?} -> {score}");
}
let action = scores
.into_iter()
.enumerate()
.filter_map(|(index, info)| {
info.as_ref().map(|info| {
(
match index {
.max_by_key(|(_, score)| FloatOrd(*score))
.map(|(index, _)| match index {
0 => Direction::Up,
1 => Direction::Down,
2 => Direction::Left,
3 => Direction::Right,
_ => unreachable!(),
},
info,
)
})
})
.max_by_key(|(_, info)| FloatOrd(info.score as f32 / info.played as f32))
.map(|(action, _)| action);
});
if let Some(action) = action {
info!(
"found action {action:?} after {} simulations.",
mcts_manager.base.played
);
info!("found action {action:?} after {played} simulations.",);
} else {
warn!("unable to find a valid action");
}
@ -169,13 +270,28 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> {
direction: action.unwrap_or(Direction::Up),
shout: None,
})
})
.await
.unwrap()
}
async fn end(request: Json<Request>) {
let board = Board::from(&*request);
const fn score_solo(board: &Board, _id: u8) -> u32 {
board.turn()
}
fn score_standard(board: &Board, id: u8) -> u32 {
if board.alive(id) {
1 + u32::from(board.max_length() == board.length(id))
} else {
0
}
}
async fn end(
State(sender): State<UnboundedSender<(RequestType, Request)>>,
Json(request): Json<Request>,
) {
if let Err(e) = sender.send((RequestType::End, request.clone())) {
warn!("Unable to observe request: {e}");
}
let board = Board::from(&request);
info!("got end request: {board}");
}
@ -231,7 +347,7 @@ impl MctsManager {
self.base.score += score;
let mut current = &mut self.base;
for action in &self.actions {
let Some(ref mut new_current) = &mut current.next[usize::from(*action)] else {
let Some(new_current) = &mut current.next[usize::from(*action)] else {
error!("got action without actioninfo");
break;
};
@ -243,14 +359,14 @@ impl MctsManager {
self.expanded = false;
}
fn next_action(&mut self, board: &Board, c: f32) -> Option<Direction> {
fn next_action(&mut self, board: &Board, c: f32, rng: &mut impl RngCore) -> Option<Direction> {
if self.expanded {
return None;
}
let mut current = &mut self.base;
for action in &self.actions {
let Some(ref mut new_current) = &mut current.next[usize::from(*action)] else {
let Some(new_current) = &mut current.next[usize::from(*action)] else {
error!("got action without actioninfo");
return None;
};
@ -264,11 +380,7 @@ impl MctsManager {
.collect();
trace!("got actions: {ucts:?}");
if ucts.iter().any(|(_, uct)| uct.is_none()) {
let action = ucts
.iter()
.filter(|(_, uct)| uct.is_none())
.choose(&mut thread_rng())?
.0;
let action = ucts.iter().filter(|(_, uct)| uct.is_none()).choose(rng)?.0;
self.expanded = true;
current.next[usize::from(action)].replace(ActionInfo::new());
self.actions.push(action);

View File

@ -1,10 +1,12 @@
use std::fmt::Display;
use enum_iterator::Sequence;
use serde::{Deserialize, Serialize};
pub mod simulation;
pub mod wire;
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
pub struct Coord {
pub x: u8,
pub y: u8,
@ -33,7 +35,13 @@ impl Coord {
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Sequence)]
impl Display for Coord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Serialize, Sequence)]
#[serde(rename_all = "lowercase")]
pub enum Direction {
/// Move in positive y direction

View File

@ -0,0 +1,43 @@
mod royale;
mod standard;
use blanket::blanket;
use enum_dispatch::enum_dispatch;
use rand::Rng;
use royale::Royale;
use standard::Standard;
use crate::types::wire::Game;
use super::Board;
#[blanket(derive(Ref, Arc, Mut, Box))]
#[enum_dispatch(Maps)]
pub trait Map {
/// Called before the board is updated
fn pre_update(&self, board: &mut Board, rng: &mut impl Rng);
/// Called after the board is updated
fn post_update(&self, board: &mut Board, rng: &mut impl Rng);
}
#[enum_dispatch]
#[derive(Debug, Clone, Copy)]
pub enum Maps {
Standard,
Royale,
}
pub trait MapId {
/// ID of the map for detection
const ID: &str;
}
impl From<&Game> for Maps {
fn from(value: &Game) -> Self {
match value.map.as_ref() {
Royale::ID => Royale::from(&value.ruleset.settings).into(),
_ => Standard::from(&value.ruleset.settings).into(),
}
}
}

View File

@ -0,0 +1,86 @@
use rand::{Rng, seq::IteratorRandom};
use crate::types::{Coord, Direction, simulation::Board, wire::Settings};
use super::{Map, MapId, standard::Standard};
#[derive(Debug, Clone, Copy)]
pub struct Royale {
standard: Standard,
shrink_every_n_turns: u8,
}
impl Map for Royale {
fn pre_update(&self, board: &mut Board, rng: &mut impl Rng) {
self.standard.pre_update(board, rng);
}
fn post_update(&self, board: &mut Board, rng: &mut impl Rng) {
self.standard.post_update(board, rng);
// Royale uses the current turn to generate hazards, not the previous turn that's in the
// board state
let turn = board.turn + 1;
if turn < u32::from(self.shrink_every_n_turns) {
return;
}
let side = enum_iterator::all::<Direction>()
.choose(rng)
.unwrap_or(Direction::Up);
match side {
Direction::Up => {
if let Some(i) = board.hazard.first_zero() {
let y = board.linear_to_coord(i).y;
for x in 0..board.width {
let i = board.coord_to_linear(Coord { x, y });
board.hazard.set(i, true);
}
}
}
Direction::Down => {
if let Some(i) = board.hazard.last_zero() {
let y = board.linear_to_coord(i).y;
for x in 0..board.width {
let i = board.coord_to_linear(Coord { x, y });
board.hazard.set(i, true);
}
}
}
Direction::Left => {
if let Some(i) = board.hazard.first_zero() {
let x = board.linear_to_coord(i).y;
for y in 0..board.height {
let i = board.coord_to_linear(Coord { x, y });
board.hazard.set(i, true);
}
}
}
Direction::Right => {
if let Some(i) = board.hazard.last_zero() {
let x = board.linear_to_coord(i).y;
for y in 0..board.height {
let i = board.coord_to_linear(Coord { x, y });
board.hazard.set(i, true);
}
}
}
}
}
}
impl MapId for Royale {
const ID: &str = "royale";
}
impl From<&Settings> for Royale {
fn from(value: &Settings) -> Self {
let standard = Standard::from(value);
let shrink_every_n_turns = value.royale.shrink_every_n_turns;
Self {
standard,
shrink_every_n_turns,
}
}
}

View File

@ -0,0 +1,78 @@
use az::SaturatingAs;
use rand::{Rng, seq::SliceRandom};
use crate::types::{Coord, simulation::Board, wire::Settings};
use super::{Map, MapId};
#[derive(Debug, Clone, Copy)]
pub struct Standard {
min_food: u16,
food_spawn_chance: u8,
}
impl Map for Standard {
fn pre_update(&self, _board: &mut Board, _rng: &mut impl Rng) {}
fn post_update(&self, board: &mut Board, rng: &mut impl Rng) {
let food_needed = self.check_food_needing_placement(board, rng);
if food_needed > 0 {
Self::place_food_randomly(board, rng, food_needed);
}
}
}
impl MapId for Standard {
const ID: &str = "standard";
}
impl From<&Settings> for Standard {
fn from(value: &Settings) -> Self {
let min_food = value.minimum_food;
let food_spawn_chance = value.food_spawn_chance;
Self {
min_food,
food_spawn_chance,
}
}
}
impl Standard {
fn check_food_needing_placement(self, board: &Board, rng: &mut impl rand::Rng) -> u16 {
let num_current_food: u16 = board.food.count_ones().saturating_as();
if num_current_food < self.min_food {
return self.min_food - num_current_food;
}
if self.food_spawn_chance > 0 && (100 - rng.random_range(0..100)) < self.food_spawn_chance {
return 1;
}
0
}
fn place_food_randomly(board: &mut Board, rng: &mut impl rand::Rng, food_needed: u16) {
let mut unoccupied_points: Vec<_> = board.get_unoccupied_points(false, false).collect();
Self::place_food_ramdomly_at_positions(
board,
rng,
food_needed,
unoccupied_points.as_mut_slice(),
);
}
fn place_food_ramdomly_at_positions(
board: &mut Board,
rng: &mut impl rand::Rng,
food_needed: u16,
positions: &mut [Coord],
) {
let food_needed = usize::from(food_needed).min(positions.len());
positions.shuffle(rng);
for tile in &positions[..food_needed] {
let i = board.coord_to_linear(*tile);
board.food.set(i, true);
}
}
}

View File

@ -1,3 +1,6 @@
mod maps;
mod rules;
use std::{
collections::VecDeque,
fmt::Display,
@ -6,11 +9,14 @@ use std::{
sync::Arc,
};
use az::SaturatingAs;
use bitvec::prelude::*;
use log::{error, warn};
use maps::{Map, Maps};
use rand::prelude::*;
use rules::{Ruleset, Rulesets};
use super::{wire::Request, Coord, Direction};
use super::{Coord, Direction, wire::Request};
#[derive(Debug, PartialEq, Eq, Clone)]
enum SmallBitBox {
@ -58,19 +64,79 @@ impl DerefMut for SmallBitBox {
}
}
#[derive(Clone)]
pub struct Game {
pub board: Board,
map: Maps,
ruleset: Rulesets,
}
impl From<&Request> for Game {
fn from(value: &Request) -> Self {
let board = value.into();
let map = (&value.game).into();
let ruleset = (&value.game.ruleset).into();
Self {
board,
map,
ruleset,
}
}
}
impl Game {
pub fn simulate_random(&mut self, rng: &mut impl Rng) {
loop {
let random_actions: Vec<_> = (0..self.board.snakes.len())
.filter_map(|i| {
self.board
.valid_actions_index(i)
.choose(rng)
.map(|direction| (i.saturating_as(), direction))
})
.collect();
if self.next_turn(&random_actions, rng) {
break;
}
}
}
pub fn next_turn_random(&mut self, actions: &[(u8, Direction)], rng: &mut impl Rng) -> bool {
let random_actions: Vec<_> = (0..self.board.snakes.len())
.filter_map(|i| {
actions
.iter()
.find(|(j, _)| i == usize::from(*j))
.copied()
.or_else(|| {
self.board
.valid_actions_index(i)
.choose(rng)
.map(|direction| (i.saturating_as(), direction))
})
})
.collect();
self.next_turn(&random_actions, rng)
}
pub fn next_turn(&mut self, actions: &[(u8, Direction)], rng: &mut impl Rng) -> bool {
self.map.pre_update(&mut self.board, rng);
let game_over = self.ruleset.execute(&mut self.board, actions);
self.map.post_update(&mut self.board, rng);
self.board.turn += 1;
game_over
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Board {
width: u8,
height: u8,
hazard_damage: u8,
food_spawn_chance: u8,
min_food: u16,
turn: u32,
food: SmallBitBox,
hazard: SmallBitBox,
free: SmallBitBox,
snakes: Vec<Snake>,
constrictor: bool,
id_map: Arc<[(u8, Arc<str>)]>,
}
@ -94,15 +160,11 @@ impl From<&Request> for Board {
let mut board = Self {
width,
height,
hazard_damage: value.game.ruleset.settings.hazard_damage_per_turn,
food_spawn_chance: value.game.ruleset.settings.food_spawn_chance,
min_food: value.game.ruleset.settings.minimum_food,
turn: value.turn,
food: SmallBitBox::new(false, fields),
hazard: SmallBitBox::new(false, fields),
free: SmallBitBox::new(true, fields),
snakes: Vec::with_capacity(value.board.snakes.len()),
constrictor: &*value.game.ruleset.name == "constrictor",
id_map: id_map.into(),
};
@ -116,11 +178,12 @@ impl From<&Request> for Board {
board.hazard.set(index, true);
}
let constrictor = value.game.ruleset.name.as_ref() == "constrictor";
for (id, snake) in value.board.snakes.iter().enumerate() {
for &tile in snake
.body
.iter()
.take(snake.body.len() - usize::from(!board.constrictor))
.take(snake.body.len() - usize::from(!constrictor))
{
let index = board.coord_to_linear(tile);
board.free.set(index, false);
@ -139,17 +202,7 @@ impl From<&Request> for Board {
impl Display for Board {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"{} {}x{} {}% ({}) {}dmg @ {}",
if self.constrictor { "constrictor" } else { "" },
self.width,
self.height,
self.food_spawn_chance,
self.min_food,
self.hazard_damage,
self.turn
)?;
writeln!(f, "{}x{} @ {}", self.width, self.height, self.turn)?;
for y in (0..self.height).rev() {
for x in 0..self.width {
@ -210,6 +263,23 @@ impl Board {
self.id_to_index(id).is_some()
}
#[must_use]
pub fn length(&self, id: u8) -> usize {
let Some(index) = self.id_to_index(id) else {
return 0;
};
self.snakes[index].body.len()
}
#[must_use]
pub fn max_length(&self) -> usize {
self.snakes
.iter()
.map(|snake| snake.body.len())
.max()
.unwrap_or(0)
}
#[must_use]
pub fn is_food(&self, tile: Coord) -> bool {
let index = self.coord_to_linear(tile);
@ -224,7 +294,7 @@ impl Board {
#[must_use]
pub fn is_free(&self, tile: Coord) -> bool {
if !(tile.x < self.width && tile.y < self.height) {
if !self.is_in_bounds(tile) {
return false;
}
let index = self.coord_to_linear(tile);
@ -233,55 +303,35 @@ impl Board {
pub fn valid_actions(&self, id: u8) -> impl Iterator<Item = Direction> + use<'_> {
let index = self.id_to_index(id);
if index.is_none() {
warn!("Asked for a snake that doesn't exist");
}
index
.into_iter()
.flat_map(|index| self.valid_actions_index(index))
}
#[must_use]
pub fn random_action(&self, id: u8) -> Direction {
pub fn random_action(&self, id: u8, rng: &mut impl RngCore) -> Direction {
let Some(index) = self.id_to_index(id) else {
return Direction::Up;
};
self.valid_actions_index(index)
.choose(&mut thread_rng())
.choose(rng)
.unwrap_or(Direction::Up)
}
pub fn random_actions(&self) -> impl Iterator<Item = (u8, Direction)> + use<'_> {
pub fn random_actions<'a, R: RngCore>(
&self,
rng: &'a mut R,
) -> impl Iterator<Item = (u8, Direction)> + use<'_, 'a, R> {
(0..self.snakes.len()).map(|index| {
(
self.snakes[index].id,
self.valid_actions_index(index)
.choose(&mut thread_rng())
.choose(rng)
.unwrap_or(Direction::Up),
)
})
}
pub fn simulate_random<T>(&mut self, stop: impl Fn(&Self) -> Option<T>) -> T {
loop {
if let Some(score) = stop(self) {
break score;
}
self.next_turn(&[]);
}
}
pub fn next_turn(&mut self, actions: &[(u8, Direction)]) {
self.move_standard(actions);
self.starvation_standard();
self.hazard_damage_standard();
self.feed_snakes_standard();
self.eliminate_snake_standard();
self.update_free_map();
self.spawn_food();
self.turn += 1;
}
fn id_to_index(&self, id: u8) -> Option<usize> {
self.snakes.binary_search_by_key(&id, |snake| snake.id).ok()
}
@ -289,131 +339,7 @@ impl Board {
fn valid_actions_index(&self, index: usize) -> impl Iterator<Item = Direction> + use<'_> {
let head = self.snakes[index].head();
enum_iterator::all::<Direction>()
.map(move |direction| (direction, head.wrapping_apply(direction)))
.filter(|(_, tile)| self.is_in_bounds(*tile))
.filter(|(_, tile)| self.is_free(*tile))
.map(|(direction, _)| direction)
}
fn move_standard(&mut self, actions: &[(u8, Direction)]) {
for i in 0..self.snakes.len() {
let snake = &self.snakes[i];
let action = actions.iter().find(|(id, _)| *id == snake.id).map_or_else(
|| {
self.valid_actions_index(i)
.choose(&mut thread_rng())
.unwrap_or(Direction::Up)
},
|(_, action)| *action,
);
let new_head = snake.head().wrapping_apply(action);
self.snakes[i].advance(new_head);
}
}
fn starvation_standard(&mut self) {
for snake in &mut self.snakes {
snake.health = snake.health.saturating_sub(1);
}
}
fn hazard_damage_standard(&mut self) {
let mut i = 0;
while i < self.snakes.len() {
let head = self.snakes[i].head();
if self.is_in_bounds(head) {
let head_index = self.coord_to_linear(head);
if self.hazard[head_index] && !self.food[head_index] {
let health = &mut self.snakes[i].health;
*health = health.saturating_sub(1);
if *health == 0 {
let snake = self.snakes.remove(i);
for tile in snake.body {
let index = self.coord_to_linear(tile);
self.free.set(index, true);
}
continue;
}
}
}
i += 1;
}
}
fn feed_snakes_standard(&mut self) {
if self.constrictor {
for snake in &mut self.snakes {
snake.feed();
}
} else {
let mut eaten_food = vec![];
for i in 0..self.snakes.len() {
let head = self.snakes[i].head();
if self.is_in_bounds(head) {
let head_index = self.coord_to_linear(head);
if self.food[head_index] {
eaten_food.push(head_index);
self.snakes[i].feed();
}
}
}
for food_index in eaten_food {
self.food.set(food_index, false);
}
}
}
fn eliminate_snake_standard(&mut self) {
// eliminate out of health and out of bounds
let mut i = 0;
while i < self.snakes.len() {
let snake = &self.snakes[i];
if snake.health == 0 || !self.is_in_bounds(snake.head()) {
let snake = self.snakes.remove(i);
for tile in snake.body.iter().skip(1) {
if self.is_in_bounds(*tile) {
let index = self.coord_to_linear(*tile);
self.free.set(index, true);
}
}
continue;
}
i += 1;
}
// look for collisions
let mut collisions = vec![];
for snake in &self.snakes {
let head = snake.head();
let head_index = self.coord_to_linear(head);
if !self.free[head_index] {
collisions.push(snake.id);
continue;
}
for snake2 in &self.snakes {
if snake.id != snake2.id
&& snake.head() == snake2.head()
&& snake.body.len() <= snake2.body.len()
{
collisions.push(snake.id);
break;
}
}
}
// apply collisions
let mut i = 0;
while i < self.snakes.len() {
if collisions.contains(&self.snakes[i].id) {
let snake = self.snakes.remove(i);
for tile in snake.body {
let index = self.coord_to_linear(tile);
self.free.set(index, true);
}
continue;
}
i += 1;
}
.filter(move |direction| self.is_free(head.wrapping_apply(*direction)))
}
fn update_free_map(&mut self) {
@ -434,32 +360,14 @@ impl Board {
}
}
fn spawn_food(&mut self) {
let num_food = self.food.count_ones();
let needed_food = if num_food < usize::from(self.min_food) {
usize::from(self.min_food) - num_food
fn get_unoccupied_points(
&self,
include_possible_moves: bool,
include_hazards: bool,
) -> impl Iterator<Item = Coord> + use<'_> {
let possible_moves: Vec<_> = if include_possible_moves {
Vec::new()
} else {
usize::from(
self.food_spawn_chance > 0
&& thread_rng().gen_range(0..100) < self.food_spawn_chance,
)
};
if needed_food == 0 {
return;
}
let food_spots = self
.free
.iter()
.enumerate()
.filter_map(|(i, free)| free.then_some(i))
.filter(|i| {
self.snakes
.iter()
.all(|snake| self.coord_to_linear(snake.tail()) != *i)
})
.filter(|i| {
self.snakes
.iter()
.flat_map(|snake| {
@ -467,20 +375,34 @@ impl Board {
enum_iterator::all::<Direction>()
.map(move |direction| head.wrapping_apply(direction))
.filter(|tile| self.is_in_bounds(*tile))
.map(|tile| self.coord_to_linear(tile))
})
.all(|action| *i != self.coord_to_linear(action))
.collect()
};
self.free
.iter()
.zip(self.food.iter())
.zip(self.hazard.iter())
.enumerate()
.filter(move |(i, ((free, food), hazard))| {
**free
&& !**food
&& (include_hazards || !**hazard)
&& (include_possible_moves || !possible_moves.contains(i))
})
.filter(|i| !self.food[*i])
.choose_multiple(&mut thread_rng(), needed_food);
for index in food_spots {
self.food.set(index, true);
}
.map(|(i, _)| self.linear_to_coord(i))
}
fn coord_to_linear(&self, coord: Coord) -> usize {
usize::from(coord.x) + usize::from(coord.y) * usize::from(self.width)
}
fn linear_to_coord(&self, linear: usize) -> Coord {
let x = (linear % usize::from(self.width)).saturating_as();
let y = (linear / usize::from(self.width)).saturating_as();
Coord { x, y }
}
const fn is_in_bounds(&self, coord: Coord) -> bool {
coord.x < self.width && coord.y < self.height
}

View File

@ -0,0 +1,38 @@
use crate::types::{Direction, simulation::Board, wire};
use super::{
Ruleset,
standard::{DamageHazards, eliminate_snakes, game_over, move_snakes, reduce_snake_health},
};
#[derive(Debug, Clone, Copy)]
pub struct Constrictor {
damage_hazards: DamageHazards,
}
impl From<&wire::Ruleset> for Constrictor {
fn from(value: &wire::Ruleset) -> Self {
Self {
damage_hazards: (&value.settings).into(),
}
}
}
impl Ruleset for Constrictor {
fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool {
let game_over = game_over(board);
move_snakes(board, actions);
reduce_snake_health(board);
self.damage_hazards.execute(board);
eliminate_snakes(board);
grow_snakes(board);
board.update_free_map();
game_over
}
}
fn grow_snakes(board: &mut Board) {
for snake in &mut board.snakes {
snake.feed();
}
}

View File

@ -0,0 +1,37 @@
mod constrictor;
mod solo;
mod standard;
use constrictor::Constrictor;
use enum_dispatch::enum_dispatch;
use solo::Solo;
use standard::Standard;
use crate::types::{Direction, wire};
use super::Board;
#[enum_dispatch]
pub trait Ruleset {
/// executes one turn of the ruleset.
/// Returns true if the game is over
fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool;
}
#[enum_dispatch(Ruleset)]
#[derive(Debug, Clone, Copy)]
pub enum Rulesets {
Standard,
Constrictor,
Solo,
}
impl From<&wire::Ruleset> for Rulesets {
fn from(value: &wire::Ruleset) -> Self {
match value.name.as_ref() {
"solo" => Solo::from(value).into(),
"constrictor" => Constrictor::from(value).into(),
_ => Standard::from(value).into(),
}
}
}

View File

@ -0,0 +1,36 @@
use crate::types::{Direction, simulation::Board, wire};
use super::{
Ruleset,
standard::{DamageHazards, eliminate_snakes, feed_snakes, move_snakes, reduce_snake_health},
};
#[derive(Debug, Clone, Copy)]
pub struct Solo {
damage_hazards: DamageHazards,
}
impl From<&wire::Ruleset> for Solo {
fn from(value: &wire::Ruleset) -> Self {
Self {
damage_hazards: (&value.settings).into(),
}
}
}
impl Ruleset for Solo {
fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool {
let game_over = game_over(board);
move_snakes(board, actions);
reduce_snake_health(board);
self.damage_hazards.execute(board);
feed_snakes(board);
eliminate_snakes(board);
board.update_free_map();
game_over
}
}
pub fn game_over(board: &Board) -> bool {
board.num_snakes() == 0
}

View File

@ -0,0 +1,180 @@
use crate::types::{
Direction,
simulation::Board,
wire::{self, Settings},
};
use super::Ruleset;
#[derive(Debug, Clone, Copy)]
pub struct Standard {
damage_hazards: DamageHazards,
}
impl From<&wire::Ruleset> for Standard {
fn from(value: &wire::Ruleset) -> Self {
Self {
damage_hazards: (&value.settings).into(),
}
}
}
impl Ruleset for Standard {
fn execute(&self, board: &mut Board, actions: &[(u8, Direction)]) -> bool {
let game_over = game_over(board);
move_snakes(board, actions);
reduce_snake_health(board);
self.damage_hazards.execute(board);
feed_snakes(board);
eliminate_snakes(board);
board.update_free_map();
game_over
}
}
pub fn move_snakes(board: &mut Board, actions: &[(u8, Direction)]) {
for i in 0..board.snakes.len() {
let snake = &board.snakes[i];
let action = actions.iter().find(|(id, _)| *id == snake.id).map_or_else(
|| {
let head = snake.body[0];
let previous_head = snake.body[1];
let delta_x = i16::from(head.x) - i16::from(previous_head.x);
let delta_y = i16::from(head.y) - i16::from(previous_head.y);
if delta_x == 0 && delta_y == 0 {
Direction::Up
} else if delta_x.abs() > delta_y.abs() {
if delta_x < 0 {
Direction::Left
} else {
Direction::Right
}
} else if delta_y < 0 {
Direction::Down
} else {
Direction::Up
}
},
|(_, action)| *action,
);
let new_head = snake.head().wrapping_apply(action);
board.snakes[i].advance(new_head);
}
}
pub fn reduce_snake_health(board: &mut Board) {
for snake in &mut board.snakes {
snake.health = snake.health.saturating_sub(1);
}
}
#[derive(Debug, Clone, Copy)]
pub struct DamageHazards {
damage: u8,
}
impl DamageHazards {
pub fn execute(self, board: &mut Board) {
let mut i = 0;
while i < board.snakes.len() {
let head = board.snakes[i].head();
if board.is_in_bounds(head) {
let head_index = board.coord_to_linear(head);
if board.hazard[head_index] && !board.food[head_index] {
let health = &mut board.snakes[i].health;
*health = health.saturating_sub(self.damage);
if *health == 0 {
let snake = board.snakes.remove(i);
for tile in snake.body {
let index = board.coord_to_linear(tile);
board.free.set(index, true);
}
continue;
}
}
}
i += 1;
}
}
}
impl From<&Settings> for DamageHazards {
fn from(value: &Settings) -> Self {
let damage = value.hazard_damage_per_turn;
Self { damage }
}
}
pub fn eliminate_snakes(board: &mut Board) {
// eliminate out of health and out of bounds
let mut i = 0;
while i < board.snakes.len() {
let snake = &board.snakes[i];
if snake.health == 0 || !board.is_in_bounds(snake.head()) {
let snake = board.snakes.remove(i);
for tile in snake.body.iter().skip(1) {
if board.is_in_bounds(*tile) {
let index = board.coord_to_linear(*tile);
board.free.set(index, true);
}
}
continue;
}
i += 1;
}
// look for collisions
let mut collisions = vec![];
for snake in &board.snakes {
let head = snake.head();
let head_index = board.coord_to_linear(head);
if !board.free[head_index] {
collisions.push(snake.id);
continue;
}
for snake2 in &board.snakes {
if snake.id != snake2.id
&& snake.head() == snake2.head()
&& snake.body.len() <= snake2.body.len()
{
collisions.push(snake.id);
break;
}
}
}
// apply collisions
let mut i = 0;
while i < board.snakes.len() {
if collisions.contains(&board.snakes[i].id) {
let snake = board.snakes.remove(i);
for tile in snake.body {
let index = board.coord_to_linear(tile);
board.free.set(index, true);
}
continue;
}
i += 1;
}
}
pub fn feed_snakes(board: &mut Board) {
let mut eaten_food = vec![];
for i in 0..board.snakes.len() {
let head = board.snakes[i].head();
if board.is_in_bounds(head) {
let head_index = board.coord_to_linear(head);
if board.food[head_index] {
eaten_food.push(head_index);
board.snakes[i].feed();
}
}
}
for food_index in eaten_food {
board.food.set(food_index, false);
}
}
pub fn game_over(board: &Board) -> bool {
board.num_snakes() <= 1
}

View File

@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
use super::{Coord, Direction};
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct Request {
/// Game object describing the game being played.
pub game: Game,
@ -16,7 +16,7 @@ pub struct Request {
pub you: Battlesnake,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct Game {
/// A unique identifier for this Game.
pub id: Arc<str>,
@ -36,7 +36,7 @@ pub struct Game {
pub source: Arc<str>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct Ruleset {
/// Name of the ruleset being used to run this game.
pub name: Arc<str>,
@ -47,7 +47,7 @@ pub struct Ruleset {
pub settings: Settings,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Settings {
/// Percentage chance of spawning a new food every round.
@ -61,14 +61,14 @@ pub struct Settings {
pub royale: RoyaleSettings,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RoyaleSettings {
/// The number of turns between generating new hazards (shrinking the safe board space).
pub shrink_every_n_turns: u8,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct Board {
/// The number of rows in the y-axis of the game board.
pub height: u8,
@ -83,7 +83,7 @@ pub struct Board {
pub snakes: Vec<Battlesnake>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct Battlesnake {
/// Unique identifier for this Battlesnake in the context of the current Game.
pub id: Arc<str>,

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "stable"

View File

@ -1,7 +1,8 @@
[package]
name = "xtask"
version = "0.1.0"
edition = "2021"
edition = "2024"
[dependencies]
rayon = "1.10"
rich_progress_bar = "1.1"

View File

@ -4,11 +4,13 @@ use std::{
net::TcpStream,
path::{Path, PathBuf},
process::{Child, Command, Stdio},
sync::Mutex,
thread::sleep,
time::Duration,
};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use rich_progress_bar::{Colors, RichProgressBar};
type DynError = Box<dyn std::error::Error>;
@ -232,9 +234,13 @@ fn regression() -> Result<(), DynError> {
}
};
sleep(Duration::from_secs(1));
let res = try_regression();
snake.kill().and(prod.kill())?;
stop_local_docker()?;
stop_production()?;
let (won, draw, loose) = res?;
let games = won + draw + loose;
println!(
@ -261,22 +267,29 @@ fn try_regression() -> Result<(usize, usize, usize), DynError> {
const GAMES: usize = 100;
// limit the parallelism
rayon::ThreadPoolBuilder::new()
.num_threads(std::thread::available_parallelism()?.get() / 3)
.num_threads(std::thread::available_parallelism()?.get() / 4)
.build_global()
.unwrap();
let mut progress = RichProgressBar::new();
progress
.set_progress_character('=')
.set_color(Colors::BrightCyan)
.set_total(GAMES as u64);
let progress = Mutex::new(&mut progress);
let stats = (0..GAMES)
.into_par_iter()
.flat_map(|_| {
eprint!(".");
let game = Command::new("./battlesnake-cli")
.current_dir(project_root())
.args([
"play",
"-W",
"--width",
"11",
"-H",
"--height",
"11",
"--timeout",
"100",
"--name",
"local",
"--url",
@ -290,7 +303,7 @@ fn try_regression() -> Result<(usize, usize, usize), DynError> {
])
.output()
.ok()?;
if !game.status.success() {
let res = if !game.status.success() {
eprintln!("game output: {}", String::from_utf8(game.stderr).ok()?);
eprintln!("game status: {}", game.status);
None
@ -308,7 +321,11 @@ fn try_regression() -> Result<(usize, usize, usize), DynError> {
} else {
Some((0, 1, 0))
}
};
if let Ok(mut progress) = progress.lock() {
let _ = progress.inc();
}
res
})
.reduce(
|| (0, 0, 0),
@ -331,7 +348,7 @@ fn run_snake(port: u16, log: Option<&str>) -> Result<Child, DynError> {
}
let mut snake = snake
.args(
["run", "--bin", "battlesnake"]
["run", "--bin", "battlesnake", "--release"]
.map(str::to_string)
.into_iter()
.chain(env::args().skip(2)),
@ -366,7 +383,7 @@ fn run_local_docker(port: u16) -> Result<Child, DynError> {
"--env",
"RUST_LOG=error",
"--env",
format!("ROCKET_PORT={}", port).as_str(),
format!("PORT={}", port).as_str(),
"--network=host",
"--rm",
"local_snake",
@ -386,6 +403,13 @@ fn run_local_docker(port: u16) -> Result<Child, DynError> {
}
}
fn stop_local_docker() -> Result<(), DynError> {
Command::new("docker")
.args(["stop", "battlesnake-regression-local"])
.status()?;
Ok(())
}
fn run_production(port: u16) -> Result<Child, DynError> {
let mut snake = Command::new("docker")
.args([
@ -396,7 +420,7 @@ fn run_production(port: u16) -> Result<Child, DynError> {
"--env",
"RUST_LOG=error",
"--env",
format!("ROCKET_PORT={}", port).as_str(),
format!("PORT={}", port).as_str(),
"--network=host",
"--rm",
"docker.mkaenner.de/snake:latest",
@ -416,6 +440,13 @@ fn run_production(port: u16) -> Result<Child, DynError> {
}
}
fn stop_production() -> Result<(), DynError> {
Command::new("docker")
.args(["stop", "battlesnake-regression-production"])
.status()?;
Ok(())
}
fn docker() -> Result<(), DynError> {
if !Command::new("docker")
.current_dir(project_root())