Compare commits
12 Commits
b4b332bdbb
...
main
Author | SHA1 | Date | |
---|---|---|---|
f0156ccec4 | |||
2c2bfd3489 | |||
263b0642f4 | |||
081c41b753 | |||
549d4fe36d | |||
6f54282c9a | |||
6f3a1d138a | |||
0876f1ed3c | |||
87fc6cccd2 | |||
2f87e2aa60 | |||
5f1d3dfc4f | |||
d44538b749 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
target
|
target
|
||||||
|
assets
|
||||||
|
games
|
||||||
|
1417
Cargo.lock
generated
1417
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,7 @@ authors = ["Max Känner"]
|
|||||||
name = "battlesnake"
|
name = "battlesnake"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
edition = "2024"
|
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,11 +11,21 @@ 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]
|
||||||
# server
|
# server
|
||||||
tokio = { version = "1.43", features = ["full"] }
|
tokio = { version = "1.43", features = ["full"] }
|
||||||
@ -31,6 +42,20 @@ enum-iterator = "2.1"
|
|||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
float-ord = "0.3"
|
float-ord = "0.3"
|
||||||
futures-util = "0.3.31"
|
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 = "0.5"
|
criterion = "0.5"
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
use std::sync::{
|
use std::{
|
||||||
Arc,
|
hint::black_box,
|
||||||
atomic::{AtomicU32, Ordering},
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicU32, Ordering},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
use criterion::{Bencher, BenchmarkId, Criterion, black_box, criterion_group, criterion_main};
|
use criterion::{Bencher, BenchmarkId, Criterion, criterion_group, criterion_main};
|
||||||
|
|
||||||
use battlesnake::types::{
|
use battlesnake::types::{
|
||||||
Coord,
|
Coord,
|
||||||
simulation::Board,
|
simulation::Game as Board,
|
||||||
wire::{Battlesnake, Board as WireBoard, Game, Request, RoyaleSettings, Ruleset, Settings},
|
wire::{Battlesnake, Board as WireBoard, Game, Request, RoyaleSettings, Ruleset, Settings},
|
||||||
};
|
};
|
||||||
use rand::{SeedableRng, rngs::SmallRng};
|
use rand::{SeedableRng, rngs::SmallRng};
|
||||||
@ -75,13 +78,8 @@ fn standard(c: &mut Criterion) {
|
|||||||
let benchmark = |b: &mut Bencher, board: &Board| {
|
let benchmark = |b: &mut Bencher, board: &Board| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let mut board = board.clone();
|
let mut board = board.clone();
|
||||||
let turn = board.simulate_random(&mut SmallRng::from_os_rng(), |board| {
|
board.simulate_random(&mut SmallRng::from_os_rng());
|
||||||
if board.num_snakes() <= 1 {
|
let turn = board.board.turn();
|
||||||
Some(board.turn())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if turn < turns_min.load(Ordering::Relaxed) {
|
if turn < turns_min.load(Ordering::Relaxed) {
|
||||||
turns_min.store(turn, Ordering::Relaxed);
|
turns_min.store(turn, Ordering::Relaxed);
|
||||||
@ -152,13 +150,8 @@ fn constrictor(c: &mut Criterion) {
|
|||||||
let benchmark = |b: &mut Bencher, board: &Board| {
|
let benchmark = |b: &mut Bencher, board: &Board| {
|
||||||
b.iter(|| {
|
b.iter(|| {
|
||||||
let mut board = board.clone();
|
let mut board = board.clone();
|
||||||
let turn = board.simulate_random(&mut SmallRng::from_os_rng(), |board| {
|
board.simulate_random(&mut SmallRng::from_os_rng());
|
||||||
if board.num_snakes() <= 1 {
|
let turn = board.board.turn();
|
||||||
Some(board.turn())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if turn < turns_min.load(Ordering::Relaxed) {
|
if turn < turns_min.load(Ordering::Relaxed) {
|
||||||
turns_min.store(turn, Ordering::Relaxed);
|
turns_min.store(turn, Ordering::Relaxed);
|
||||||
|
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,40 +1,110 @@
|
|||||||
use std::env;
|
use std::{
|
||||||
|
env,
|
||||||
|
sync::atomic::{AtomicUsize, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::Json,
|
extract::{Json, State},
|
||||||
response,
|
response,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use battlesnake::types::{
|
use battlesnake::types::{
|
||||||
Direction,
|
Direction,
|
||||||
simulation::Board,
|
simulation::{Board, Game},
|
||||||
wire::{Request, Response},
|
wire::{Request, Response},
|
||||||
};
|
};
|
||||||
use float_ord::FloatOrd;
|
use float_ord::FloatOrd;
|
||||||
use futures_util::future::join_all;
|
use futures_util::future::join_all;
|
||||||
|
use hashbrown::HashMap;
|
||||||
use log::{debug, error, info, trace, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
|
fs::File,
|
||||||
|
io::AsyncWriteExt,
|
||||||
net::TcpListener,
|
net::TcpListener,
|
||||||
|
sync::mpsc::{UnboundedSender, unbounded_channel},
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static THREADS: AtomicUsize = AtomicUsize::new(1);
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
|
|
||||||
|
let (sender, mut receiver) = unbounded_channel();
|
||||||
|
|
||||||
debug!("Creating routes");
|
debug!("Creating routes");
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(info))
|
.route("/", get(info))
|
||||||
.route("/start", post(start))
|
.route("/start", post(start))
|
||||||
.route("/move", post(get_move))
|
.route("/move", post(get_move))
|
||||||
.route("/end", post(end));
|
.route("/end", post(end))
|
||||||
|
.with_state(sender);
|
||||||
|
|
||||||
|
let threads = env::var("THREADS")
|
||||||
|
.ok()
|
||||||
|
.and_then(|threads| {
|
||||||
|
threads
|
||||||
|
.parse()
|
||||||
|
.inspect_err(|err| error!("Unable to parse number of threads: {err}"))
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
.unwrap_or(1);
|
||||||
|
THREADS.store(threads, Ordering::Relaxed);
|
||||||
|
|
||||||
debug!("Creating listener");
|
debug!("Creating listener");
|
||||||
let port = env::var("PORT").unwrap_or_else(|_| "8000".into());
|
let port = env::var("PORT").unwrap_or_else(|_| "8000".into());
|
||||||
let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await.unwrap();
|
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");
|
debug!("Starting server");
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
@ -61,22 +131,41 @@ struct Info {
|
|||||||
version: &'static str,
|
version: &'static str,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start(request: Json<Request>) {
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
|
||||||
let board = Board::from(&*request);
|
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}");
|
info!("got start request: {board}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
async fn get_move(request: Json<Request>) -> response::Json<Response> {
|
async fn get_move(
|
||||||
|
State(sender): State<UnboundedSender<(RequestType, Request)>>,
|
||||||
|
Json(request): Json<Request>,
|
||||||
|
) -> response::Json<Response> {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let board = Board::from(&*request);
|
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 timeout = Duration::from_millis(u64::from(request.game.timeout));
|
||||||
let id = board.get_id(&request.you.id).unwrap_or_else(|| {
|
let id = board.board.get_id(&request.you.id).unwrap_or_else(|| {
|
||||||
error!("My id is not in the simulation board");
|
error!("My id is not in the simulation board");
|
||||||
0
|
0
|
||||||
});
|
});
|
||||||
debug!("got move request: {board}");
|
debug!("got move request: {}", board.board);
|
||||||
let actions = board.valid_actions(id).collect::<Vec<_>>();
|
let actions = board.board.valid_actions(id).collect::<Vec<_>>();
|
||||||
if actions.len() <= 1 {
|
if actions.len() <= 1 {
|
||||||
info!(
|
info!(
|
||||||
"only one possible action. Fast forwarding {:?}",
|
"only one possible action. Fast forwarding {:?}",
|
||||||
@ -95,20 +184,15 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> {
|
|||||||
start.elapsed().as_millis()
|
start.elapsed().as_millis()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let end_condition: fn(&Board) -> Option<()> = match &*request.game.ruleset.name {
|
|
||||||
"solo" => end_solo,
|
|
||||||
_ => end_standard,
|
|
||||||
};
|
|
||||||
let score_fn: fn(&Board, u8) -> u32 = match &*request.game.ruleset.name {
|
let score_fn: fn(&Board, u8) -> u32 = match &*request.game.ruleset.name {
|
||||||
"solo" => score_solo,
|
"solo" => score_solo,
|
||||||
_ => score_standard,
|
_ => score_standard,
|
||||||
};
|
};
|
||||||
|
|
||||||
let action_futures = (0..3).map(|_| {
|
let action_futures = (0..THREADS.load(Ordering::Relaxed)).map(|_| {
|
||||||
let request = request.clone();
|
let request = request.clone();
|
||||||
let board = board.clone();
|
let board = board.clone();
|
||||||
let mut rng = SmallRng::from_os_rng();
|
let mut rng = SmallRng::from_os_rng();
|
||||||
let actions = actions.clone();
|
|
||||||
tokio::task::spawn_blocking(move || {
|
tokio::task::spawn_blocking(move || {
|
||||||
let mut mcts_managers: Vec<_> = (0..request.board.snakes.len())
|
let mut mcts_managers: Vec<_> = (0..request.board.snakes.len())
|
||||||
.map(|id| MctsManager::new(u8::try_from(id).unwrap()))
|
.map(|id| MctsManager::new(u8::try_from(id).unwrap()))
|
||||||
@ -117,36 +201,29 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> {
|
|||||||
let mut mcts_actions = Vec::new();
|
let mut mcts_actions = Vec::new();
|
||||||
while start.elapsed() < timeout * 4 / 5 {
|
while start.elapsed() < timeout * 4 / 5 {
|
||||||
let mut board = board.clone();
|
let mut board = board.clone();
|
||||||
while end_condition(&board).is_none() {
|
let mut game_over = false;
|
||||||
|
while !game_over {
|
||||||
mcts_actions.clear();
|
mcts_actions.clear();
|
||||||
mcts_actions.extend(mcts_managers.iter_mut().filter_map(|mcts_manager| {
|
mcts_actions.extend(mcts_managers.iter_mut().filter_map(|mcts_manager| {
|
||||||
mcts_manager
|
mcts_manager
|
||||||
.next_action(&board, c, &mut rng)
|
.next_action(&board.board, c, &mut rng)
|
||||||
.map(|action| (mcts_manager.snake, action))
|
.map(|action| (mcts_manager.snake, action))
|
||||||
}));
|
}));
|
||||||
board.next_turn(&mcts_actions, &mut rng);
|
game_over = board.next_turn_random(&mcts_actions, &mut rng);
|
||||||
if mcts_actions.is_empty() {
|
if mcts_actions.is_empty() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
board.simulate_random(&mut rng, end_condition);
|
if !game_over {
|
||||||
|
board.simulate_random(&mut rng);
|
||||||
|
}
|
||||||
for mcts_manager in &mut mcts_managers {
|
for mcts_manager in &mut mcts_managers {
|
||||||
let id = mcts_manager.snake;
|
let id = mcts_manager.snake;
|
||||||
let score = score_fn(&board, id);
|
let score = score_fn(&board.board, id);
|
||||||
mcts_manager.apply_score(score);
|
mcts_manager.apply_score(score);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let my_mcts_manager = mcts_managers.into_iter().nth(usize::from(id)).unwrap();
|
let my_mcts_manager = mcts_managers.into_iter().nth(usize::from(id)).unwrap();
|
||||||
for action in actions {
|
|
||||||
let score = my_mcts_manager.base.next[usize::from(action)]
|
|
||||||
.as_ref()
|
|
||||||
.map(|info| info.score as f32 / info.played as f32);
|
|
||||||
if let Some(score) = score {
|
|
||||||
info!("{action:?} -> {score}");
|
|
||||||
} else {
|
|
||||||
info!("{action:?} -> None");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let actions = my_mcts_manager.base.next.map(|action| {
|
let actions = my_mcts_manager.base.next.map(|action| {
|
||||||
action.map_or(0.0, |action| action.score as f32 / action.played as f32)
|
action.map_or(0.0, |action| action.score as f32 / action.played as f32)
|
||||||
@ -155,7 +232,7 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> {
|
|||||||
(actions, my_mcts_manager.base.played)
|
(actions, my_mcts_manager.base.played)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
let (actions, played) = join_all(action_futures).await.into_iter().fold(
|
let (scores, played) = join_all(action_futures).await.into_iter().fold(
|
||||||
([0.0; 4], 0),
|
([0.0; 4], 0),
|
||||||
|(mut total, mut games), actions| {
|
|(mut total, mut games), actions| {
|
||||||
if let Ok((actions, new_games)) = actions {
|
if let Ok((actions, new_games)) = actions {
|
||||||
@ -167,7 +244,11 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> {
|
|||||||
(total, games)
|
(total, games)
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let action = actions
|
for action in actions {
|
||||||
|
let score = scores[usize::from(action)];
|
||||||
|
info!("{action:?} -> {score}");
|
||||||
|
}
|
||||||
|
let action = scores
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.max_by_key(|(_, score)| FloatOrd(*score))
|
.max_by_key(|(_, score)| FloatOrd(*score))
|
||||||
@ -191,16 +272,8 @@ async fn get_move(request: Json<Request>) -> response::Json<Response> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end_solo(board: &Board) -> Option<()> {
|
const fn score_solo(board: &Board, _id: u8) -> u32 {
|
||||||
(board.valid_actions(0).count() == 0).then_some(())
|
board.turn()
|
||||||
}
|
|
||||||
|
|
||||||
fn end_standard(board: &Board) -> Option<()> {
|
|
||||||
(board.num_snakes() <= 1).then_some(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn score_solo(board: &Board, id: u8) -> u32 {
|
|
||||||
u32::try_from(board.length(id)).unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn score_standard(board: &Board, id: u8) -> u32 {
|
fn score_standard(board: &Board, id: u8) -> u32 {
|
||||||
@ -211,8 +284,14 @@ fn score_standard(board: &Board, id: u8) -> u32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn end(request: Json<Request>) {
|
async fn end(
|
||||||
let board = Board::from(&*request);
|
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}");
|
info!("got end request: {board}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use enum_iterator::Sequence;
|
use enum_iterator::Sequence;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub mod simulation;
|
pub mod simulation;
|
||||||
pub mod wire;
|
pub mod wire;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||||
pub struct Coord {
|
pub struct Coord {
|
||||||
pub x: u8,
|
pub x: u8,
|
||||||
pub y: u8,
|
pub y: u8,
|
||||||
@ -33,6 +35,12 @@ impl Coord {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Serialize, Sequence)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,6 @@
|
|||||||
|
mod maps;
|
||||||
|
mod rules;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
@ -6,9 +9,12 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use az::SaturatingAs;
|
||||||
use bitvec::prelude::*;
|
use bitvec::prelude::*;
|
||||||
use log::{error, warn};
|
use log::{error, warn};
|
||||||
|
use maps::{Map, Maps};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
use rules::{Ruleset, Rulesets};
|
||||||
|
|
||||||
use super::{Coord, Direction, wire::Request};
|
use super::{Coord, Direction, wire::Request};
|
||||||
|
|
||||||
@ -58,19 +64,79 @@ impl DerefMut for SmallBitBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Game {
|
||||||
|
pub board: Board,
|
||||||
|
map: Maps,
|
||||||
|
ruleset: Rulesets,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Request> for Game {
|
||||||
|
fn from(value: &Request) -> Self {
|
||||||
|
let board = value.into();
|
||||||
|
let map = (&value.game).into();
|
||||||
|
let ruleset = (&value.game.ruleset).into();
|
||||||
|
Self {
|
||||||
|
board,
|
||||||
|
map,
|
||||||
|
ruleset,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Game {
|
||||||
|
pub fn simulate_random(&mut self, rng: &mut impl Rng) {
|
||||||
|
loop {
|
||||||
|
let random_actions: Vec<_> = (0..self.board.snakes.len())
|
||||||
|
.filter_map(|i| {
|
||||||
|
self.board
|
||||||
|
.valid_actions_index(i)
|
||||||
|
.choose(rng)
|
||||||
|
.map(|direction| (i.saturating_as(), direction))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if self.next_turn(&random_actions, rng) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_turn_random(&mut self, actions: &[(u8, Direction)], rng: &mut impl Rng) -> bool {
|
||||||
|
let random_actions: Vec<_> = (0..self.board.snakes.len())
|
||||||
|
.filter_map(|i| {
|
||||||
|
actions
|
||||||
|
.iter()
|
||||||
|
.find(|(j, _)| i == usize::from(*j))
|
||||||
|
.copied()
|
||||||
|
.or_else(|| {
|
||||||
|
self.board
|
||||||
|
.valid_actions_index(i)
|
||||||
|
.choose(rng)
|
||||||
|
.map(|direction| (i.saturating_as(), direction))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.next_turn(&random_actions, rng)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn next_turn(&mut self, actions: &[(u8, Direction)], rng: &mut impl Rng) -> bool {
|
||||||
|
self.map.pre_update(&mut self.board, rng);
|
||||||
|
let game_over = self.ruleset.execute(&mut self.board, actions);
|
||||||
|
self.map.post_update(&mut self.board, rng);
|
||||||
|
self.board.turn += 1;
|
||||||
|
game_over
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||||
pub struct Board {
|
pub struct Board {
|
||||||
width: u8,
|
width: u8,
|
||||||
height: u8,
|
height: u8,
|
||||||
hazard_damage: u8,
|
|
||||||
food_spawn_chance: u8,
|
|
||||||
min_food: u16,
|
|
||||||
turn: u32,
|
turn: u32,
|
||||||
food: SmallBitBox,
|
food: SmallBitBox,
|
||||||
hazard: SmallBitBox,
|
hazard: SmallBitBox,
|
||||||
free: SmallBitBox,
|
free: SmallBitBox,
|
||||||
snakes: Vec<Snake>,
|
snakes: Vec<Snake>,
|
||||||
constrictor: bool,
|
|
||||||
id_map: Arc<[(u8, Arc<str>)]>,
|
id_map: Arc<[(u8, Arc<str>)]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,15 +160,11 @@ impl From<&Request> for Board {
|
|||||||
let mut board = Self {
|
let mut board = Self {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
hazard_damage: value.game.ruleset.settings.hazard_damage_per_turn,
|
|
||||||
food_spawn_chance: value.game.ruleset.settings.food_spawn_chance,
|
|
||||||
min_food: value.game.ruleset.settings.minimum_food,
|
|
||||||
turn: value.turn,
|
turn: value.turn,
|
||||||
food: SmallBitBox::new(false, fields),
|
food: SmallBitBox::new(false, fields),
|
||||||
hazard: SmallBitBox::new(false, fields),
|
hazard: SmallBitBox::new(false, fields),
|
||||||
free: SmallBitBox::new(true, fields),
|
free: SmallBitBox::new(true, fields),
|
||||||
snakes: Vec::with_capacity(value.board.snakes.len()),
|
snakes: Vec::with_capacity(value.board.snakes.len()),
|
||||||
constrictor: &*value.game.ruleset.name == "constrictor",
|
|
||||||
id_map: id_map.into(),
|
id_map: id_map.into(),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -116,11 +178,12 @@ impl From<&Request> for Board {
|
|||||||
board.hazard.set(index, true);
|
board.hazard.set(index, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let constrictor = value.game.ruleset.name.as_ref() == "constrictor";
|
||||||
for (id, snake) in value.board.snakes.iter().enumerate() {
|
for (id, snake) in value.board.snakes.iter().enumerate() {
|
||||||
for &tile in snake
|
for &tile in snake
|
||||||
.body
|
.body
|
||||||
.iter()
|
.iter()
|
||||||
.take(snake.body.len() - usize::from(!board.constrictor))
|
.take(snake.body.len() - usize::from(!constrictor))
|
||||||
{
|
{
|
||||||
let index = board.coord_to_linear(tile);
|
let index = board.coord_to_linear(tile);
|
||||||
board.free.set(index, false);
|
board.free.set(index, false);
|
||||||
@ -139,17 +202,7 @@ impl From<&Request> for Board {
|
|||||||
|
|
||||||
impl Display for Board {
|
impl Display for Board {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
writeln!(
|
writeln!(f, "{}x{} @ {}", self.width, self.height, self.turn)?;
|
||||||
f,
|
|
||||||
"{} {}x{} {}% ({}) {}dmg @ {}",
|
|
||||||
if self.constrictor { "constrictor" } else { "" },
|
|
||||||
self.width,
|
|
||||||
self.height,
|
|
||||||
self.food_spawn_chance,
|
|
||||||
self.min_food,
|
|
||||||
self.hazard_damage,
|
|
||||||
self.turn
|
|
||||||
)?;
|
|
||||||
|
|
||||||
for y in (0..self.height).rev() {
|
for y in (0..self.height).rev() {
|
||||||
for x in 0..self.width {
|
for x in 0..self.width {
|
||||||
@ -279,30 +332,6 @@ impl Board {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn simulate_random<T>(
|
|
||||||
&mut self,
|
|
||||||
rng: &mut impl RngCore,
|
|
||||||
stop: impl Fn(&Self) -> Option<T>,
|
|
||||||
) -> T {
|
|
||||||
loop {
|
|
||||||
if let Some(score) = stop(self) {
|
|
||||||
break score;
|
|
||||||
}
|
|
||||||
self.next_turn(&[], rng);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn next_turn(&mut self, actions: &[(u8, Direction)], rng: &mut impl RngCore) {
|
|
||||||
self.move_standard(actions, rng);
|
|
||||||
self.starvation_standard();
|
|
||||||
self.hazard_damage_standard();
|
|
||||||
self.feed_snakes_standard();
|
|
||||||
self.eliminate_snake_standard();
|
|
||||||
self.update_free_map();
|
|
||||||
self.spawn_food(rng);
|
|
||||||
self.turn += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn id_to_index(&self, id: u8) -> Option<usize> {
|
fn id_to_index(&self, id: u8) -> Option<usize> {
|
||||||
self.snakes.binary_search_by_key(&id, |snake| snake.id).ok()
|
self.snakes.binary_search_by_key(&id, |snake| snake.id).ok()
|
||||||
}
|
}
|
||||||
@ -313,127 +342,6 @@ impl Board {
|
|||||||
.filter(move |direction| self.is_free(head.wrapping_apply(*direction)))
|
.filter(move |direction| self.is_free(head.wrapping_apply(*direction)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn move_standard(&mut self, actions: &[(u8, Direction)], rng: &mut impl RngCore) {
|
|
||||||
for i in 0..self.snakes.len() {
|
|
||||||
let snake = &self.snakes[i];
|
|
||||||
let action = actions.iter().find(|(id, _)| *id == snake.id).map_or_else(
|
|
||||||
|| {
|
|
||||||
self.valid_actions_index(i)
|
|
||||||
.choose(rng)
|
|
||||||
.unwrap_or(Direction::Up)
|
|
||||||
},
|
|
||||||
|(_, action)| *action,
|
|
||||||
);
|
|
||||||
let new_head = snake.head().wrapping_apply(action);
|
|
||||||
self.snakes[i].advance(new_head);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn starvation_standard(&mut self) {
|
|
||||||
for snake in &mut self.snakes {
|
|
||||||
snake.health = snake.health.saturating_sub(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn hazard_damage_standard(&mut self) {
|
|
||||||
let mut i = 0;
|
|
||||||
while i < self.snakes.len() {
|
|
||||||
let head = self.snakes[i].head();
|
|
||||||
if self.is_in_bounds(head) {
|
|
||||||
let head_index = self.coord_to_linear(head);
|
|
||||||
if self.hazard[head_index] && !self.food[head_index] {
|
|
||||||
let health = &mut self.snakes[i].health;
|
|
||||||
*health = health.saturating_sub(1);
|
|
||||||
if *health == 0 {
|
|
||||||
let snake = self.snakes.remove(i);
|
|
||||||
for tile in snake.body {
|
|
||||||
let index = self.coord_to_linear(tile);
|
|
||||||
self.free.set(index, true);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn feed_snakes_standard(&mut self) {
|
|
||||||
if self.constrictor {
|
|
||||||
for snake in &mut self.snakes {
|
|
||||||
snake.feed();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut eaten_food = vec![];
|
|
||||||
for i in 0..self.snakes.len() {
|
|
||||||
let head = self.snakes[i].head();
|
|
||||||
if self.is_in_bounds(head) {
|
|
||||||
let head_index = self.coord_to_linear(head);
|
|
||||||
if self.food[head_index] {
|
|
||||||
eaten_food.push(head_index);
|
|
||||||
self.snakes[i].feed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for food_index in eaten_food {
|
|
||||||
self.food.set(food_index, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eliminate_snake_standard(&mut self) {
|
|
||||||
// eliminate out of health and out of bounds
|
|
||||||
let mut i = 0;
|
|
||||||
while i < self.snakes.len() {
|
|
||||||
let snake = &self.snakes[i];
|
|
||||||
if snake.health == 0 || !self.is_in_bounds(snake.head()) {
|
|
||||||
let snake = self.snakes.remove(i);
|
|
||||||
for tile in snake.body.iter().skip(1) {
|
|
||||||
if self.is_in_bounds(*tile) {
|
|
||||||
let index = self.coord_to_linear(*tile);
|
|
||||||
self.free.set(index, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// look for collisions
|
|
||||||
let mut collisions = vec![];
|
|
||||||
for snake in &self.snakes {
|
|
||||||
let head = snake.head();
|
|
||||||
let head_index = self.coord_to_linear(head);
|
|
||||||
if !self.free[head_index] {
|
|
||||||
collisions.push(snake.id);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for snake2 in &self.snakes {
|
|
||||||
if snake.id != snake2.id
|
|
||||||
&& snake.head() == snake2.head()
|
|
||||||
&& snake.body.len() <= snake2.body.len()
|
|
||||||
{
|
|
||||||
collisions.push(snake.id);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply collisions
|
|
||||||
let mut i = 0;
|
|
||||||
while i < self.snakes.len() {
|
|
||||||
if collisions.contains(&self.snakes[i].id) {
|
|
||||||
let snake = self.snakes.remove(i);
|
|
||||||
for tile in snake.body {
|
|
||||||
let index = self.coord_to_linear(tile);
|
|
||||||
self.free.set(index, true);
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn update_free_map(&mut self) {
|
fn update_free_map(&mut self) {
|
||||||
// free tails
|
// free tails
|
||||||
for snake in &self.snakes {
|
for snake in &self.snakes {
|
||||||
@ -452,65 +360,49 @@ impl Board {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_food(&mut self, rng: &mut impl RngCore) {
|
fn get_unoccupied_points(
|
||||||
let food_needed = self.check_food_needing_placement(rng);
|
&self,
|
||||||
|
include_possible_moves: bool,
|
||||||
if food_needed > 0 {
|
include_hazards: bool,
|
||||||
self.place_food_randomly(food_needed, rng);
|
) -> impl Iterator<Item = Coord> + use<'_> {
|
||||||
}
|
let possible_moves: Vec<_> = if include_possible_moves {
|
||||||
}
|
Vec::new()
|
||||||
|
} else {
|
||||||
fn check_food_needing_placement(&self, rng: &mut impl RngCore) -> u16 {
|
self.snakes
|
||||||
let min_food = self.min_food;
|
.iter()
|
||||||
let food_spawn_chance = self.food_spawn_chance;
|
.flat_map(|snake| {
|
||||||
let num_current_food = u16::try_from(self.food.count_ones()).unwrap_or(u16::MAX);
|
let head = snake.head();
|
||||||
|
enum_iterator::all::<Direction>()
|
||||||
if num_current_food < min_food {
|
.map(move |direction| head.wrapping_apply(direction))
|
||||||
return min_food - num_current_food;
|
.filter(|tile| self.is_in_bounds(*tile))
|
||||||
}
|
.map(|tile| self.coord_to_linear(tile))
|
||||||
if food_spawn_chance > 0 && (100 - rng.random_range(0..100)) < food_spawn_chance {
|
})
|
||||||
return 1;
|
.collect()
|
||||||
}
|
};
|
||||||
|
self.free
|
||||||
0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn place_food_randomly(&mut self, amount: u16, rng: &mut impl RngCore) {
|
|
||||||
let tails: Vec<_> = self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
.iter()
|
||||||
.map(|snake| self.coord_to_linear(snake.tail()))
|
.zip(self.food.iter())
|
||||||
.collect();
|
.zip(self.hazard.iter())
|
||||||
let possible_moves: Vec<_> = 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();
|
|
||||||
let unoccupied_points = self
|
|
||||||
.free
|
|
||||||
.iter()
|
|
||||||
.by_vals()
|
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.zip(self.hazard.iter().by_vals())
|
.filter(move |(i, ((free, food), hazard))| {
|
||||||
.filter_map(|((i, free), hazard)| (!hazard && free).then_some(i))
|
**free
|
||||||
.filter(|i| !tails.contains(i))
|
&& !**food
|
||||||
.filter(|i| !possible_moves.contains(i));
|
&& (include_hazards || !**hazard)
|
||||||
|
&& (include_possible_moves || !possible_moves.contains(i))
|
||||||
for food_spot in unoccupied_points.choose_multiple(rng, usize::from(amount)) {
|
})
|
||||||
self.food.set(food_spot, true);
|
.map(|(i, _)| self.linear_to_coord(i))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn coord_to_linear(&self, coord: Coord) -> usize {
|
fn coord_to_linear(&self, coord: Coord) -> usize {
|
||||||
usize::from(coord.x) + usize::from(coord.y) * usize::from(self.width)
|
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 {
|
const fn is_in_bounds(&self, coord: Coord) -> bool {
|
||||||
coord.x < self.width && coord.y < self.height
|
coord.x < self.width && coord.y < self.height
|
||||||
}
|
}
|
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
|
||||||
|
}
|
@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use super::{Coord, Direction};
|
use super::{Coord, Direction};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||||
pub struct Request {
|
pub struct Request {
|
||||||
/// Game object describing the game being played.
|
/// Game object describing the game being played.
|
||||||
pub game: Game,
|
pub game: Game,
|
||||||
@ -16,7 +16,7 @@ pub struct Request {
|
|||||||
pub you: Battlesnake,
|
pub you: Battlesnake,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
/// A unique identifier for this Game.
|
/// A unique identifier for this Game.
|
||||||
pub id: Arc<str>,
|
pub id: Arc<str>,
|
||||||
@ -36,7 +36,7 @@ pub struct Game {
|
|||||||
pub source: Arc<str>,
|
pub source: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||||
pub struct Ruleset {
|
pub struct Ruleset {
|
||||||
/// Name of the ruleset being used to run this game.
|
/// Name of the ruleset being used to run this game.
|
||||||
pub name: Arc<str>,
|
pub name: Arc<str>,
|
||||||
@ -47,7 +47,7 @@ pub struct Ruleset {
|
|||||||
pub settings: Settings,
|
pub settings: Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Settings {
|
pub struct Settings {
|
||||||
/// Percentage chance of spawning a new food every round.
|
/// Percentage chance of spawning a new food every round.
|
||||||
@ -61,14 +61,14 @@ pub struct Settings {
|
|||||||
pub royale: RoyaleSettings,
|
pub royale: RoyaleSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct RoyaleSettings {
|
pub struct RoyaleSettings {
|
||||||
/// The number of turns between generating new hazards (shrinking the safe board space).
|
/// The number of turns between generating new hazards (shrinking the safe board space).
|
||||||
pub shrink_every_n_turns: u8,
|
pub shrink_every_n_turns: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||||
pub struct Board {
|
pub struct Board {
|
||||||
/// The number of rows in the y-axis of the game board.
|
/// The number of rows in the y-axis of the game board.
|
||||||
pub height: u8,
|
pub height: u8,
|
||||||
@ -83,7 +83,7 @@ pub struct Board {
|
|||||||
pub snakes: Vec<Battlesnake>,
|
pub snakes: Vec<Battlesnake>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Deserialize)]
|
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
|
||||||
pub struct Battlesnake {
|
pub struct Battlesnake {
|
||||||
/// Unique identifier for this Battlesnake in the context of the current Game.
|
/// Unique identifier for this Battlesnake in the context of the current Game.
|
||||||
pub id: Arc<str>,
|
pub id: Arc<str>,
|
||||||
|
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
@ -239,6 +239,8 @@ fn regression() -> Result<(), DynError> {
|
|||||||
let res = try_regression();
|
let res = try_regression();
|
||||||
|
|
||||||
snake.kill().and(prod.kill())?;
|
snake.kill().and(prod.kill())?;
|
||||||
|
stop_local_docker()?;
|
||||||
|
stop_production()?;
|
||||||
let (won, draw, loose) = res?;
|
let (won, draw, loose) = res?;
|
||||||
let games = won + draw + loose;
|
let games = won + draw + loose;
|
||||||
println!(
|
println!(
|
||||||
@ -265,7 +267,7 @@ 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() / 8)
|
.num_threads(std::thread::available_parallelism()?.get() / 4)
|
||||||
.build_global()
|
.build_global()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
@ -346,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)),
|
||||||
@ -401,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([
|
||||||
@ -431,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