Compare commits
54 Commits
f3d7c3160d
...
main
Author | SHA1 | Date | |
---|---|---|---|
f0156ccec4 | |||
2c2bfd3489 | |||
263b0642f4 | |||
081c41b753 | |||
549d4fe36d | |||
6f54282c9a | |||
6f3a1d138a | |||
0876f1ed3c | |||
87fc6cccd2 | |||
2f87e2aa60 | |||
5f1d3dfc4f | |||
d44538b749 | |||
b4b332bdbb | |||
b97d7c895a | |||
e5600fe038 | |||
15d90357ec | |||
99acd4ad1f | |||
bad4d916b8 | |||
caa6eed783 | |||
9333f6c6fb | |||
1d527a89cd | |||
c5097ec417 | |||
879f99e23f | |||
302f5cac50 | |||
d715aa47b3 | |||
6f04d7cb7f | |||
3f91660583 | |||
32883b2a1b | |||
c26da8869c | |||
d3abaf61a7 | |||
d46e2d8163 | |||
5b440bc7db | |||
d858d88e76 | |||
3a163fd6e7 | |||
108aeaa49d | |||
ea96a0eb0d | |||
47f75563ae | |||
5bb7476af6 | |||
2f2b7ac11e | |||
1410602c6c | |||
5e81a7952f | |||
9c76df1f69 | |||
d91d0e0a82 | |||
f9e34d119a | |||
d9ee6ae2f7 | |||
f3eba2ba75 | |||
932023451a | |||
7c79c718d0 | |||
923b661583 | |||
b2364a1ade | |||
7227f1776f | |||
2b0b97cba8 | |||
55050d5451 | |||
8895f875b2 |
@ -1,5 +1,2 @@
|
|||||||
[alias]
|
[alias]
|
||||||
xtask = "run --package xtask --"
|
xtask = "run --package xtask --"
|
||||||
|
|
||||||
[build]
|
|
||||||
rustflags = "-Ctarget-cpu=core-avx2"
|
|
||||||
|
17
.github/workflows/build_docker.yaml
vendored
Normal file
17
.github/workflows/build_docker.yaml
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
name: Build
|
||||||
|
run-name: ${{ gitea.actor }} is runs ci pipeline
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
- name: Build Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
push: false
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
target
|
target
|
||||||
|
assets
|
||||||
|
games
|
||||||
|
2426
Cargo.lock
generated
2426
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,9 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["battlesnake", "xtask"]
|
members = ["battlesnake", "xtask"]
|
||||||
|
resolver = "3"
|
||||||
|
|
||||||
resolver = "2"
|
default-members = ["battlesnake"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
lto = "fat"
|
||||||
|
codegen-units = 1
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
FROM rust:1.81-bookworm as build
|
FROM rust:1.86-bullseye AS build
|
||||||
|
|
||||||
COPY battlesnake/ /usr/app
|
COPY battlesnake/ /usr/app
|
||||||
WORKDIR /usr/app
|
WORKDIR /usr/app
|
||||||
|
|
||||||
RUN cargo install --path .
|
RUN cargo install --path .
|
||||||
|
|
||||||
FROM debian:bookworm
|
FROM debian:bullseye-slim
|
||||||
COPY --from=build /usr/local/cargo/bin/battlesnake /bin/battlesnake
|
COPY --from=build /usr/local/cargo/bin/battlesnake /bin/battlesnake
|
||||||
|
|
||||||
CMD ["battlesnake"]
|
CMD ["battlesnake"]
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
authors = ["Max Känner"]
|
authors = ["Max Känner"]
|
||||||
name = "battlesnake"
|
name = "battlesnake"
|
||||||
version = "1.0.0"
|
version = "2.0.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
rust-version = "1.86"
|
||||||
|
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://git.mkaenner.de/max/battlesnake"
|
repository = "https://git.mkaenner.de/max/battlesnake"
|
||||||
@ -10,31 +11,54 @@ keywords = ["battlesnake"]
|
|||||||
description = """
|
description = """
|
||||||
A simple Battlesnake written in Rust
|
A simple Battlesnake written in Rust
|
||||||
"""
|
"""
|
||||||
|
default-run = "battlesnake"
|
||||||
|
|
||||||
[lints.clippy]
|
[lints.clippy]
|
||||||
pedantic = "warn"
|
pedantic = "warn"
|
||||||
nursery = "warn"
|
nursery = "warn"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "battlesnake"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "seed-cracker"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "generate"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rocket = { version = "0.5.0", features = ["json"] }
|
# server
|
||||||
serde = { version = "1.0.117", features = ["derive"] }
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
serde_json = "1.0.59"
|
axum = { version = "0.8", features = ["http2", "multipart", "ws"] }
|
||||||
log = "0.4.0"
|
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||||
env_logger = "0.11.5"
|
|
||||||
rand = "0.8.4"
|
# logging
|
||||||
|
log = "0.4"
|
||||||
|
env_logger = "0.11"
|
||||||
|
|
||||||
|
# other
|
||||||
|
bitvec = "1.0"
|
||||||
enum-iterator = "2.1"
|
enum-iterator = "2.1"
|
||||||
iter_tools = "0.21"
|
rand = "0.9"
|
||||||
ordered-float = "4.3.0"
|
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]
|
[dev-dependencies]
|
||||||
# criterion = { version = "0.5.1", features = ["html_reports"] }
|
criterion = "0.5"
|
||||||
criterion2 = "1.1.1"
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
lto = "fat"
|
|
||||||
codegen-units = 1
|
|
||||||
panic = "abort"
|
|
||||||
strip = true
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "simulation"
|
name = "simulation"
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
[default]
|
|
||||||
address = "0.0.0.0"
|
|
||||||
port = 8000
|
|
||||||
keep_alive = 0
|
|
@ -1,120 +1,229 @@
|
|||||||
use battlesnake::{
|
use std::{
|
||||||
simulation::{self, SnakeToken},
|
hint::black_box,
|
||||||
Coord,
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicU32, Ordering},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
||||||
use rand::{rngs::StdRng, SeedableRng};
|
|
||||||
|
|
||||||
fn random_moves(board: &battlesnake::Board) -> Option<SnakeToken> {
|
use criterion::{Bencher, BenchmarkId, Criterion, criterion_group, criterion_main};
|
||||||
let token_map = SnakeToken::from_board(board);
|
|
||||||
let mut board = simulation::Board::from_game_board(board, &token_map, 0, 15, 1, false);
|
|
||||||
let mut rng = StdRng::seed_from_u64(0);
|
|
||||||
|
|
||||||
board.simulate_until(&mut rng, |board| board.alive_snakes() <= 1);
|
use battlesnake::types::{
|
||||||
|
Coord,
|
||||||
|
simulation::Game as Board,
|
||||||
|
wire::{Battlesnake, Board as WireBoard, Game, Request, RoyaleSettings, Ruleset, Settings},
|
||||||
|
};
|
||||||
|
use rand::{SeedableRng, rngs::SmallRng};
|
||||||
|
|
||||||
let winner = board.snakes().next();
|
fn create_start_snake(coord: Coord) -> Battlesnake {
|
||||||
winner
|
let id: Arc<str> = format!("{coord:?}").into();
|
||||||
|
Battlesnake {
|
||||||
|
id: id.clone(),
|
||||||
|
name: id.clone(),
|
||||||
|
health: 100,
|
||||||
|
body: vec![coord; 3],
|
||||||
|
latency: "0".into(),
|
||||||
|
head: coord,
|
||||||
|
length: 3,
|
||||||
|
shout: None,
|
||||||
|
squad: id,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bench_duel_random_moves(c: &mut Criterion) {
|
fn create_standard_start_request(starts: [Coord; 4]) -> Request {
|
||||||
c.bench_function("duel random moves", |b| {
|
Request {
|
||||||
|
game: Game {
|
||||||
|
id: "test".into(),
|
||||||
|
ruleset: Ruleset {
|
||||||
|
name: "standard".into(),
|
||||||
|
version: "0".into(),
|
||||||
|
settings: Settings {
|
||||||
|
food_spawn_chance: 15,
|
||||||
|
minimum_food: 1,
|
||||||
|
hazard_damage_per_turn: 30,
|
||||||
|
royale: RoyaleSettings {
|
||||||
|
shrink_every_n_turns: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
map: "standard".into(),
|
||||||
|
timeout: 500,
|
||||||
|
source: "other".into(),
|
||||||
|
},
|
||||||
|
turn: 0,
|
||||||
|
board: WireBoard {
|
||||||
|
height: 11,
|
||||||
|
width: 11,
|
||||||
|
food: vec![Coord { x: 5, y: 5 }],
|
||||||
|
hazards: vec![],
|
||||||
|
snakes: vec![
|
||||||
|
create_start_snake(starts[0]),
|
||||||
|
create_start_snake(starts[1]),
|
||||||
|
create_start_snake(starts[2]),
|
||||||
|
create_start_snake(starts[3]),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
you: create_start_snake(starts[0]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn standard(c: &mut Criterion) {
|
||||||
|
let turns_min = AtomicU32::new(u32::MAX);
|
||||||
|
let turns_max = AtomicU32::new(u32::MIN);
|
||||||
|
let turns_sum = AtomicU32::new(0);
|
||||||
|
let turns_total = AtomicU32::new(0);
|
||||||
|
let mut group = c.benchmark_group("standard");
|
||||||
|
group.sample_size(10000);
|
||||||
|
|
||||||
|
let benchmark = |b: &mut Bencher, board: &Board| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
random_moves(black_box(&battlesnake::Board {
|
let mut board = board.clone();
|
||||||
height: 11,
|
board.simulate_random(&mut SmallRng::from_os_rng());
|
||||||
width: 11,
|
let turn = board.board.turn();
|
||||||
food: vec![Coord { x: 5, y: 5 }],
|
|
||||||
snakes: vec![
|
if turn < turns_min.load(Ordering::Relaxed) {
|
||||||
battlesnake::Battlesnake {
|
turns_min.store(turn, Ordering::Relaxed);
|
||||||
id: "1".to_owned(),
|
}
|
||||||
name: "1".to_owned(),
|
if turn > turns_max.load(Ordering::Relaxed) {
|
||||||
health: 100,
|
turns_max.store(turn, Ordering::Relaxed);
|
||||||
body: vec![Coord { x: 5, y: 1 }; 3],
|
}
|
||||||
head: Coord { x: 5, y: 1 },
|
turns_sum.fetch_add(turn, Ordering::Relaxed);
|
||||||
length: 3,
|
turns_total.fetch_add(1, Ordering::Relaxed);
|
||||||
latency: "0".to_owned(),
|
|
||||||
shout: None,
|
|
||||||
squad: String::new(),
|
|
||||||
},
|
|
||||||
battlesnake::Battlesnake {
|
|
||||||
id: "2".to_owned(),
|
|
||||||
name: "2".to_owned(),
|
|
||||||
health: 100,
|
|
||||||
body: vec![Coord { x: 5, y: 9 }; 3],
|
|
||||||
head: Coord { x: 5, y: 9 },
|
|
||||||
length: 3,
|
|
||||||
latency: "0".to_owned(),
|
|
||||||
shout: None,
|
|
||||||
squad: String::new(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hazards: vec![],
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
let request = create_standard_start_request([
|
||||||
|
Coord { x: 1, y: 1 },
|
||||||
|
Coord { x: 9, y: 1 },
|
||||||
|
Coord { x: 1, y: 9 },
|
||||||
|
Coord { x: 9, y: 9 },
|
||||||
|
]);
|
||||||
|
let board = Board::from(&request);
|
||||||
|
group.bench_with_input(
|
||||||
|
BenchmarkId::from_parameter("start x"),
|
||||||
|
black_box(&board),
|
||||||
|
benchmark,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let max = turns_max.load(Ordering::Relaxed);
|
||||||
|
let min = turns_min.load(Ordering::Relaxed);
|
||||||
|
let sum = turns_sum.load(Ordering::Relaxed);
|
||||||
|
let total = turns_total.load(Ordering::Relaxed);
|
||||||
|
let avg = sum / total;
|
||||||
|
println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
|
||||||
|
turns_max.store(u32::MIN, Ordering::Relaxed);
|
||||||
|
turns_min.store(u32::MAX, Ordering::Relaxed);
|
||||||
|
turns_sum.store(0, Ordering::Relaxed);
|
||||||
|
turns_total.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = create_standard_start_request([
|
||||||
|
Coord { x: 5, y: 1 },
|
||||||
|
Coord { x: 1, y: 5 },
|
||||||
|
Coord { x: 5, y: 9 },
|
||||||
|
Coord { x: 9, y: 5 },
|
||||||
|
]);
|
||||||
|
let board = Board::from(&request);
|
||||||
|
group.bench_with_input(
|
||||||
|
BenchmarkId::from_parameter("start +"),
|
||||||
|
black_box(&board),
|
||||||
|
benchmark,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let max = turns_max.load(Ordering::Relaxed);
|
||||||
|
let min = turns_min.load(Ordering::Relaxed);
|
||||||
|
let sum = turns_sum.load(Ordering::Relaxed);
|
||||||
|
let total = turns_total.load(Ordering::Relaxed);
|
||||||
|
let avg = sum / total;
|
||||||
|
println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bench_standard_random_moves(c: &mut Criterion) {
|
fn constrictor(c: &mut Criterion) {
|
||||||
c.bench_function("standard random moves", |b| {
|
let turns_min = AtomicU32::new(u32::MAX);
|
||||||
|
let turns_max = AtomicU32::new(u32::MIN);
|
||||||
|
let turns_sum = AtomicU32::new(0);
|
||||||
|
let turns_total = AtomicU32::new(0);
|
||||||
|
let mut group = c.benchmark_group("constrictor");
|
||||||
|
group.sample_size(10000);
|
||||||
|
|
||||||
|
let benchmark = |b: &mut Bencher, board: &Board| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
random_moves(black_box(&battlesnake::Board {
|
let mut board = board.clone();
|
||||||
height: 11,
|
board.simulate_random(&mut SmallRng::from_os_rng());
|
||||||
width: 11,
|
let turn = board.board.turn();
|
||||||
food: vec![Coord { x: 5, y: 5 }],
|
|
||||||
snakes: vec![
|
if turn < turns_min.load(Ordering::Relaxed) {
|
||||||
battlesnake::Battlesnake {
|
turns_min.store(turn, Ordering::Relaxed);
|
||||||
id: "1".to_owned(),
|
}
|
||||||
name: "1".to_owned(),
|
if turn > turns_max.load(Ordering::Relaxed) {
|
||||||
health: 100,
|
turns_max.store(turn, Ordering::Relaxed);
|
||||||
body: vec![Coord { x: 5, y: 1 }; 3],
|
}
|
||||||
head: Coord { x: 5, y: 1 },
|
turns_sum.fetch_add(turn, Ordering::Relaxed);
|
||||||
length: 3,
|
turns_total.fetch_add(1, Ordering::Relaxed);
|
||||||
latency: "0".to_owned(),
|
|
||||||
shout: None,
|
|
||||||
squad: String::new(),
|
|
||||||
},
|
|
||||||
battlesnake::Battlesnake {
|
|
||||||
id: "2".to_owned(),
|
|
||||||
name: "2".to_owned(),
|
|
||||||
health: 100,
|
|
||||||
body: vec![Coord { x: 5, y: 9 }; 3],
|
|
||||||
head: Coord { x: 5, y: 9 },
|
|
||||||
length: 3,
|
|
||||||
latency: "0".to_owned(),
|
|
||||||
shout: None,
|
|
||||||
squad: String::new(),
|
|
||||||
},
|
|
||||||
battlesnake::Battlesnake {
|
|
||||||
id: "3".to_owned(),
|
|
||||||
name: "3".to_owned(),
|
|
||||||
health: 100,
|
|
||||||
body: vec![Coord { x: 1, y: 5 }; 3],
|
|
||||||
head: Coord { x: 1, y: 5 },
|
|
||||||
length: 3,
|
|
||||||
latency: "0".to_owned(),
|
|
||||||
shout: None,
|
|
||||||
squad: String::new(),
|
|
||||||
},
|
|
||||||
battlesnake::Battlesnake {
|
|
||||||
id: "4".to_owned(),
|
|
||||||
name: "4".to_owned(),
|
|
||||||
health: 100,
|
|
||||||
body: vec![Coord { x: 9, y: 5 }; 3],
|
|
||||||
head: Coord { x: 9, y: 5 },
|
|
||||||
length: 3,
|
|
||||||
latency: "0".to_owned(),
|
|
||||||
shout: None,
|
|
||||||
squad: String::new(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
hazards: vec![],
|
|
||||||
}))
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
|
let mut request = create_standard_start_request([
|
||||||
|
Coord { x: 1, y: 1 },
|
||||||
|
Coord { x: 9, y: 1 },
|
||||||
|
Coord { x: 1, y: 9 },
|
||||||
|
Coord { x: 9, y: 9 },
|
||||||
|
]);
|
||||||
|
request.game.ruleset.name = "constrictor".into();
|
||||||
|
let board = Board::from(&request);
|
||||||
|
group.bench_with_input(
|
||||||
|
BenchmarkId::from_parameter("start x"),
|
||||||
|
black_box(&board),
|
||||||
|
benchmark,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let max = turns_max.load(Ordering::Relaxed);
|
||||||
|
let min = turns_min.load(Ordering::Relaxed);
|
||||||
|
let sum = turns_sum.load(Ordering::Relaxed);
|
||||||
|
let total = turns_total.load(Ordering::Relaxed);
|
||||||
|
let avg = sum / total;
|
||||||
|
println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
|
||||||
|
turns_max.store(u32::MIN, Ordering::Relaxed);
|
||||||
|
turns_min.store(u32::MAX, Ordering::Relaxed);
|
||||||
|
turns_sum.store(0, Ordering::Relaxed);
|
||||||
|
turns_total.store(0, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut request = create_standard_start_request([
|
||||||
|
Coord { x: 5, y: 1 },
|
||||||
|
Coord { x: 1, y: 5 },
|
||||||
|
Coord { x: 5, y: 9 },
|
||||||
|
Coord { x: 9, y: 5 },
|
||||||
|
]);
|
||||||
|
request.game.ruleset.name = "constrictor".into();
|
||||||
|
let board = Board::from(&request);
|
||||||
|
group.bench_with_input(
|
||||||
|
BenchmarkId::from_parameter("start +"),
|
||||||
|
black_box(&board),
|
||||||
|
benchmark,
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let max = turns_max.load(Ordering::Relaxed);
|
||||||
|
let min = turns_min.load(Ordering::Relaxed);
|
||||||
|
let sum = turns_sum.load(Ordering::Relaxed);
|
||||||
|
let total = turns_total.load(Ordering::Relaxed);
|
||||||
|
let avg = sum / total;
|
||||||
|
println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
criterion_group!(
|
fn clone(c: &mut Criterion) {
|
||||||
benches,
|
let request = create_standard_start_request([
|
||||||
bench_duel_random_moves,
|
Coord { x: 1, y: 1 },
|
||||||
bench_standard_random_moves
|
Coord { x: 9, y: 1 },
|
||||||
);
|
Coord { x: 1, y: 9 },
|
||||||
|
Coord { x: 9, y: 9 },
|
||||||
|
]);
|
||||||
|
let board = Board::from(&request);
|
||||||
|
|
||||||
|
c.bench_function("clone", |b| b.iter(|| board.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, standard, constrictor, clone);
|
||||||
criterion_main!(benches);
|
criterion_main!(benches);
|
||||||
|
47
battlesnake/src/bin/generate.rs
Normal file
47
battlesnake/src/bin/generate.rs
Normal 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 }
|
||||||
|
}
|
1470
battlesnake/src/bin/seed-cracker.rs
Normal file
1470
battlesnake/src/bin/seed-cracker.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,183 +1 @@
|
|||||||
use enum_iterator::Sequence;
|
pub mod types;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub mod logic;
|
|
||||||
pub mod simulation;
|
|
||||||
|
|
||||||
pub const MAX_HEALTH: i32 = 100;
|
|
||||||
|
|
||||||
// API and Response Objects
|
|
||||||
// See https://docs.battlesnake.com/api
|
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Deserialize, Serialize, Sequence,
|
|
||||||
)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum Direction {
|
|
||||||
/// Move left (-x)
|
|
||||||
Left,
|
|
||||||
/// Move up (+y)
|
|
||||||
Up,
|
|
||||||
/// Move right (+x)
|
|
||||||
Right,
|
|
||||||
/// Move down (-y)
|
|
||||||
Down,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Action {
|
|
||||||
/// In which direction the snake should move
|
|
||||||
pub r#move: Direction,
|
|
||||||
/// Say something to the other snakes
|
|
||||||
#[serde(default, skip_serializing_if = "is_default")]
|
|
||||||
pub shout: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_default<T: Default + PartialEq>(value: &T) -> bool {
|
|
||||||
*value == T::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
|
||||||
pub struct Game {
|
|
||||||
/// A unique identifier for this Game
|
|
||||||
pub id: String,
|
|
||||||
/// Information about the ruleset being used to run this game
|
|
||||||
pub ruleset: Ruleset,
|
|
||||||
/// The name of the map being played on.
|
|
||||||
pub map: String,
|
|
||||||
/// How much time your snake has to respond to requests for this Game
|
|
||||||
pub timeout: u32,
|
|
||||||
/// The source of this game.
|
|
||||||
///
|
|
||||||
/// One of:
|
|
||||||
/// - "tournament"
|
|
||||||
/// - "league"
|
|
||||||
/// - "arena"
|
|
||||||
/// - "challenge"
|
|
||||||
/// - "custom"
|
|
||||||
///
|
|
||||||
/// The values may change.
|
|
||||||
pub source: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct Ruleset {
|
|
||||||
/// Name of the ruleset being used to run this game.
|
|
||||||
pub name: String,
|
|
||||||
/// The release version of the [Rules](https://github.com/BattlesnakeOfficial/rules) module used in this game.
|
|
||||||
pub version: String,
|
|
||||||
/// A collection of specific settings being used by the current game that control how the rules
|
|
||||||
/// are applied.
|
|
||||||
pub settings: RulesetSettings,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct RulesetSettings {
|
|
||||||
/// Percentage chance of spawning a new food every round.
|
|
||||||
#[serde(rename = "foodSpawnChance")]
|
|
||||||
pub food_spawn_chance: u8,
|
|
||||||
/// Minimum food to keep on the board every turn.
|
|
||||||
#[serde(rename = "minimumFood")]
|
|
||||||
pub minimum_food: u8,
|
|
||||||
/// Health damage a snake will take when ending its turn in a hazard. This stacks on top of the
|
|
||||||
/// regular 1 damage a snake takes per turn.
|
|
||||||
#[serde(rename = "hazardDamagePerTurn")]
|
|
||||||
pub hazard_damage_per_turn: u8,
|
|
||||||
/// rules for the royale mode
|
|
||||||
pub royale: RulesetRoyale,
|
|
||||||
/// rules for the squad mode
|
|
||||||
pub squad: RulesetSquad,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct RulesetRoyale {
|
|
||||||
/// The number of turns between generating new hazards (shrinking the safe board space).
|
|
||||||
#[serde(rename = "shrinkEveryNTurns")]
|
|
||||||
pub shrink_every_n_turns: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
pub struct RulesetSquad {
|
|
||||||
/// Allow members of the same squad to move over each other without dying.
|
|
||||||
#[serde(rename = "allowBodyCollisions")]
|
|
||||||
pub allow_body_collisions: bool,
|
|
||||||
/// All squad members are eliminated when one is eliminated.
|
|
||||||
#[serde(rename = "sharedElimination")]
|
|
||||||
pub shared_elimination: bool,
|
|
||||||
/// All squad members share health.
|
|
||||||
#[serde(rename = "sharedHealth")]
|
|
||||||
pub shared_health: bool,
|
|
||||||
/// All squad members share length.
|
|
||||||
#[serde(rename = "sharedLength")]
|
|
||||||
pub shared_length: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
pub struct Board {
|
|
||||||
/// The number of rows in the y-axis of the game board.
|
|
||||||
pub height: i32,
|
|
||||||
/// The number of columns in the x-axis of the game board.
|
|
||||||
pub width: i32,
|
|
||||||
/// Array of coordinates representing food locations on the game board.
|
|
||||||
pub food: Vec<Coord>,
|
|
||||||
/// Array of Battlesnakes representing all Battlesnakes remaining on the game board (including
|
|
||||||
/// yourself if you haven't been eliminated).
|
|
||||||
pub snakes: Vec<Battlesnake>,
|
|
||||||
/// Array of coordinates representing hazardous locations on the game board.
|
|
||||||
pub hazards: Vec<Coord>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
||||||
pub struct Battlesnake {
|
|
||||||
/// Unique identifier for this Battlesnake in the context of the current Game
|
|
||||||
pub id: String,
|
|
||||||
/// Name given to this Battlesnake by its author
|
|
||||||
pub name: String,
|
|
||||||
/// Health value of this Battlesnake, between 0 and 100
|
|
||||||
pub health: i32,
|
|
||||||
/// Array of coordinates representing this Battlesnake's location on the game board. This array
|
|
||||||
/// is ordered from head to tail
|
|
||||||
pub body: Vec<Coord>,
|
|
||||||
/// Coordinates for this Battlesnake's head. Equivalent to the first element of the body array.
|
|
||||||
pub head: Coord,
|
|
||||||
/// Length of this Battlesnake from head to tail. Equivalent to the length of the body array.
|
|
||||||
pub length: i32,
|
|
||||||
/// The previous response time of this Battlesnake, in milliseconds. If the Battlesnake timed
|
|
||||||
/// out and failed to respond, the game timeout will be returned.
|
|
||||||
pub latency: String,
|
|
||||||
/// Message shouted by this Battlesnake on the previous turn.
|
|
||||||
pub shout: Option<String>,
|
|
||||||
/// The squad that the Battlesnake belongs to. Used to identify squad members in Squad Mode
|
|
||||||
/// games.
|
|
||||||
pub squad: String,
|
|
||||||
// /// The collection of customizations that control how this Battlesnake is displayed.
|
|
||||||
// customizations: {color, head, tail}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Deserialize, Serialize)]
|
|
||||||
pub struct Coord {
|
|
||||||
pub x: i32,
|
|
||||||
pub y: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Coord {
|
|
||||||
#[must_use]
|
|
||||||
pub const fn move_to(mut self, direction: Direction) -> Self {
|
|
||||||
match direction {
|
|
||||||
Direction::Left => self.x -= 1,
|
|
||||||
Direction::Up => self.y += 1,
|
|
||||||
Direction::Right => self.x += 1,
|
|
||||||
Direction::Down => self.y -= 1,
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
|
||||||
pub struct GameState {
|
|
||||||
pub game: Game,
|
|
||||||
pub turn: i32,
|
|
||||||
pub board: Board,
|
|
||||||
pub you: Battlesnake,
|
|
||||||
}
|
|
||||||
|
@ -1,439 +0,0 @@
|
|||||||
// 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 core::f64;
|
|
||||||
use std::{
|
|
||||||
collections::{BTreeMap, HashMap},
|
|
||||||
sync::{Arc, LazyLock, Mutex},
|
|
||||||
time::{Duration, Instant},
|
|
||||||
};
|
|
||||||
|
|
||||||
use log::{error, info, warn};
|
|
||||||
use ordered_float::OrderedFloat;
|
|
||||||
use rand::{prelude::*, thread_rng};
|
|
||||||
use serde_json::{json, Value};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
simulation::{self, SnakeToken},
|
|
||||||
Action, Battlesnake, Board, Direction, 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
|
|
||||||
#[must_use]
|
|
||||||
pub fn info() -> Value {
|
|
||||||
info!("INFO");
|
|
||||||
|
|
||||||
json!({
|
|
||||||
"apiversion": "1",
|
|
||||||
"author": "der-informatiker",
|
|
||||||
"color": "#00FFEE",
|
|
||||||
"head": "smart-caterpillar",
|
|
||||||
"tail": "mouse",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
struct GameInfo {
|
|
||||||
calculation_time: Arc<Mutex<Duration>>,
|
|
||||||
token_mapping: Arc<BTreeMap<String, SnakeToken>>,
|
|
||||||
my_token: SnakeToken,
|
|
||||||
tree: Arc<Mutex<Option<Node>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
static GAME_INFOS: LazyLock<Mutex<HashMap<(String, String), GameInfo>>> =
|
|
||||||
LazyLock::new(|| Mutex::new(HashMap::new()));
|
|
||||||
|
|
||||||
// start is called when your Battlesnake begins a game
|
|
||||||
pub fn start(game: &Game, _turn: i32, board: &Board, you: &Battlesnake) {
|
|
||||||
info!("GAME START");
|
|
||||||
let token_mapping = Arc::new(SnakeToken::from_board(board));
|
|
||||||
let my_token = token_mapping[&you.id];
|
|
||||||
let Ok(mut game_infos) = GAME_INFOS.lock() else {
|
|
||||||
error!("unable to lock game infos");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
game_infos.insert(
|
|
||||||
(game.id.clone(), you.id.clone()),
|
|
||||||
GameInfo {
|
|
||||||
calculation_time: Arc::new(Mutex::new(Duration::from_millis(
|
|
||||||
u64::from(game.timeout) / 2,
|
|
||||||
))),
|
|
||||||
token_mapping,
|
|
||||||
my_token,
|
|
||||||
tree: Arc::new(Mutex::new(None)),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// end is called when your Battlesnake finishes a game
|
|
||||||
pub fn end(game: &Game, turn: i32, _board: &Board, you: &Battlesnake) {
|
|
||||||
info!("GAME OVER after {turn} turns");
|
|
||||||
let Ok(mut game_infos) = GAME_INFOS.lock() else {
|
|
||||||
error!("unable to lock game infos");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
game_infos.remove(&(game.id.clone(), you.id.clone()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
start: &Instant,
|
|
||||||
) -> Option<Action> {
|
|
||||||
let calc_start = Instant::now();
|
|
||||||
if calc_start - *start > Duration::from_millis(10) {
|
|
||||||
error!(
|
|
||||||
"The calculation was started long after the request ({}ms)",
|
|
||||||
(calc_start - *start).as_millis()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let game_info = GAME_INFOS
|
|
||||||
.lock()
|
|
||||||
.ok()
|
|
||||||
.and_then(|guard| guard.get(&(game.id.clone(), you.id.clone())).cloned())
|
|
||||||
.unwrap_or_else(|| {
|
|
||||||
let token_mapping = Arc::new(SnakeToken::from_board(board));
|
|
||||||
let my_token = token_mapping[&you.id];
|
|
||||||
GameInfo {
|
|
||||||
calculation_time: Arc::new(Mutex::new(Duration::from_millis(
|
|
||||||
u64::from(game.timeout) / 2,
|
|
||||||
))),
|
|
||||||
token_mapping,
|
|
||||||
my_token,
|
|
||||||
tree: Arc::new(Mutex::new(None)),
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let board = simulation::Board::from_game_board(
|
|
||||||
board,
|
|
||||||
&game_info.token_mapping,
|
|
||||||
turn,
|
|
||||||
game.ruleset.settings.food_spawn_chance,
|
|
||||||
game.ruleset.settings.minimum_food,
|
|
||||||
game.ruleset.name == "constrictor",
|
|
||||||
);
|
|
||||||
let possible_actions = board.possible_actions().get(&game_info.my_token).cloned()?;
|
|
||||||
if possible_actions.is_empty() {
|
|
||||||
info!("No movement options in turn {turn}");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// do some latency compensation
|
|
||||||
let deadline = *start
|
|
||||||
+ game_info.calculation_time.lock().map_or_else(
|
|
||||||
|_| Duration::from_millis(u64::from(game.timeout) / 2),
|
|
||||||
|mut guard| {
|
|
||||||
let target_latency = game.timeout / 2;
|
|
||||||
let latency = you.latency.parse().unwrap_or_else(|e| {
|
|
||||||
warn!("Unable to parse latency: {e}");
|
|
||||||
target_latency
|
|
||||||
});
|
|
||||||
let last_computation_time = u32::try_from(guard.as_millis()).unwrap_or(0);
|
|
||||||
let computation_time =
|
|
||||||
(last_computation_time + target_latency).saturating_sub(latency);
|
|
||||||
*guard =
|
|
||||||
Duration::from_millis(u64::from(computation_time.clamp(1, target_latency)));
|
|
||||||
*guard
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut tree_guard = game_info.tree.lock();
|
|
||||||
let tree = match tree_guard {
|
|
||||||
Err(ref e) => {
|
|
||||||
error!("unable to lock tree: {e}");
|
|
||||||
None
|
|
||||||
}
|
|
||||||
Ok(ref mut guard) => guard.as_mut(),
|
|
||||||
};
|
|
||||||
let mut tree = tree
|
|
||||||
.and_then(|node| {
|
|
||||||
let snake_length_direction: BTreeMap<_, _> = board
|
|
||||||
.snakes()
|
|
||||||
.map(|snake| {
|
|
||||||
let length = board.snake_length(snake).unwrap_or_default();
|
|
||||||
let action = board.last_action(snake).unwrap_or(Direction::Up);
|
|
||||||
(snake, (action, length))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
let node_key = node
|
|
||||||
.childs
|
|
||||||
.keys()
|
|
||||||
.find(|child| {
|
|
||||||
child.iter().all(|(snake, (direction, length))| {
|
|
||||||
snake_length_direction
|
|
||||||
.get(snake)
|
|
||||||
.copied()
|
|
||||||
.unwrap_or((*direction, 0))
|
|
||||||
== (*direction, *length)
|
|
||||||
})
|
|
||||||
})?
|
|
||||||
.clone();
|
|
||||||
let node = node.childs.remove(&node_key)?;
|
|
||||||
info!(
|
|
||||||
"using previous node with {} simulations",
|
|
||||||
node.statistic.played
|
|
||||||
);
|
|
||||||
Some(node)
|
|
||||||
})
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
while Instant::now() < deadline {
|
|
||||||
let mut board = board.clone();
|
|
||||||
if game.ruleset.name == "solo" {
|
|
||||||
let _ = tree.monte_carlo_step_solo(&mut board, &deadline);
|
|
||||||
} else {
|
|
||||||
let _ = tree.monte_carlo_step(&mut board, &deadline);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let actions = tree.child_statistics.entry(game_info.my_token).or_default();
|
|
||||||
|
|
||||||
info!("actions {}: {actions:?}", you.name);
|
|
||||||
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
let chosen = actions
|
|
||||||
.iter()
|
|
||||||
.max_by_key(|(_, stat)| OrderedFloat(stat.won as f64 / stat.played as f64))
|
|
||||||
.map(|(direction, _)| *direction)
|
|
||||||
.or_else(|| possible_actions.iter().choose(&mut thread_rng()).copied())?;
|
|
||||||
|
|
||||||
if let Ok(ref mut guard) = tree_guard {
|
|
||||||
**guard = Some(tree);
|
|
||||||
}
|
|
||||||
std::mem::drop(tree_guard);
|
|
||||||
|
|
||||||
info!(
|
|
||||||
"DIRECTION {turn}: {chosen:?} after {}ms ({})",
|
|
||||||
start.elapsed().as_millis(),
|
|
||||||
you.name,
|
|
||||||
);
|
|
||||||
Some(Action {
|
|
||||||
r#move: chosen,
|
|
||||||
shout: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
|
||||||
struct Statistics {
|
|
||||||
/// Number of times this node was simulated
|
|
||||||
played: usize,
|
|
||||||
/// Number of times this node was simulated and the agent has won.
|
|
||||||
won: BTreeMap<SnakeToken, usize>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
|
||||||
struct ActionStatistic {
|
|
||||||
played: usize,
|
|
||||||
won: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DeadlineError;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
|
||||||
struct Node {
|
|
||||||
statistic: Statistics,
|
|
||||||
child_statistics: BTreeMap<SnakeToken, BTreeMap<Direction, ActionStatistic>>,
|
|
||||||
childs: BTreeMap<BTreeMap<SnakeToken, (Direction, usize)>, Node>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Node {
|
|
||||||
/// Performs one monte carlo simulation step
|
|
||||||
///
|
|
||||||
/// Returns the snake that has won the simulation
|
|
||||||
fn monte_carlo_step(
|
|
||||||
&mut self,
|
|
||||||
board: &mut simulation::Board,
|
|
||||||
deadline: &Instant,
|
|
||||||
) -> Result<Option<SnakeToken>, DeadlineError> {
|
|
||||||
let stop_condition =
|
|
||||||
|board: &simulation::Board| board.alive_snakes() <= 1 || Instant::now() >= *deadline;
|
|
||||||
let winner = if stop_condition(board) {
|
|
||||||
if Instant::now() >= *deadline {
|
|
||||||
return Err(DeadlineError);
|
|
||||||
}
|
|
||||||
board.snakes().next()
|
|
||||||
} else if self.statistic.played == 0 {
|
|
||||||
// We didn't simulate a game for this node yet. Do that
|
|
||||||
board.simulate_until(&mut thread_rng(), stop_condition);
|
|
||||||
if Instant::now() >= *deadline {
|
|
||||||
return Err(DeadlineError);
|
|
||||||
}
|
|
||||||
board.snakes().next()
|
|
||||||
} else {
|
|
||||||
// select a node to simulate
|
|
||||||
let possible_actions = board.possible_actions();
|
|
||||||
|
|
||||||
let actions = possible_actions
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(token, actions)| {
|
|
||||||
let statistics = self.child_statistics.entry(*token).or_default();
|
|
||||||
let selected = actions.iter().copied().max_by_key(|direction| {
|
|
||||||
let statistics = statistics.entry(*direction).or_default();
|
|
||||||
if statistics.played == 0 {
|
|
||||||
return OrderedFloat(f64::INFINITY);
|
|
||||||
}
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
let exploitation = statistics.won as f64 / statistics.played as f64;
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
let exploration = f64::consts::SQRT_2
|
|
||||||
* f64::sqrt(
|
|
||||||
f64::ln(self.statistic.played as f64) / statistics.played as f64,
|
|
||||||
);
|
|
||||||
OrderedFloat(exploitation + exploration)
|
|
||||||
})?;
|
|
||||||
Some((*token, selected))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
board.simulate_actions(&actions, &mut thread_rng());
|
|
||||||
let map_actions = actions
|
|
||||||
.iter()
|
|
||||||
.map(|(&snake, &action)| (snake, (action, board.snake_length(snake).unwrap_or(0))))
|
|
||||||
.collect();
|
|
||||||
let winner = self
|
|
||||||
.childs
|
|
||||||
.entry(map_actions)
|
|
||||||
.or_default()
|
|
||||||
.monte_carlo_step(board, deadline)?;
|
|
||||||
|
|
||||||
// update child statistics
|
|
||||||
for (token, action) in &actions {
|
|
||||||
let entry = self
|
|
||||||
.child_statistics
|
|
||||||
.entry(*token)
|
|
||||||
.or_default()
|
|
||||||
.entry(*action)
|
|
||||||
.or_default();
|
|
||||||
entry.played += 1;
|
|
||||||
if Some(*token) == winner {
|
|
||||||
entry.won += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
winner
|
|
||||||
};
|
|
||||||
self.statistic.played += 1;
|
|
||||||
if let Some(token) = winner {
|
|
||||||
self.statistic
|
|
||||||
.won
|
|
||||||
.entry(token)
|
|
||||||
.and_modify(|won| *won += 1)
|
|
||||||
.or_insert(1);
|
|
||||||
}
|
|
||||||
Ok(winner)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Performs one monte carlo simulation step for a solo game
|
|
||||||
///
|
|
||||||
/// Returns the lengths before death
|
|
||||||
fn monte_carlo_step_solo(
|
|
||||||
&mut self,
|
|
||||||
board: &mut simulation::Board,
|
|
||||||
deadline: &Instant,
|
|
||||||
) -> Result<BTreeMap<SnakeToken, usize>, DeadlineError> {
|
|
||||||
let lengths = if self.statistic.played == 0 {
|
|
||||||
// We didn't simulate a game for this node yet. Do that
|
|
||||||
let mut lengths: BTreeMap<_, _> = board
|
|
||||||
.snakes()
|
|
||||||
.filter_map(|snake| Some((snake, board.snake_length(snake)?)))
|
|
||||||
.collect();
|
|
||||||
board.simulate_until(&mut thread_rng(), |board| {
|
|
||||||
if Instant::now() >= *deadline {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
for snake in board.snakes() {
|
|
||||||
if let Some(length) = board.snake_length(snake) {
|
|
||||||
lengths.insert(snake, length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
board.alive_snakes() == 0
|
|
||||||
});
|
|
||||||
if Instant::now() >= *deadline {
|
|
||||||
return Err(DeadlineError);
|
|
||||||
}
|
|
||||||
lengths
|
|
||||||
} else {
|
|
||||||
// select a node to simulate
|
|
||||||
let possible_actions = board.possible_actions();
|
|
||||||
|
|
||||||
let actions = possible_actions
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(token, actions)| {
|
|
||||||
let statistics = self.child_statistics.entry(*token).or_default();
|
|
||||||
let selected = actions.iter().copied().max_by_key(|direction| {
|
|
||||||
let statistics = statistics.entry(*direction).or_default();
|
|
||||||
if statistics.played == 0 {
|
|
||||||
return OrderedFloat(f64::INFINITY);
|
|
||||||
}
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
let exploitation = statistics.won as f64
|
|
||||||
/ board.spaces() as f64
|
|
||||||
/ statistics.played as f64;
|
|
||||||
#[allow(clippy::cast_precision_loss)]
|
|
||||||
let exploration = f64::consts::SQRT_2
|
|
||||||
* f64::sqrt(
|
|
||||||
f64::ln(self.statistic.played as f64) / statistics.played as f64,
|
|
||||||
);
|
|
||||||
OrderedFloat(exploitation + exploration)
|
|
||||||
})?;
|
|
||||||
Some((*token, selected))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if Instant::now() >= *deadline {
|
|
||||||
return Err(DeadlineError);
|
|
||||||
}
|
|
||||||
board.simulate_actions(&actions, &mut thread_rng());
|
|
||||||
let map_actions = actions
|
|
||||||
.iter()
|
|
||||||
.map(|(&snake, &action)| (snake, (action, board.snake_length(snake).unwrap_or(0))))
|
|
||||||
.collect();
|
|
||||||
let lengths = self
|
|
||||||
.childs
|
|
||||||
.entry(map_actions)
|
|
||||||
.or_default()
|
|
||||||
.monte_carlo_step_solo(board, deadline)?;
|
|
||||||
|
|
||||||
// update child statistics
|
|
||||||
for (token, action) in &actions {
|
|
||||||
let entry = self
|
|
||||||
.child_statistics
|
|
||||||
.entry(*token)
|
|
||||||
.or_default()
|
|
||||||
.entry(*action)
|
|
||||||
.or_default();
|
|
||||||
entry.played += 1;
|
|
||||||
if let Some(length) = lengths.get(token) {
|
|
||||||
entry.won += length;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lengths
|
|
||||||
};
|
|
||||||
self.statistic.played += 1;
|
|
||||||
for (token, length) in &lengths {
|
|
||||||
self.statistic
|
|
||||||
.won
|
|
||||||
.entry(*token)
|
|
||||||
.and_modify(|won| *won += length)
|
|
||||||
.or_insert(*length);
|
|
||||||
}
|
|
||||||
Ok(lengths)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +1,399 @@
|
|||||||
#![allow(clippy::needless_pass_by_value)]
|
use std::{
|
||||||
|
env,
|
||||||
use battlesnake::{logic, Action, Direction, GameState};
|
sync::atomic::{AtomicUsize, Ordering},
|
||||||
use log::{error, info};
|
|
||||||
use rocket::{
|
|
||||||
fairing::AdHoc, get, http::Status, launch, post, routes, serde::json::Json, tokio::task,
|
|
||||||
};
|
};
|
||||||
use serde_json::Value;
|
|
||||||
use std::{env, time::Instant};
|
|
||||||
|
|
||||||
#[get("/")]
|
use axum::{
|
||||||
fn handle_index() -> Json<Value> {
|
Router,
|
||||||
Json(logic::info())
|
extract::{Json, State},
|
||||||
}
|
response,
|
||||||
|
routing::{get, post},
|
||||||
|
};
|
||||||
|
use battlesnake::types::{
|
||||||
|
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},
|
||||||
|
};
|
||||||
|
|
||||||
#[post("/start", format = "json", data = "<start_req>")]
|
static THREADS: AtomicUsize = AtomicUsize::new(1);
|
||||||
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>")]
|
|
||||||
async fn handle_move(move_req: Json<GameState>) -> Json<Action> {
|
|
||||||
let start = Instant::now();
|
|
||||||
let response = task::spawn_blocking(move || {
|
|
||||||
logic::get_move(
|
|
||||||
&move_req.game,
|
|
||||||
move_req.turn,
|
|
||||||
&move_req.board,
|
|
||||||
&move_req.you,
|
|
||||||
&start,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.inspect_err(|e| error!("failed to join compute thread: {e}"))
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
.unwrap_or(Action {
|
|
||||||
r#move: Direction::Up,
|
|
||||||
shout: Some("I am so dead".to_owned()),
|
|
||||||
});
|
|
||||||
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
info!("Starting Battlesnake Server...");
|
let (sender, mut receiver) = unbounded_channel();
|
||||||
|
|
||||||
rocket::build()
|
debug!("Creating routes");
|
||||||
.attach(AdHoc::on_response("Server ID Middleware", |_, res| {
|
let app = Router::new()
|
||||||
Box::pin(async move {
|
.route("/", get(info))
|
||||||
res.set_raw_header("Server", "battlesnake/github/starter-snake-rust");
|
.route("/start", post(start))
|
||||||
})
|
.route("/move", post(get_move))
|
||||||
}))
|
.route("/end", post(end))
|
||||||
.mount(
|
.with_state(sender);
|
||||||
"/",
|
|
||||||
routes![handle_index, handle_start, handle_move, handle_end],
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn info() -> response::Json<Info> {
|
||||||
|
info!("got info request");
|
||||||
|
response::Json(Info {
|
||||||
|
apiversion: "1",
|
||||||
|
author: "der-informatiker",
|
||||||
|
color: "#00FFEE",
|
||||||
|
head: "smart-caterpillar",
|
||||||
|
tail: "mouse",
|
||||||
|
version: env!("CARGO_PKG_VERSION"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
struct Info {
|
||||||
|
apiversion: &'static str,
|
||||||
|
author: &'static str,
|
||||||
|
color: &'static str,
|
||||||
|
head: &'static str,
|
||||||
|
tail: &'static str,
|
||||||
|
version: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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();
|
||||||
|
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.board);
|
||||||
|
let actions = board.board.valid_actions(id).collect::<Vec<_>>();
|
||||||
|
if actions.len() <= 1 {
|
||||||
|
info!(
|
||||||
|
"only one possible action. Fast forwarding {:?}",
|
||||||
|
actions.first()
|
||||||
|
);
|
||||||
|
return response::Json(Response {
|
||||||
|
direction: actions.first().copied().unwrap_or(Direction::Up),
|
||||||
|
shout: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
info!("valid actions: {actions:?}");
|
||||||
|
|
||||||
|
if start.elapsed() > Duration::from_millis(10) {
|
||||||
|
error!(
|
||||||
|
"The calculation started late ({}ms)",
|
||||||
|
start.elapsed().as_millis()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let score_fn: fn(&Board, u8) -> u32 = match &*request.game.ruleset.name {
|
||||||
|
"solo" => score_solo,
|
||||||
|
_ => score_standard,
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
let mut mcts_actions = Vec::new();
|
||||||
|
while start.elapsed() < timeout * 4 / 5 {
|
||||||
|
let mut board = board.clone();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let my_mcts_manager = mcts_managers.into_iter().nth(usize::from(id)).unwrap();
|
||||||
|
|
||||||
|
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()
|
||||||
|
.max_by_key(|(_, score)| FloatOrd(*score))
|
||||||
|
.map(|(index, _)| match index {
|
||||||
|
0 => Direction::Up,
|
||||||
|
1 => Direction::Down,
|
||||||
|
2 => Direction::Left,
|
||||||
|
3 => Direction::Right,
|
||||||
|
_ => unreachable!(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(action) = action {
|
||||||
|
info!("found action {action:?} after {played} simulations.",);
|
||||||
|
} else {
|
||||||
|
warn!("unable to find a valid action");
|
||||||
|
}
|
||||||
|
info!("chose {action:?}");
|
||||||
|
response::Json(Response {
|
||||||
|
direction: action.unwrap_or(Direction::Up),
|
||||||
|
shout: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ActionInfo {
|
||||||
|
score: u32,
|
||||||
|
played: u32,
|
||||||
|
next: Box<[Option<ActionInfo>; 4]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionInfo {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
score: 0,
|
||||||
|
played: 0,
|
||||||
|
next: Box::new([None, None, None, None]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn uct(&self, c: f32) -> [Option<f32>; 4] {
|
||||||
|
let mut ucts = [None; 4];
|
||||||
|
for (action, uct) in self.next.iter().zip(ucts.iter_mut()) {
|
||||||
|
if let Some(action) = action {
|
||||||
|
let exploitation = action.score as f32 / action.played as f32;
|
||||||
|
let exploration = f32::sqrt(f32::ln(self.played as f32) / action.played as f32);
|
||||||
|
uct.replace(c.mul_add(exploration, exploitation));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ucts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MctsManager {
|
||||||
|
base: ActionInfo,
|
||||||
|
actions: Vec<Direction>,
|
||||||
|
expanded: bool,
|
||||||
|
snake: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MctsManager {
|
||||||
|
fn new(snake: u8) -> Self {
|
||||||
|
Self {
|
||||||
|
base: ActionInfo::new(),
|
||||||
|
actions: Vec::new(),
|
||||||
|
expanded: false,
|
||||||
|
snake,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_score(&mut self, score: u32) {
|
||||||
|
self.base.played += 1;
|
||||||
|
self.base.score += score;
|
||||||
|
let mut current = &mut self.base;
|
||||||
|
for action in &self.actions {
|
||||||
|
let Some(new_current) = &mut current.next[usize::from(*action)] else {
|
||||||
|
error!("got action without actioninfo");
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
current = new_current;
|
||||||
|
current.played += 1;
|
||||||
|
current.score += score;
|
||||||
|
}
|
||||||
|
self.actions.clear();
|
||||||
|
self.expanded = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(new_current) = &mut current.next[usize::from(*action)] else {
|
||||||
|
error!("got action without actioninfo");
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
current = new_current;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ucts = current.uct(c);
|
||||||
|
let valid_actions = board.valid_actions(self.snake);
|
||||||
|
let ucts: Vec<_> = valid_actions
|
||||||
|
.map(|action| (action, ucts[usize::from(action)]))
|
||||||
|
.collect();
|
||||||
|
trace!("got actions: {ucts:?}");
|
||||||
|
if ucts.iter().any(|(_, uct)| uct.is_none()) {
|
||||||
|
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);
|
||||||
|
return Some(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
let action = ucts
|
||||||
|
.iter()
|
||||||
|
.max_by_key(|(_, uct)| FloatOrd(uct.unwrap_or(f32::NEG_INFINITY)))
|
||||||
|
.map(|(action, _)| *action);
|
||||||
|
if let Some(action) = action {
|
||||||
|
self.actions.push(action);
|
||||||
|
}
|
||||||
|
action
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,326 +0,0 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
|
||||||
|
|
||||||
use iter_tools::Itertools;
|
|
||||||
use rand::{seq::IteratorRandom, Rng};
|
|
||||||
|
|
||||||
use crate::{Coord, Direction};
|
|
||||||
|
|
||||||
#[allow(clippy::cast_possible_truncation)]
|
|
||||||
const MAX_HEALTH: u8 = crate::MAX_HEALTH as u8;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
|
||||||
pub struct SnakeToken {
|
|
||||||
id: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SnakeToken {
|
|
||||||
/// create a token map from the current game board.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
///
|
|
||||||
/// This function panics when there are more than 256 snakes on the board.
|
|
||||||
#[must_use]
|
|
||||||
pub fn from_board(board: &crate::Board) -> BTreeMap<String, Self> {
|
|
||||||
board
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, snake)| {
|
|
||||||
(
|
|
||||||
snake.id.clone(),
|
|
||||||
Self {
|
|
||||||
id: u8::try_from(i).expect("Way to many snakes for a single game"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct Board {
|
|
||||||
turn: i32,
|
|
||||||
/// Height of the board
|
|
||||||
height: i32,
|
|
||||||
/// Width of the board
|
|
||||||
width: i32,
|
|
||||||
/// Food on the board
|
|
||||||
food: BTreeSet<Coord>,
|
|
||||||
/// Chance of new food spawning each round
|
|
||||||
food_chance: u8,
|
|
||||||
/// minimum quantity of food that must be on the board
|
|
||||||
min_food: u8,
|
|
||||||
/// Alive snakes
|
|
||||||
snakes: BTreeMap<SnakeToken, Battlesnake>,
|
|
||||||
/// True when playing constrictor mode. In this mode the snakes don't loose health and grow
|
|
||||||
/// every turn
|
|
||||||
constrictor: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Board {
|
|
||||||
#[must_use]
|
|
||||||
pub fn from_game_board(
|
|
||||||
board: &crate::Board,
|
|
||||||
token_map: &BTreeMap<String, SnakeToken>,
|
|
||||||
turn: i32,
|
|
||||||
food_chance: u8,
|
|
||||||
min_food: u8,
|
|
||||||
constrictor: bool,
|
|
||||||
) -> Self {
|
|
||||||
let width = board.width;
|
|
||||||
debug_assert!(width > 0);
|
|
||||||
let height = board.height;
|
|
||||||
debug_assert!(height > 0);
|
|
||||||
let food = board.food.iter().copied().collect();
|
|
||||||
let snakes = board
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.map(|snake| {
|
|
||||||
let token = token_map[&snake.id];
|
|
||||||
(token, Battlesnake::from_game_snake(snake))
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
turn,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
food,
|
|
||||||
food_chance,
|
|
||||||
min_food,
|
|
||||||
snakes,
|
|
||||||
constrictor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub const fn turn(&self) -> i32 {
|
|
||||||
self.turn
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::cast_sign_loss)]
|
|
||||||
#[must_use]
|
|
||||||
pub const fn spaces(&self) -> usize {
|
|
||||||
self.height as usize * self.width as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn alive_snakes(&self) -> usize {
|
|
||||||
self.snakes.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snakes(&self) -> impl Iterator<Item = SnakeToken> + '_ {
|
|
||||||
self.snakes.keys().copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn snake_length(&self, token: SnakeToken) -> Option<usize> {
|
|
||||||
self.snakes.get(&token).map(|snake| snake.body.len())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn last_action(&self, token: SnakeToken) -> Option<Direction> {
|
|
||||||
self.snakes.get(&token).and_then(|snake| {
|
|
||||||
enum_iterator::all::<Direction>()
|
|
||||||
.find(|direction| snake.body[1].move_to(*direction) == *snake.head())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_actions(
|
|
||||||
&mut self,
|
|
||||||
actions: &BTreeMap<SnakeToken, Direction>,
|
|
||||||
rng: &mut impl Rng,
|
|
||||||
) {
|
|
||||||
// move snakes
|
|
||||||
for (token, snake) in &mut self.snakes {
|
|
||||||
snake.perform_action(actions.get(token).copied().unwrap_or(Direction::Up));
|
|
||||||
}
|
|
||||||
|
|
||||||
// feed snakes
|
|
||||||
for snake in &mut self.snakes.values_mut() {
|
|
||||||
let head = snake.head();
|
|
||||||
if self.constrictor || self.food.remove(head) {
|
|
||||||
snake.health = MAX_HEALTH;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// kill snakes
|
|
||||||
let alive_ids = self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, snake)| {
|
|
||||||
// snake must have enough health
|
|
||||||
snake.health != 0
|
|
||||||
})
|
|
||||||
.map(|(token, snake)| (*token, snake.body.len(), *snake.head()))
|
|
||||||
.filter(|(_, _, head)| {
|
|
||||||
// head in bounds
|
|
||||||
(0..self.width).contains(&head.x) && (0..self.height).contains(&head.y)
|
|
||||||
})
|
|
||||||
.filter(|(_, _, head)| {
|
|
||||||
// body collision
|
|
||||||
!self
|
|
||||||
.snakes
|
|
||||||
.values()
|
|
||||||
.flat_map(|snake2| snake2.body.iter().skip(1))
|
|
||||||
.any(|body| body == head)
|
|
||||||
})
|
|
||||||
.filter(|(token, len, head)| {
|
|
||||||
// head to head collision
|
|
||||||
!self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.filter(|(token2, snake2)| *token2 != token && snake2.body.len() >= *len)
|
|
||||||
.any(|(_, snake2)| snake2.head() == head)
|
|
||||||
})
|
|
||||||
.map(|(token, _, _)| token)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
self.snakes.retain(|token, _| alive_ids.contains(token));
|
|
||||||
|
|
||||||
// spawn new food
|
|
||||||
if self.food.len() < usize::from(self.min_food)
|
|
||||||
|| rng.gen_ratio(u32::from(self.food_chance), 100)
|
|
||||||
{
|
|
||||||
let free_fields = (0..self.width)
|
|
||||||
.flat_map(|x| (0..self.height).map(move |y| Coord { x, y }))
|
|
||||||
.filter(|coord| {
|
|
||||||
!self
|
|
||||||
.snakes
|
|
||||||
.values()
|
|
||||||
.flat_map(|snake| snake.body.iter())
|
|
||||||
.any(|body| body == coord)
|
|
||||||
})
|
|
||||||
.filter(|coord| self.food.contains(coord));
|
|
||||||
if let Some(field) = free_fields.choose(rng) {
|
|
||||||
self.food.insert(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.turn += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_until(&mut self, rng: &mut impl Rng, mut exit: impl FnMut(&Self) -> bool) {
|
|
||||||
while !exit(self) {
|
|
||||||
let actions = self
|
|
||||||
.possible_actions()
|
|
||||||
.iter()
|
|
||||||
.map(|(token, actions)| {
|
|
||||||
(
|
|
||||||
*token,
|
|
||||||
actions.iter().choose(rng).copied().unwrap_or(Direction::Up),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
self.simulate_actions(&actions, rng);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn possible_actions(&self) -> BTreeMap<SnakeToken, BTreeSet<Direction>> {
|
|
||||||
let mut actions: BTreeMap<_, BTreeSet<_>> = self
|
|
||||||
.snakes
|
|
||||||
.keys()
|
|
||||||
.map(|&token| (token, enum_iterator::all::<Direction>().collect()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for (token, actions) in &mut actions {
|
|
||||||
let snake = &self.snakes[token];
|
|
||||||
let head = snake.head();
|
|
||||||
|
|
||||||
actions.retain(|direction| {
|
|
||||||
let target = head.move_to(*direction);
|
|
||||||
|
|
||||||
// don't move out of bounds
|
|
||||||
if !((0..self.width).contains(&target.x) && (0..self.height).contains(&target.y)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't collide with other snakes
|
|
||||||
!self
|
|
||||||
.snakes
|
|
||||||
.values()
|
|
||||||
.flat_map(|snake| {
|
|
||||||
let has_eaten = snake.health == MAX_HEALTH;
|
|
||||||
snake
|
|
||||||
.body
|
|
||||||
.iter()
|
|
||||||
.take(snake.body.len() - usize::from(!has_eaten))
|
|
||||||
})
|
|
||||||
.any(|coord| *coord == target)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't move into bigger snakes heads with only one movement option
|
|
||||||
let bigger_snakes = self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.sorted_unstable_by(|(_, snake1), (_, snake2)| snake2.health.cmp(&snake1.health))
|
|
||||||
.map(|(token, snake)| {
|
|
||||||
let actions = &actions[token];
|
|
||||||
if actions.len() == 1 {
|
|
||||||
let Some(action) = actions.first() else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
(snake.body.len(), Some(snake.head().move_to(*action)))
|
|
||||||
} else {
|
|
||||||
(snake.body.len(), None)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
for (token, actions) in &mut actions {
|
|
||||||
let snake = &self.snakes[token];
|
|
||||||
let head = snake.head();
|
|
||||||
|
|
||||||
actions.retain(|direction| {
|
|
||||||
let target = head.move_to(*direction);
|
|
||||||
!bigger_snakes
|
|
||||||
.iter()
|
|
||||||
.take_while(|(length, _)| *length > snake.body.len())
|
|
||||||
.any(|(_, coord)| coord.map_or(false, |coord| coord == target))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
actions
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct Battlesnake {
|
|
||||||
/// health points
|
|
||||||
health: u8,
|
|
||||||
/// Body of the snake. The head is the first element in the queue
|
|
||||||
body: VecDeque<Coord>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Battlesnake {
|
|
||||||
#[must_use]
|
|
||||||
pub fn from_game_snake(snake: &crate::Battlesnake) -> Self {
|
|
||||||
let body: VecDeque<_> = snake.body.iter().copied().collect();
|
|
||||||
debug_assert_eq!(Ok(body.len()), usize::try_from(snake.length));
|
|
||||||
debug_assert!(snake.health <= crate::MAX_HEALTH);
|
|
||||||
let Ok(health) = u8::try_from(snake.health.min(100)) else {
|
|
||||||
unreachable!()
|
|
||||||
};
|
|
||||||
Self { health, body }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn perform_action(&mut self, direction: Direction) {
|
|
||||||
debug_assert!(!self.body.is_empty());
|
|
||||||
// move the head along
|
|
||||||
self.body.push_front(self.head().move_to(direction));
|
|
||||||
|
|
||||||
// move tail
|
|
||||||
if self.health != MAX_HEALTH {
|
|
||||||
// only move the tail if we didn't eat
|
|
||||||
self.body.pop_back();
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrease helth
|
|
||||||
self.health = self.health.saturating_sub(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn head(&self) -> &Coord {
|
|
||||||
&self.body[0]
|
|
||||||
}
|
|
||||||
}
|
|
66
battlesnake/src/types/mod.rs
Normal file
66
battlesnake/src/types/mod.rs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
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, Serialize)]
|
||||||
|
pub struct Coord {
|
||||||
|
pub x: u8,
|
||||||
|
pub y: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Coord {
|
||||||
|
#[must_use]
|
||||||
|
pub fn apply(self, direction: Direction) -> Option<Self> {
|
||||||
|
match direction {
|
||||||
|
Direction::Up => self.y.checked_add(1).map(|y| Self { y, x: self.x }),
|
||||||
|
Direction::Down => self.y.checked_sub(1).map(|y| Self { y, x: self.x }),
|
||||||
|
Direction::Left => self.x.checked_sub(1).map(|x| Self { x, y: self.y }),
|
||||||
|
Direction::Right => self.x.checked_add(1).map(|x| Self { x, y: self.y }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn wrapping_apply(mut self, direction: Direction) -> Self {
|
||||||
|
match direction {
|
||||||
|
Direction::Up => self.y = self.y.wrapping_add(1),
|
||||||
|
Direction::Down => self.y = self.y.wrapping_sub(1),
|
||||||
|
Direction::Left => self.x = self.x.wrapping_sub(1),
|
||||||
|
Direction::Right => self.x = self.x.wrapping_add(1),
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
Up,
|
||||||
|
/// Move in negative y direction
|
||||||
|
Down,
|
||||||
|
/// Move in negative x direction
|
||||||
|
Left,
|
||||||
|
/// Move in positive x direction
|
||||||
|
Right,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Direction> for usize {
|
||||||
|
fn from(value: Direction) -> Self {
|
||||||
|
match value {
|
||||||
|
Direction::Up => 0,
|
||||||
|
Direction::Down => 1,
|
||||||
|
Direction::Left => 2,
|
||||||
|
Direction::Right => 3,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
43
battlesnake/src/types/simulation/maps/mod.rs
Normal file
43
battlesnake/src/types/simulation/maps/mod.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
86
battlesnake/src/types/simulation/maps/royale.rs
Normal file
86
battlesnake/src/types/simulation/maps/royale.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
78
battlesnake/src/types/simulation/maps/standard.rs
Normal file
78
battlesnake/src/types/simulation/maps/standard.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
444
battlesnake/src/types/simulation/mod.rs
Normal file
444
battlesnake/src/types/simulation/mod.rs
Normal file
@ -0,0 +1,444 @@
|
|||||||
|
mod maps;
|
||||||
|
mod rules;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
fmt::Display,
|
||||||
|
num::NonZeroUsize,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
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::{Coord, Direction, wire::Request};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
enum SmallBitBox {
|
||||||
|
Stack {
|
||||||
|
storage: BitArr!(for Self::STACK_BITS),
|
||||||
|
len: NonZeroUsize,
|
||||||
|
},
|
||||||
|
Heap(BitBox),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SmallBitBox {
|
||||||
|
const STACK_BITS: usize = usize::BITS as usize * 3;
|
||||||
|
|
||||||
|
fn new(initial: bool, len: usize) -> Self {
|
||||||
|
if let len @ 1..Self::STACK_BITS = len {
|
||||||
|
let Some(len) = NonZeroUsize::new(len) else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
let mut storage = BitArray::ZERO;
|
||||||
|
storage.fill(initial);
|
||||||
|
Self::Stack { storage, len }
|
||||||
|
} else {
|
||||||
|
Self::Heap(bitbox![u8::from(initial); len])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for SmallBitBox {
|
||||||
|
type Target = BitSlice;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
match self {
|
||||||
|
Self::Stack { storage, len } => &storage[..len.get()],
|
||||||
|
Self::Heap(bit_box) => bit_box,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for SmallBitBox {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
match self {
|
||||||
|
Self::Stack { storage, len } => &mut storage[..len.get()],
|
||||||
|
Self::Heap(bit_box) => bit_box,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
turn: u32,
|
||||||
|
food: SmallBitBox,
|
||||||
|
hazard: SmallBitBox,
|
||||||
|
free: SmallBitBox,
|
||||||
|
snakes: Vec<Snake>,
|
||||||
|
id_map: Arc<[(u8, Arc<str>)]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Request> for Board {
|
||||||
|
fn from(value: &Request) -> Self {
|
||||||
|
let width = value.board.width;
|
||||||
|
let height = value.board.height;
|
||||||
|
let fields = usize::from(width) * usize::from(height);
|
||||||
|
let id_map = value
|
||||||
|
.board
|
||||||
|
.snakes
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter_map(|(i, snake)| {
|
||||||
|
u8::try_from(i)
|
||||||
|
.inspect_err(|e| warn!("unable to convert id to u8: {e}"))
|
||||||
|
.ok()
|
||||||
|
.map(|i| (i, snake.id.clone()))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut board = Self {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
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()),
|
||||||
|
id_map: id_map.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
for &food in &value.board.food {
|
||||||
|
let index = board.coord_to_linear(food);
|
||||||
|
board.food.set(index, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for &hazard in &value.board.hazards {
|
||||||
|
let index = board.coord_to_linear(hazard);
|
||||||
|
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(!constrictor))
|
||||||
|
{
|
||||||
|
let index = board.coord_to_linear(tile);
|
||||||
|
board.free.set(index, false);
|
||||||
|
}
|
||||||
|
let snake = Snake {
|
||||||
|
body: snake.body.iter().rev().copied().collect(),
|
||||||
|
id: u8::try_from(id).unwrap_or(u8::MAX),
|
||||||
|
health: snake.health,
|
||||||
|
};
|
||||||
|
board.snakes.push(snake);
|
||||||
|
}
|
||||||
|
|
||||||
|
board
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Board {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
writeln!(f, "{}x{} @ {}", self.width, self.height, self.turn)?;
|
||||||
|
|
||||||
|
for y in (0..self.height).rev() {
|
||||||
|
for x in 0..self.width {
|
||||||
|
let tile = Coord { x, y };
|
||||||
|
if self.snakes.iter().any(|snake| snake.head() == tile) {
|
||||||
|
write!(f, "H")?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.snakes.iter().any(|snake| snake.tail() == tile) {
|
||||||
|
write!(f, "T")?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = self.coord_to_linear(tile);
|
||||||
|
if !self.free[index] {
|
||||||
|
write!(f, "S")?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.food[index] {
|
||||||
|
write!(f, "f")?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if self.hazard[index] {
|
||||||
|
write!(f, "h")?;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, ".")?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Board {
|
||||||
|
#[must_use]
|
||||||
|
pub fn get_id(&self, str_id: &str) -> Option<u8> {
|
||||||
|
self.id_map
|
||||||
|
.iter()
|
||||||
|
.find(|(_, str_id2)| *str_id == **str_id2)
|
||||||
|
.map(|(id, _)| *id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub const fn turn(&self) -> u32 {
|
||||||
|
self.turn
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn num_snakes(&self) -> usize {
|
||||||
|
self.snakes.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn alive(&self, id: u8) -> bool {
|
||||||
|
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);
|
||||||
|
self.food[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_hazard(&self, tile: Coord) -> bool {
|
||||||
|
let index = self.coord_to_linear(tile);
|
||||||
|
self.hazard[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_free(&self, tile: Coord) -> bool {
|
||||||
|
if !self.is_in_bounds(tile) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let index = self.coord_to_linear(tile);
|
||||||
|
self.free[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn valid_actions(&self, id: u8) -> impl Iterator<Item = Direction> + use<'_> {
|
||||||
|
let index = self.id_to_index(id);
|
||||||
|
index
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|index| self.valid_actions_index(index))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
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(rng)
|
||||||
|
.unwrap_or(Direction::Up)
|
||||||
|
}
|
||||||
|
|
||||||
|
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(rng)
|
||||||
|
.unwrap_or(Direction::Up),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn id_to_index(&self, id: u8) -> Option<usize> {
|
||||||
|
self.snakes.binary_search_by_key(&id, |snake| snake.id).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn valid_actions_index(&self, index: usize) -> impl Iterator<Item = Direction> + use<'_> {
|
||||||
|
let head = self.snakes[index].head();
|
||||||
|
enum_iterator::all::<Direction>()
|
||||||
|
.filter(move |direction| self.is_free(head.wrapping_apply(*direction)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_free_map(&mut self) {
|
||||||
|
// free tails
|
||||||
|
for snake in &self.snakes {
|
||||||
|
let tail = snake.tail();
|
||||||
|
let pre_tail = snake.body[snake.body.len() - 2];
|
||||||
|
if tail != pre_tail {
|
||||||
|
let tail_index = self.coord_to_linear(tail);
|
||||||
|
self.free.set(tail_index, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// block heads
|
||||||
|
for snake in &self.snakes {
|
||||||
|
let head = snake.head();
|
||||||
|
let head_index = self.coord_to_linear(head);
|
||||||
|
self.free.set(head_index, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
self.snakes
|
||||||
|
.iter()
|
||||||
|
.flat_map(|snake| {
|
||||||
|
let head = snake.head();
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
.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))
|
||||||
|
})
|
||||||
|
.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
|
struct Snake {
|
||||||
|
id: u8,
|
||||||
|
health: u8,
|
||||||
|
body: VecDeque<Coord>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Snake {
|
||||||
|
pub fn head(&self) -> Coord {
|
||||||
|
self.body.back().copied().unwrap_or_else(|| {
|
||||||
|
error!("Snake without a head: {self:?}");
|
||||||
|
Coord { x: 0, y: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tail(&self) -> Coord {
|
||||||
|
self.body.front().copied().unwrap_or_else(|| {
|
||||||
|
error!("Snake without a tail: {self:?}");
|
||||||
|
Coord { x: 0, y: 0 }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn advance(&mut self, head: Coord) {
|
||||||
|
self.body.push_back(head);
|
||||||
|
self.body.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn feed(&mut self) {
|
||||||
|
if let Some(tail) = self.body.front() {
|
||||||
|
self.body.push_front(*tail);
|
||||||
|
self.health = 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
battlesnake/src/types/simulation/rules/constrictor.rs
Normal file
38
battlesnake/src/types/simulation/rules/constrictor.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
37
battlesnake/src/types/simulation/rules/mod.rs
Normal file
37
battlesnake/src/types/simulation/rules/mod.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
battlesnake/src/types/simulation/rules/solo.rs
Normal file
36
battlesnake/src/types/simulation/rules/solo.rs
Normal 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
|
||||||
|
}
|
180
battlesnake/src/types/simulation/rules/standard.rs
Normal file
180
battlesnake/src/types/simulation/rules/standard.rs
Normal 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
|
||||||
|
}
|
121
battlesnake/src/types/wire.rs
Normal file
121
battlesnake/src/types/wire.rs
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use super::{Coord, Direction};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Request {
|
||||||
|
/// Game object describing the game being played.
|
||||||
|
pub game: Game,
|
||||||
|
/// Turn number for this move.
|
||||||
|
pub turn: u32,
|
||||||
|
/// Board object describing the initial state of the game board.
|
||||||
|
pub board: Board,
|
||||||
|
/// Battlesnake Object describing your Battlesnake.
|
||||||
|
pub you: Battlesnake,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Game {
|
||||||
|
/// A unique identifier for this Game.
|
||||||
|
pub id: Arc<str>,
|
||||||
|
/// Information about the ruleset being used to run this Game.
|
||||||
|
pub ruleset: Ruleset,
|
||||||
|
/// The name of the map being played on.
|
||||||
|
pub map: Arc<str>,
|
||||||
|
/// How much time your snake has to respond to requests for this Game.
|
||||||
|
pub timeout: u16,
|
||||||
|
/// The source of this Game.
|
||||||
|
/// One of:
|
||||||
|
/// - tournament
|
||||||
|
/// - league
|
||||||
|
/// - arena
|
||||||
|
/// - challenge
|
||||||
|
/// - custom
|
||||||
|
pub source: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct Ruleset {
|
||||||
|
/// Name of the ruleset being used to run this game.
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// The release version of the Rules module used in this game.
|
||||||
|
pub version: Arc<str>,
|
||||||
|
/// A collection of specific settings being used by the current game that control how the rules
|
||||||
|
/// are applied.
|
||||||
|
pub settings: Settings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Settings {
|
||||||
|
/// Percentage chance of spawning a new food every round.
|
||||||
|
pub food_spawn_chance: u8,
|
||||||
|
/// Minimum food to keep on the board every turn.
|
||||||
|
pub minimum_food: u16,
|
||||||
|
/// Health damage a snake will take when ending its turn in a hazard. This stacks on top of the
|
||||||
|
/// regular 1 damage a snake takes per turn.
|
||||||
|
pub hazard_damage_per_turn: u8,
|
||||||
|
/// Settings for the royale game mode
|
||||||
|
pub royale: RoyaleSettings,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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, Serialize)]
|
||||||
|
pub struct Board {
|
||||||
|
/// The number of rows in the y-axis of the game board.
|
||||||
|
pub height: u8,
|
||||||
|
/// The number of rows in the x-axis of the game board.
|
||||||
|
pub width: u8,
|
||||||
|
/// Array of coordinates representing food locations on the game board.
|
||||||
|
pub food: Vec<Coord>,
|
||||||
|
/// Array of coordinates representing hazardous locations on the game board.
|
||||||
|
pub hazards: Vec<Coord>,
|
||||||
|
/// Array of Battlesnake objects representing all Battlesnakes remaining on the game board
|
||||||
|
/// (including yourself if you haven't been eliminated).
|
||||||
|
pub snakes: Vec<Battlesnake>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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>,
|
||||||
|
/// Name given to this Battlesnake by its author
|
||||||
|
pub name: Arc<str>,
|
||||||
|
/// Health value of this Battlesnake, between 0 and 100
|
||||||
|
pub health: u8,
|
||||||
|
/// Array of coordinates representing the Battlesnake's location on the game board.
|
||||||
|
/// This array is ordered from head to tail.
|
||||||
|
pub body: Vec<Coord>,
|
||||||
|
/// The previous response time of this Battlesnake, in milliseconds.
|
||||||
|
/// If the Battlesnake timed out and failed to respond, the game timeout will be returned
|
||||||
|
pub latency: Arc<str>,
|
||||||
|
/// Coordinates for this Battlesnake's head.
|
||||||
|
/// Equivalent to the first element of the body array.
|
||||||
|
pub head: Coord,
|
||||||
|
/// Length of this Battlesnake from head to tail.
|
||||||
|
/// Equivalent to the length of the body array.
|
||||||
|
pub length: u16,
|
||||||
|
/// Message shouted by this Battlesnake on the previous turn
|
||||||
|
pub shout: Option<Arc<str>>,
|
||||||
|
/// The squad that the Battlesnake belongs to.
|
||||||
|
/// Used to identify squad members in Squad Mode games.
|
||||||
|
pub squad: Arc<str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
/// Your Battlesnake's move for this turn.
|
||||||
|
#[serde(rename = "move")]
|
||||||
|
pub direction: Direction,
|
||||||
|
/// An optional message sent to all other Battlesnakes on the next turn.
|
||||||
|
/// Must be 256 characters or less.
|
||||||
|
pub shout: Option<String>,
|
||||||
|
}
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
@ -1,7 +1,8 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "xtask"
|
name = "xtask"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rayon = "1.10"
|
rayon = "1.10"
|
||||||
|
rich_progress_bar = "1.1"
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
use std::{
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
env,
|
env,
|
||||||
net::TcpStream,
|
net::TcpStream,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::{Child, Command, Stdio},
|
process::{Child, Command, Stdio},
|
||||||
|
sync::Mutex,
|
||||||
thread::sleep,
|
thread::sleep,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||||
|
use rich_progress_bar::{Colors, RichProgressBar};
|
||||||
|
|
||||||
type DynError = Box<dyn std::error::Error>;
|
type DynError = Box<dyn std::error::Error>;
|
||||||
|
|
||||||
@ -23,6 +26,8 @@ fn try_main() -> Result<(), DynError> {
|
|||||||
match task.as_deref() {
|
match task.as_deref() {
|
||||||
Some("local") => local()?,
|
Some("local") => local()?,
|
||||||
Some("local2") => local2()?,
|
Some("local2") => local2()?,
|
||||||
|
Some("local4") => local4()?,
|
||||||
|
Some("constrictor4") => constrictor4()?,
|
||||||
Some("vs_production" | "vs_prod") => vs_production()?,
|
Some("vs_production" | "vs_prod") => vs_production()?,
|
||||||
Some("regression") => regression()?,
|
Some("regression") => regression()?,
|
||||||
Some("docker") => docker()?,
|
Some("docker") => docker()?,
|
||||||
@ -37,6 +42,8 @@ fn print_help() {
|
|||||||
|
|
||||||
local runs the snake on a local game
|
local runs the snake on a local game
|
||||||
local2 runs the snake twice on a local game
|
local2 runs the snake twice on a local game
|
||||||
|
local4 runs the snake on a local standard game
|
||||||
|
constrictor4 runs the snake on a local constrictor game
|
||||||
vs_production | vs_prod runs the snake against the active server
|
vs_production | vs_prod runs the snake against the active server
|
||||||
regression runs the snake against the active server multiple times
|
regression runs the snake against the active server multiple times
|
||||||
|
|
||||||
@ -99,6 +106,80 @@ fn local2() -> Result<(), DynError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn local4() -> Result<(), DynError> {
|
||||||
|
let mut snake = run_snake(8000, None)?;
|
||||||
|
|
||||||
|
let game = Command::new("./battlesnake-cli")
|
||||||
|
.current_dir(project_root())
|
||||||
|
.args([
|
||||||
|
"play",
|
||||||
|
"-W",
|
||||||
|
"11",
|
||||||
|
"-H",
|
||||||
|
"11",
|
||||||
|
"--name",
|
||||||
|
"local test1",
|
||||||
|
"--url",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"--name",
|
||||||
|
"local test2",
|
||||||
|
"--url",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"--name",
|
||||||
|
"local test3",
|
||||||
|
"--url",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"--name",
|
||||||
|
"local test4",
|
||||||
|
"--url",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"-g",
|
||||||
|
"standard",
|
||||||
|
"--browser",
|
||||||
|
])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
game.and(snake.kill())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn constrictor4() -> Result<(), DynError> {
|
||||||
|
let mut snake = run_snake(8000, None)?;
|
||||||
|
|
||||||
|
let game = Command::new("./battlesnake-cli")
|
||||||
|
.current_dir(project_root())
|
||||||
|
.args([
|
||||||
|
"play",
|
||||||
|
"-W",
|
||||||
|
"11",
|
||||||
|
"-H",
|
||||||
|
"11",
|
||||||
|
"--name",
|
||||||
|
"local test1",
|
||||||
|
"--url",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"--name",
|
||||||
|
"local test2",
|
||||||
|
"--url",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"--name",
|
||||||
|
"local test3",
|
||||||
|
"--url",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"--name",
|
||||||
|
"local test4",
|
||||||
|
"--url",
|
||||||
|
"http://localhost:8000",
|
||||||
|
"-g",
|
||||||
|
"constrictor",
|
||||||
|
"--browser",
|
||||||
|
])
|
||||||
|
.status();
|
||||||
|
|
||||||
|
game.and(snake.kill())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn vs_production() -> Result<(), DynError> {
|
fn vs_production() -> Result<(), DynError> {
|
||||||
let mut snake = run_snake(8000, None)?;
|
let mut snake = run_snake(8000, None)?;
|
||||||
|
|
||||||
@ -153,37 +234,62 @@ fn regression() -> Result<(), DynError> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
sleep(Duration::from_secs(1));
|
||||||
|
|
||||||
let res = try_regression();
|
let res = try_regression();
|
||||||
|
|
||||||
snake.kill().and(prod.kill())?;
|
snake.kill().and(prod.kill())?;
|
||||||
let (won, games) = res?;
|
stop_local_docker()?;
|
||||||
|
stop_production()?;
|
||||||
|
let (won, draw, loose) = res?;
|
||||||
|
let games = won + draw + loose;
|
||||||
println!(
|
println!(
|
||||||
"\nThe local snake has won {won}/{games} games ({}%)",
|
"\nThe local snake has won {won}/{games} games ({}%)",
|
||||||
(won + games / 200) * 100 / games
|
(won + games / 200) * 100 / games
|
||||||
);
|
);
|
||||||
|
println!(
|
||||||
|
"The local snake has drawn {draw}/{games} games ({}%)",
|
||||||
|
(draw + games / 200) * 100 / games
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"The local snake has lost {loose}/{games} games ({}%)",
|
||||||
|
(loose + games / 200) * 100 / games
|
||||||
|
);
|
||||||
|
match won.cmp(&loose) {
|
||||||
|
Ordering::Less => println!("The local nake is worse than the production"),
|
||||||
|
Ordering::Equal => println!("The local snake is equivalent to the production"),
|
||||||
|
Ordering::Greater => println!("The local snake is better than the production"),
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_regression() -> Result<(usize, usize), DynError> {
|
fn try_regression() -> Result<(usize, usize, usize), DynError> {
|
||||||
const GAMES: usize = 100;
|
const GAMES: usize = 100;
|
||||||
// limit the parallelism
|
// limit the parallelism
|
||||||
rayon::ThreadPoolBuilder::new()
|
rayon::ThreadPoolBuilder::new()
|
||||||
.num_threads(std::thread::available_parallelism()?.get() / 3)
|
.num_threads(std::thread::available_parallelism()?.get() / 4)
|
||||||
.build_global()
|
.build_global()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let won_count = (0..GAMES)
|
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()
|
.into_par_iter()
|
||||||
.map(|_| -> Option<usize> {
|
.flat_map(|_| {
|
||||||
eprint!(".");
|
|
||||||
let game = Command::new("./battlesnake-cli")
|
let game = Command::new("./battlesnake-cli")
|
||||||
.current_dir(project_root())
|
.current_dir(project_root())
|
||||||
.args([
|
.args([
|
||||||
"play",
|
"play",
|
||||||
"-W",
|
"--width",
|
||||||
"11",
|
"11",
|
||||||
"-H",
|
"--height",
|
||||||
"11",
|
"11",
|
||||||
|
"--timeout",
|
||||||
|
"100",
|
||||||
"--name",
|
"--name",
|
||||||
"local",
|
"local",
|
||||||
"--url",
|
"--url",
|
||||||
@ -197,21 +303,38 @@ fn try_regression() -> Result<(usize, usize), DynError> {
|
|||||||
])
|
])
|
||||||
.output()
|
.output()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
if !game.status.success() {
|
let res = if !game.status.success() {
|
||||||
eprintln!("game output: {}", String::from_utf8(game.stderr).ok()?);
|
eprintln!("game output: {}", String::from_utf8(game.stderr).ok()?);
|
||||||
eprintln!("game status: {}", game.status);
|
eprintln!("game status: {}", game.status);
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
const WIN_PHRASE: &[u8] = b"local was the winner.\n";
|
const WIN_PHRASE: &[u8] = b"local was the winner.\n";
|
||||||
let won = &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()]
|
const LOOSE_PHRASE: &[u8] = b"production was the winner.\n";
|
||||||
== WIN_PHRASE;
|
if &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()]
|
||||||
Some(usize::from(won))
|
== WIN_PHRASE
|
||||||
|
{
|
||||||
|
Some((1, 0, 0))
|
||||||
|
} else if &game.stderr[(game.stderr.len() - LOOSE_PHRASE.len())..game.stderr.len()]
|
||||||
|
== LOOSE_PHRASE
|
||||||
|
{
|
||||||
|
Some((0, 0, 1))
|
||||||
|
} else {
|
||||||
|
Some((0, 1, 0))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Ok(mut progress) = progress.lock() {
|
||||||
|
let _ = progress.inc();
|
||||||
}
|
}
|
||||||
|
res
|
||||||
})
|
})
|
||||||
.flatten()
|
.reduce(
|
||||||
.sum();
|
|| (0, 0, 0),
|
||||||
|
|(sum_win, sum_draw, sum_loose), (win, draw, loose)| {
|
||||||
|
(sum_win + win, sum_draw + draw, sum_loose + loose)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
Ok((won_count, GAMES))
|
Ok(stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_snake(port: u16, log: Option<&str>) -> Result<Child, DynError> {
|
fn run_snake(port: u16, log: Option<&str>) -> Result<Child, DynError> {
|
||||||
@ -225,7 +348,7 @@ fn run_snake(port: u16, log: Option<&str>) -> Result<Child, DynError> {
|
|||||||
}
|
}
|
||||||
let mut snake = snake
|
let mut snake = snake
|
||||||
.args(
|
.args(
|
||||||
["run", "--bin", "battlesnake"]
|
["run", "--bin", "battlesnake", "--release"]
|
||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(env::args().skip(2)),
|
.chain(env::args().skip(2)),
|
||||||
@ -260,7 +383,7 @@ fn run_local_docker(port: u16) -> Result<Child, DynError> {
|
|||||||
"--env",
|
"--env",
|
||||||
"RUST_LOG=error",
|
"RUST_LOG=error",
|
||||||
"--env",
|
"--env",
|
||||||
format!("ROCKET_PORT={}", port).as_str(),
|
format!("PORT={}", port).as_str(),
|
||||||
"--network=host",
|
"--network=host",
|
||||||
"--rm",
|
"--rm",
|
||||||
"local_snake",
|
"local_snake",
|
||||||
@ -280,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> {
|
fn run_production(port: u16) -> Result<Child, DynError> {
|
||||||
let mut snake = Command::new("docker")
|
let mut snake = Command::new("docker")
|
||||||
.args([
|
.args([
|
||||||
@ -290,7 +420,7 @@ fn run_production(port: u16) -> Result<Child, DynError> {
|
|||||||
"--env",
|
"--env",
|
||||||
"RUST_LOG=error",
|
"RUST_LOG=error",
|
||||||
"--env",
|
"--env",
|
||||||
format!("ROCKET_PORT={}", port).as_str(),
|
format!("PORT={}", port).as_str(),
|
||||||
"--network=host",
|
"--network=host",
|
||||||
"--rm",
|
"--rm",
|
||||||
"docker.mkaenner.de/snake:latest",
|
"docker.mkaenner.de/snake:latest",
|
||||||
@ -310,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> {
|
fn docker() -> Result<(), DynError> {
|
||||||
if !Command::new("docker")
|
if !Command::new("docker")
|
||||||
.current_dir(project_root())
|
.current_dir(project_root())
|
||||||
|
Reference in New Issue
Block a user