use library for simulation
This commit is contained in:
parent
2b0b97cba8
commit
7227f1776f
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -154,7 +154,9 @@ dependencies = [
|
||||
name = "battlesnake"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"battlesnake-game-types",
|
||||
"criterion2",
|
||||
"dashmap",
|
||||
"enum-iterator",
|
||||
"env_logger",
|
||||
"iter_tools",
|
||||
@ -166,6 +168,20 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "battlesnake-game-types"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "370d3e9c1067908a33bb29721a19f0658eb3b923d046d6ad38f30494658b4659"
|
||||
dependencies = [
|
||||
"fxhash",
|
||||
"itertools 0.10.5",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "binascii"
|
||||
version = "0.1.4"
|
||||
@ -322,6 +338,20 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
"hashbrown",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
@ -529,6 +559,15 @@ dependencies = [
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fxhash"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generator"
|
||||
version = "0.7.5"
|
||||
@ -728,7 +767,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27812bb0a056539d62930a899759af39dfab17ac73a17d5caf58365762657891"
|
||||
dependencies = [
|
||||
"clone_dyn_types",
|
||||
"itertools",
|
||||
"itertools 0.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -25,6 +25,9 @@ rand = "0.8.4"
|
||||
enum-iterator = "2.1"
|
||||
iter_tools = "0.21"
|
||||
ordered-float = "4.3.0"
|
||||
dashmap = "6.1.0"
|
||||
|
||||
battlesnake-game-types = "0.17.0"
|
||||
|
||||
[dev-dependencies]
|
||||
# criterion = { version = "0.5.1", features = ["html_reports"] }
|
||||
@ -34,7 +37,3 @@ criterion2 = "1.1.1"
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
|
||||
[[bench]]
|
||||
name = "simulation"
|
||||
harness = false
|
||||
|
@ -1,186 +0,0 @@
|
||||
use battlesnake::{
|
||||
simulation::{self, SnakeToken},
|
||||
Coord,
|
||||
};
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use rand::{rngs::StdRng, SeedableRng};
|
||||
|
||||
fn random_moves(board: &battlesnake::Board) -> Option<SnakeToken> {
|
||||
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);
|
||||
|
||||
let winner = board.snakes().next();
|
||||
winner
|
||||
}
|
||||
|
||||
fn bench_duel_random_moves(c: &mut Criterion) {
|
||||
c.bench_function("duel random moves", |b| {
|
||||
b.iter(|| {
|
||||
random_moves(black_box(&battlesnake::Board {
|
||||
height: 11,
|
||||
width: 11,
|
||||
food: vec![Coord { x: 5, y: 5 }],
|
||||
snakes: vec![
|
||||
battlesnake::Battlesnake {
|
||||
id: "1".to_owned(),
|
||||
name: "1".to_owned(),
|
||||
health: 100,
|
||||
body: vec![Coord { x: 5, y: 1 }; 3],
|
||||
head: Coord { x: 5, y: 1 },
|
||||
length: 3,
|
||||
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![],
|
||||
}))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_standard_random_moves(c: &mut Criterion) {
|
||||
c.bench_function("standard random moves", |b| {
|
||||
b.iter(|| {
|
||||
random_moves(black_box(&battlesnake::Board {
|
||||
height: 11,
|
||||
width: 11,
|
||||
food: vec![Coord { x: 5, y: 5 }],
|
||||
snakes: vec![
|
||||
battlesnake::Battlesnake {
|
||||
id: "1".to_owned(),
|
||||
name: "1".to_owned(),
|
||||
health: 100,
|
||||
body: vec![Coord { x: 5, y: 1 }; 3],
|
||||
head: Coord { x: 5, y: 1 },
|
||||
length: 3,
|
||||
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![],
|
||||
}))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_board_clone(c: &mut Criterion) {
|
||||
let board = battlesnake::Board {
|
||||
height: 11,
|
||||
width: 11,
|
||||
food: vec![Coord { x: 5, y: 5 }],
|
||||
snakes: vec![
|
||||
battlesnake::Battlesnake {
|
||||
id: "1".to_owned(),
|
||||
name: "1".to_owned(),
|
||||
health: 100,
|
||||
body: vec![Coord { x: 5, y: 1 }; 3],
|
||||
head: Coord { x: 5, y: 1 },
|
||||
length: 3,
|
||||
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 token_map = SnakeToken::from_board(&board);
|
||||
let mut board =
|
||||
battlesnake::simulation::Board::from_game_board(&board, &token_map, 0, 12, 1, false);
|
||||
|
||||
let mut rng = StdRng::seed_from_u64(0);
|
||||
board.simulate_until(&mut rng, |board| board.turn() > 25);
|
||||
|
||||
c.bench_function("board clone", |b| {
|
||||
b.iter(|| board.clone());
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_duel_random_moves,
|
||||
bench_standard_random_moves,
|
||||
bench_board_clone,
|
||||
);
|
||||
criterion_main!(benches);
|
@ -1,183 +1,24 @@
|
||||
use enum_iterator::Sequence;
|
||||
use battlesnake_game_types::types::Move;
|
||||
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 {
|
||||
pub struct Response {
|
||||
/// 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>,
|
||||
r#move: &'static str,
|
||||
}
|
||||
|
||||
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 {
|
||||
impl Response {
|
||||
#[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,
|
||||
pub const fn new(r#move: Move) -> Self {
|
||||
Self {
|
||||
r#move: match r#move {
|
||||
Move::Left => "left",
|
||||
Move::Down => "down",
|
||||
Move::Up => "up",
|
||||
Move::Right => "right",
|
||||
},
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct GameState {
|
||||
pub game: Game,
|
||||
pub turn: i32,
|
||||
pub board: Board,
|
||||
pub you: Battlesnake,
|
||||
}
|
||||
|
@ -11,22 +11,23 @@
|
||||
// For more info see docs.battlesnake.com
|
||||
|
||||
use core::f64;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
sync::{Arc, LazyLock, Mutex},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
use std::{collections::HashMap, time::Instant};
|
||||
|
||||
use log::{error, info, warn};
|
||||
use battlesnake_game_types::{
|
||||
compact_representation::standard::CellBoard4Snakes11x11,
|
||||
types::{
|
||||
build_snake_id_map, LengthGettableGame, Move, RandomReasonableMovesGame,
|
||||
ReasonableMovesGame, SimulableGame, SimulatorInstruments, SnakeIDMap, SnakeId,
|
||||
VictorDeterminableGame, YouDeterminableGame,
|
||||
},
|
||||
wire_representation::Game,
|
||||
};
|
||||
use log::{error, info};
|
||||
use ordered_float::OrderedFloat;
|
||||
use rand::{prelude::*, thread_rng};
|
||||
use rocket::time::{ext::NumericalDuration, Duration};
|
||||
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
|
||||
@ -43,190 +44,99 @@ pub fn info() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct GameInfo {
|
||||
calculation_time: Arc<Mutex<Duration>>,
|
||||
token_mapping: Arc<BTreeMap<String, SnakeToken>>,
|
||||
my_token: SnakeToken,
|
||||
tree: Arc<Mutex<Option<Node>>>,
|
||||
#[derive(Debug)]
|
||||
pub struct GameState {
|
||||
calculation_time: Duration,
|
||||
snake_id_map: SnakeIDMap,
|
||||
}
|
||||
|
||||
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) {
|
||||
#[must_use]
|
||||
pub fn start(game: &Game) -> GameState {
|
||||
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)),
|
||||
},
|
||||
);
|
||||
let snake_id_map = build_snake_id_map(game);
|
||||
let calculation_time = (game.game.timeout / 2).milliseconds();
|
||||
|
||||
GameState {
|
||||
calculation_time,
|
||||
snake_id_map,
|
||||
}
|
||||
}
|
||||
|
||||
// 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()));
|
||||
pub fn end(game: &Game, state: GameState) {
|
||||
std::mem::drop(state);
|
||||
info!("GAME OVER after {} turns", game.turn);
|
||||
}
|
||||
|
||||
// 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> {
|
||||
pub fn get_move(game: Game, state: &mut GameState, start: &Instant) -> Move {
|
||||
let calc_start = Instant::now();
|
||||
if calc_start - *start > Duration::from_millis(10) {
|
||||
if calc_start - *start > 10.milliseconds() {
|
||||
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;
|
||||
}
|
||||
let deadline = *start + state.calculation_time;
|
||||
|
||||
// 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 name = game.you.name.clone();
|
||||
let turn = game.turn;
|
||||
let solo = game.game.ruleset.name == "solo";
|
||||
let Ok(board) = CellBoard4Snakes11x11::convert_from_game(game, &state.snake_id_map) else {
|
||||
error!("Unable to fit board");
|
||||
return Move::Down;
|
||||
};
|
||||
|
||||
let mut tree = Node {
|
||||
statistic: Statistics {
|
||||
played: 0,
|
||||
won: HashMap::new(),
|
||||
},
|
||||
child_statistics: HashMap::new(),
|
||||
childs: HashMap::new(),
|
||||
};
|
||||
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);
|
||||
if solo {
|
||||
tree.monte_carlo_solo_step(&board);
|
||||
} else {
|
||||
let _ = tree.monte_carlo_step(&mut board, &deadline);
|
||||
tree.monte_carlo_step(&board);
|
||||
}
|
||||
}
|
||||
|
||||
let actions = tree.child_statistics.entry(game_info.my_token).or_default();
|
||||
let actions = tree.child_statistics.entry(*board.you_id()).or_default();
|
||||
|
||||
info!("actions {}: {actions:?}", you.name);
|
||||
info!("actions {}: {actions:?}", name);
|
||||
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let chosen = actions
|
||||
.iter()
|
||||
.max_by_key(|(_, stat)| OrderedFloat(stat.won as f64 / stat.played as f64))
|
||||
.max_by_key(|(_, stat)| stat.played)
|
||||
.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);
|
||||
.or_else(|| {
|
||||
board
|
||||
.random_reasonable_move_for_each_snake(&mut thread_rng())
|
||||
.find(|(snake_id, _)| snake_id == board.you_id())
|
||||
.map(|(_, direction)| direction)
|
||||
})
|
||||
.unwrap_or(Move::Down);
|
||||
|
||||
info!(
|
||||
"DIRECTION {turn}: {chosen:?} after {}ms ({})",
|
||||
"DIRECTION {turn}: {chosen:?} after {}ms ({name})",
|
||||
start.elapsed().as_millis(),
|
||||
you.name,
|
||||
);
|
||||
Some(Action {
|
||||
r#move: chosen,
|
||||
shout: None,
|
||||
})
|
||||
chosen
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Instruments;
|
||||
|
||||
impl SimulatorInstruments for Instruments {
|
||||
fn observe_simulation(&self, _duration: std::time::Duration) {}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
@ -234,7 +144,7 @@ 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>,
|
||||
won: HashMap<SnakeId, usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
@ -243,46 +153,44 @@ struct ActionStatistic {
|
||||
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>,
|
||||
child_statistics: HashMap<SnakeId, HashMap<Move, ActionStatistic>>,
|
||||
childs: HashMap<[Option<(Move, u16)>; 4], 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;
|
||||
fn monte_carlo_step(&mut self, board: &CellBoard4Snakes11x11) -> Option<SnakeId> {
|
||||
let stop_condition = CellBoard4Snakes11x11::is_over;
|
||||
let winner = if stop_condition(board) {
|
||||
if Instant::now() >= *deadline {
|
||||
return Err(DeadlineError);
|
||||
}
|
||||
board.snakes().next()
|
||||
board.get_winner()
|
||||
} 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);
|
||||
let mut board = *board;
|
||||
while !stop_condition(&board) {
|
||||
let rng = &mut thread_rng();
|
||||
let moves = board.random_reasonable_move_for_each_snake(rng);
|
||||
let (_, new_board) = board
|
||||
.simulate_with_moves(
|
||||
&Instruments,
|
||||
moves.map(|(snake_id, direction)| (snake_id, [direction])),
|
||||
)
|
||||
.next()
|
||||
.unwrap();
|
||||
board = new_board;
|
||||
}
|
||||
board.snakes().next()
|
||||
board.get_winner()
|
||||
} else {
|
||||
// select a node to simulate
|
||||
let possible_actions = board.possible_actions();
|
||||
let possible_actions = board.reasonable_moves_for_each_snake();
|
||||
|
||||
let actions = possible_actions
|
||||
.iter()
|
||||
.filter_map(|(token, actions)| {
|
||||
let statistics = self.child_statistics.entry(*token).or_default();
|
||||
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 {
|
||||
@ -297,20 +205,28 @@ impl Node {
|
||||
);
|
||||
OrderedFloat(exploitation + exploration)
|
||||
})?;
|
||||
Some((*token, selected))
|
||||
Some((token, [selected]))
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
board.simulate_actions(&actions, &mut thread_rng());
|
||||
let map_actions = actions
|
||||
let (_, board) = board
|
||||
.simulate_with_moves(&Instruments, actions.iter().copied())
|
||||
.next()
|
||||
.unwrap();
|
||||
let mut map_actions = [None; 4];
|
||||
for (i, action) in map_actions.iter_mut().enumerate() {
|
||||
*action = actions
|
||||
.iter()
|
||||
.map(|(&snake, &action)| (snake, (action, board.snake_length(snake).unwrap_or(0))))
|
||||
.collect();
|
||||
.find(|(snake_id, _)| snake_id.as_usize() == i)
|
||||
.and_then(|(snake_id, moves)| {
|
||||
Some((*moves.first()?, board.get_length(snake_id)))
|
||||
});
|
||||
}
|
||||
let winner = self
|
||||
.childs
|
||||
.entry(map_actions)
|
||||
.or_default()
|
||||
.monte_carlo_step(board, deadline)?;
|
||||
.monte_carlo_step(&board);
|
||||
|
||||
// update child statistics
|
||||
for (token, action) in &actions {
|
||||
@ -318,7 +234,7 @@ impl Node {
|
||||
.child_statistics
|
||||
.entry(*token)
|
||||
.or_default()
|
||||
.entry(*action)
|
||||
.entry(action[0])
|
||||
.or_default();
|
||||
entry.played += 1;
|
||||
if Some(*token) == winner {
|
||||
@ -336,104 +252,108 @@ impl Node {
|
||||
.and_modify(|won| *won += 1)
|
||||
.or_insert(1);
|
||||
}
|
||||
Ok(winner)
|
||||
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 {
|
||||
fn monte_carlo_solo_step(&mut self, board: &CellBoard4Snakes11x11) -> u16 {
|
||||
let stop_condition = |board: &CellBoard4Snakes11x11| board.alive_snake_count() == 0;
|
||||
let winner = 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
|
||||
let mut board = *board;
|
||||
while !stop_condition(&board) {
|
||||
let moves =
|
||||
board
|
||||
.reasonable_moves_for_each_snake()
|
||||
.filter_map(|(snake_id, moves)| {
|
||||
Some((snake_id, [*moves.choose(&mut thread_rng())?]))
|
||||
});
|
||||
if Instant::now() >= *deadline {
|
||||
return Err(DeadlineError);
|
||||
let Some((_, new_board)) = board.simulate_with_moves(&Instruments, moves).next()
|
||||
else {
|
||||
break;
|
||||
};
|
||||
if stop_condition(&new_board) {
|
||||
break;
|
||||
}
|
||||
lengths
|
||||
board = new_board;
|
||||
}
|
||||
let winner = board.get_length(board.you_id());
|
||||
winner
|
||||
} else {
|
||||
// select a node to simulate
|
||||
let possible_actions = board.possible_actions();
|
||||
let possible_actions = board.reasonable_moves_for_each_snake();
|
||||
|
||||
let actions = possible_actions
|
||||
.iter()
|
||||
.filter_map(|(token, actions)| {
|
||||
let statistics = self.child_statistics.entry(*token).or_default();
|
||||
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;
|
||||
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,
|
||||
);
|
||||
)
|
||||
* 11.0
|
||||
* 11.0;
|
||||
OrderedFloat(exploitation + exploration)
|
||||
})?;
|
||||
Some((*token, selected))
|
||||
Some((token, [selected]))
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if Instant::now() >= *deadline {
|
||||
return Err(DeadlineError);
|
||||
}
|
||||
board.simulate_actions(&actions, &mut thread_rng());
|
||||
let map_actions = actions
|
||||
let (_, new_board) = board
|
||||
.simulate_with_moves(&Instruments, actions.iter().copied())
|
||||
.next()
|
||||
.unwrap();
|
||||
let mut map_actions = [None; 4];
|
||||
for (i, action) in map_actions.iter_mut().enumerate() {
|
||||
*action = actions
|
||||
.iter()
|
||||
.map(|(&snake, &action)| (snake, (action, board.snake_length(snake).unwrap_or(0))))
|
||||
.collect();
|
||||
let lengths = self
|
||||
.childs
|
||||
.find(|(snake_id, _)| snake_id.as_usize() == i)
|
||||
.and_then(|(snake_id, moves)| {
|
||||
Some((*moves.first()?, new_board.get_length(snake_id)))
|
||||
});
|
||||
}
|
||||
let winner = if stop_condition(&new_board) {
|
||||
board.get_length(board.you_id())
|
||||
} else {
|
||||
self.childs
|
||||
.entry(map_actions)
|
||||
.or_default()
|
||||
.monte_carlo_step_solo(board, deadline)?;
|
||||
.monte_carlo_solo_step(&new_board)
|
||||
};
|
||||
|
||||
// update child statistics
|
||||
for (token, action) in &actions {
|
||||
let entry = self
|
||||
.child_statistics
|
||||
.entry(*token)
|
||||
.entry(*new_board.you_id())
|
||||
.or_default()
|
||||
.entry(*action)
|
||||
.entry(
|
||||
actions
|
||||
.iter()
|
||||
.find(|(snake_id, _)| snake_id == new_board.you_id())
|
||||
.map(|(_, action)| action[0])
|
||||
.unwrap(),
|
||||
)
|
||||
.or_default();
|
||||
entry.played += 1;
|
||||
if let Some(length) = lengths.get(token) {
|
||||
entry.won += length;
|
||||
}
|
||||
}
|
||||
entry.won += usize::from(winner);
|
||||
|
||||
lengths
|
||||
winner
|
||||
};
|
||||
self.statistic.played += 1;
|
||||
for (token, length) in &lengths {
|
||||
self.statistic
|
||||
.won
|
||||
.entry(*token)
|
||||
.and_modify(|won| *won += length)
|
||||
.or_insert(*length);
|
||||
}
|
||||
Ok(lengths)
|
||||
.entry(*board.you_id())
|
||||
.and_modify(|won| *won += usize::from(winner))
|
||||
.or_insert_with(|| usize::from(winner));
|
||||
winner
|
||||
}
|
||||
}
|
||||
|
@ -1,57 +1,77 @@
|
||||
#![allow(clippy::needless_pass_by_value)]
|
||||
use std::{env, sync::Arc, time::Instant};
|
||||
|
||||
use battlesnake::{logic, Action, Direction, GameState};
|
||||
use battlesnake::{
|
||||
logic::{self, GameState},
|
||||
Response,
|
||||
};
|
||||
use battlesnake_game_types::{types::Move, wire_representation::Game};
|
||||
use dashmap::DashMap;
|
||||
use log::{error, info};
|
||||
use rocket::{
|
||||
fairing::AdHoc, get, http::Status, launch, post, routes, serde::json::Json, tokio::task,
|
||||
fairing::AdHoc, get, http::Status, launch, post, routes, serde::json::Json, tokio::task, State,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use std::{env, time::Instant};
|
||||
|
||||
type States = Arc<DashMap<(String, String), GameState>>;
|
||||
|
||||
#[get("/")]
|
||||
fn handle_index() -> Json<Value> {
|
||||
Json(logic::info())
|
||||
}
|
||||
|
||||
#[post("/start", format = "json", data = "<start_req>")]
|
||||
fn handle_start(start_req: Json<GameState>) -> Status {
|
||||
logic::start(
|
||||
&start_req.game,
|
||||
start_req.turn,
|
||||
&start_req.board,
|
||||
&start_req.you,
|
||||
);
|
||||
#[post("/start", format = "json", data = "<game>")]
|
||||
fn handle_start(state: &State<States>, game: Json<Game>) -> Status {
|
||||
if state
|
||||
.insert(
|
||||
(game.game.id.clone(), game.you.id.clone()),
|
||||
logic::start(&game),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
error!("re-started game");
|
||||
}
|
||||
|
||||
Status::Ok
|
||||
}
|
||||
|
||||
#[post("/move", format = "json", data = "<move_req>")]
|
||||
async fn handle_move(move_req: Json<GameState>) -> Json<Action> {
|
||||
#[post("/move", format = "json", data = "<game>")]
|
||||
async fn handle_move(state: &State<States>, game: Json<Game>) -> Json<Response> {
|
||||
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,
|
||||
let state = (*state).clone();
|
||||
let action = task::spawn_blocking(move || {
|
||||
let mut game_state = state.get_mut(&(game.game.id.clone(), game.you.id.clone()));
|
||||
while game_state.is_none() {
|
||||
error!("move request without previous start");
|
||||
if state
|
||||
.insert(
|
||||
(game.game.id.clone(), game.you.id.clone()),
|
||||
logic::start(&game),
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
error!("re-started game");
|
||||
}
|
||||
game_state = state.get_mut(&(game.game.id.clone(), game.you.id.clone()));
|
||||
}
|
||||
let Some(mut game_state) = game_state else {
|
||||
std::mem::drop(game_state);
|
||||
unreachable!()
|
||||
};
|
||||
logic::get_move(game.0, &mut game_state, &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)
|
||||
.unwrap_or(Move::Up);
|
||||
Json(Response::new(action))
|
||||
}
|
||||
|
||||
#[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);
|
||||
#[post("/end", format = "json", data = "<game>")]
|
||||
fn handle_end(state: &State<States>, game: Json<Game>) -> Status {
|
||||
if let Some((_key, game_state)) = state.remove(&(game.game.id.clone(), game.you.id.clone())) {
|
||||
logic::end(&game, game_state);
|
||||
} else {
|
||||
error!("ended game without state");
|
||||
}
|
||||
|
||||
Status::Ok
|
||||
}
|
||||
@ -85,4 +105,5 @@ fn rocket() -> _ {
|
||||
"/",
|
||||
routes![handle_index, handle_start, handle_move, handle_end],
|
||||
)
|
||||
.manage(States::new(DashMap::new()))
|
||||
}
|
||||
|
@ -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]
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user