diff --git a/.gitignore b/.gitignore index 35dec78..d3ccc21 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,10 @@ Cargo.lock *~* /start.zsh +perf.* +flamegraph/ +flamegraph.svg + # Python venv/ __pycache__/ diff --git a/Cargo.toml b/Cargo.toml index 9aa3fe1..a093272 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ license = "MIT" default-run = "server" [dependencies] -tokio = { version = "1.20", features = ["full"] } +tokio = { version = "1.35", features = ["full"] } warp = { version ="0.3", default_features = false } rand = { version = "0.8", default_features = false, features = ["small_rng", "getrandom", "alloc"] } serde = { version = "1.0", features = ["derive"] } @@ -27,6 +27,12 @@ env_logger = { version = "0.10", default_features = false } [dev-dependencies] criterion = { version = "0.5", features = ["async_tokio"] } +[profile.release] +lto = "thin" +opt-level = 3 +# strip = true +debug = true + [[bench]] name = "benchmarks" harness = false @@ -49,8 +55,3 @@ name = "simulate" harness = false test = false bench = false - -[profile.release] -lto = "thin" -opt-level = 3 -strip = true diff --git a/src/agents/mobility.rs b/src/agents/mobility.rs index b25a04b..374aaf5 100644 --- a/src/agents/mobility.rs +++ b/src/agents/mobility.rs @@ -66,7 +66,7 @@ impl MobilityAgent { let mut grid = game.grid.clone(); for snake in &game.snakes[1..] { if snake.body.len() >= you.body.len() { - for d in Direction::iter() { + for d in Direction::all() { let p = snake.head().apply(d); if grid.has(p) { grid[p].t = CellT::Owned; diff --git a/src/bin/move.rs b/src/bin/move.rs index 1231feb..0b49029 100644 --- a/src/bin/move.rs +++ b/src/bin/move.rs @@ -12,13 +12,13 @@ use clap::Parser; #[clap(version, author, about = "Simulate a move for an agent.")] struct Opts { /// Default configuration. - #[clap(long, default_value_t, value_parser)] + #[clap(long, default_value_t)] config: Agent, /// JSON Game request. #[clap(value_parser = parse_request)] request: GameRequest, /// Time in ms that is subtracted from the game timeouts. - #[clap(long, default_value_t = 200, value_parser)] + #[clap(long, default_value_t = 200)] latency: usize, } diff --git a/src/bin/simulate.rs b/src/bin/simulate.rs index 199d4ab..cabcf40 100644 --- a/src/bin/simulate.rs +++ b/src/bin/simulate.rs @@ -17,34 +17,34 @@ use std::time::Instant; #[clap(version, author, about = "Simulate a game between agents.")] struct Opts { /// Time each snake has for a turn. - #[clap(long, default_value_t = 200, value_parser)] + #[clap(long, default_value_t = 200)] timeout: u64, /// Board height. - #[clap(long, default_value_t = 11, value_parser)] + #[clap(long, default_value_t = 11)] width: usize, /// Board width. - #[clap(long, default_value_t = 11, value_parser)] + #[clap(long, default_value_t = 11)] height: usize, /// Chance new food spawns. - #[clap(long, default_value_t = 0.15, value_parser)] + #[clap(long, default_value_t = 0.15)] food_rate: f64, /// Number of turns after which the hazard expands. - #[clap(short, long, default_value_t = 25, value_parser)] + #[clap(short, long, default_value_t = 25)] shrink_turns: usize, /// Number of games that are played. - #[clap(short, long, default_value_t = 1, value_parser)] + #[clap(short, long, default_value_t = 1)] game_count: usize, /// Swap agent positions to get more accurate results. - #[clap(long, value_parser)] + #[clap(long)] swap: bool, /// Seed for the random number generator. - #[clap(long, default_value_t = 0, value_parser)] + #[clap(long, default_value_t = 0)] seed: u64, /// Start config. #[clap(long, value_parser = parse_request)] init: Option, /// Configurations. - #[clap(value_parser)] + #[clap()] agents: Vec, } @@ -117,6 +117,7 @@ async fn main() { agents.rotate_left(1); } + println!("Agents: {agents:?}"); println!("Result: {wins:?}"); } diff --git a/src/env.rs b/src/env.rs index 790c764..d499b35 100644 --- a/src/env.rs +++ b/src/env.rs @@ -106,11 +106,12 @@ impl Neg for Vec2D { /// The Direction is returned as part of a `MoveResponse`. /// /// The Y-Axis is positive in the up direction, and X-Axis is positive to the right. -#[derive(Serialize, Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[derive(Serialize, Debug, Default, Clone, Copy, Hash, PartialEq, Eq)] #[serde(rename_all = "lowercase")] #[repr(u8)] pub enum Direction { /// Positive Y + #[default] Up, /// Positive X Right, @@ -121,10 +122,8 @@ pub enum Direction { } impl Direction { - pub fn iter() -> impl Iterator { + pub fn all() -> [Self; 4] { [Self::Up, Self::Right, Self::Down, Self::Left] - .iter() - .copied() } /// Returns the invert direction (eg. Left for Right) @@ -138,12 +137,6 @@ impl Direction { } } -impl Default for Direction { - fn default() -> Self { - Self::Up - } -} - impl From for Direction { fn from(p: Vec2D) -> Self { if p.x < 0 { diff --git a/src/floodfill.rs b/src/floodfill.rs index cef48f3..d3461ff 100644 --- a/src/floodfill.rs +++ b/src/floodfill.rs @@ -98,7 +98,7 @@ impl FloodFill { /// Returns if `p` is within the boundaries of the board. pub fn has(&self, p: Vec2D) -> bool { - 0 <= p.x && p.x < self.width as _ && 0 <= p.y && p.y < self.height as _ + p.within(self.width, self.height) } /// Counts the total health of you or the enemies. @@ -120,31 +120,9 @@ impl FloodFill { .count() } - /// Counts the space of you or the enemies weighted by the weight function. - pub fn count_space_weighted f64>( - &self, - id: u8, - mut weight: F, - ) -> f64 { - self.cells - .iter() - .copied() - .enumerate() - .map(|(i, c)| match c { - FCell::Owned { id: o_id, .. } if o_id == id => weight( - Vec2D::new((i % self.width) as i16, (i / self.width) as i16), - c, - ), - _ => 0.0, - }) - .sum() - } - /// Clears the board so that it can be reused for another floodfill computation. pub fn clear(&mut self) { - for c in &mut self.cells { - *c = FCell::Free; - } + self.cells.fill(FCell::Free); } /// Flood fill combined with ignoring tails depending on distance to head. @@ -206,10 +184,12 @@ impl FloodFill { health, }) = queue.pop_front() { - for p in Direction::iter() - .map(|d| p.apply(d)) - .filter(|&p| grid.has(p)) - { + for d in Direction::all() { + let p = p.apply(d); + if !self.has(p) { + continue; + } + let g_cell = grid[p]; let cell = self[p]; @@ -218,7 +198,8 @@ impl FloodFill { let health = if is_food { 100 } else { - health.saturating_sub(if g_cell.hazard { HAZARD_DAMAGE } else { 1 }) + let cost = if g_cell.hazard { HAZARD_DAMAGE } else { 1 }; + health.saturating_sub(cost) }; // Collect food @@ -275,16 +256,14 @@ impl Index for FloodFill { type Output = FCell; fn index(&self, p: Vec2D) -> &Self::Output { - assert!(0 <= p.x && p.x < self.width as _); - assert!(0 <= p.y && p.y < self.height as _); + debug_assert!(p.within(self.width, self.height)); &self.cells[p.x as usize % self.width + p.y as usize * self.width] } } impl IndexMut for FloodFill { fn index_mut(&mut self, p: Vec2D) -> &mut Self::Output { - assert!(0 <= p.x && p.x < self.width as _); - assert!(0 <= p.y && p.y < self.height as _); + debug_assert!(p.within(self.width, self.height)); &mut self.cells[p.x as usize % self.width + p.y as usize * self.width] } } diff --git a/src/game.rs b/src/game.rs index 46ba83f..f9ea0ff 100644 --- a/src/game.rs +++ b/src/game.rs @@ -315,7 +315,7 @@ impl Game { let mut p = Vec2D::new((p % width) as _, (p / width) as _); let mut body = VecDeque::new(); body.push_front(p); - while let Some(next) = Direction::iter().find_map(|d| { + while let Some(next) = Direction::all().into_iter().find_map(|d| { let next = p.apply(d); (next.within(width, height) && raw_cells[(next.x + next.y * width as i16) as usize] diff --git a/src/grid.rs b/src/grid.rs index 343f747..61ac769 100644 --- a/src/grid.rs +++ b/src/grid.rs @@ -159,7 +159,7 @@ impl Grid { return Some(make_path(&data, target)); } - for d in Direction::iter() { + for d in Direction::all() { let neighbor = front.apply(d); let mut neighbor_cost = cost + 1.0; if self.is_hazardous(neighbor) { @@ -170,7 +170,7 @@ impl Grid { } if self.has(neighbor) && self[neighbor].t != CellT::Owned { - let cost_so_far = data.get(&neighbor).map_or(f64::MAX, |(_, c)| *c); + let cost_so_far = data.get(&neighbor).map_or(f64::MAX, |(_, c)| *c); if neighbor_cost < cost_so_far { data.insert(neighbor, (front, neighbor_cost)); // queue does not accept float @@ -189,16 +189,14 @@ impl Index for Grid { type Output = Cell; fn index(&self, p: Vec2D) -> &Self::Output { - assert!(0 <= p.x && p.x < self.width as _); - assert!(0 <= p.y && p.y < self.height as _); + debug_assert!(p.within(self.width, self.height)); &self.cells[p.x as usize + p.y as usize * self.width] } } impl IndexMut for Grid { fn index_mut(&mut self, p: Vec2D) -> &mut Self::Output { - assert!(0 <= p.x && p.x < self.width as _); - assert!(0 <= p.y && p.y < self.height as _); + debug_assert!(p.within(self.width, self.height)); &mut self.cells[p.x as usize + p.y as usize * self.width] } } diff --git a/src/lib.rs b/src/lib.rs index 1b0c03e..e1a9315 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ pub mod env; pub mod floodfill; pub mod game; pub mod grid; -mod savegame; pub mod search; mod util; diff --git a/src/savegame.rs b/src/savegame.rs deleted file mode 100644 index 8856042..0000000 --- a/src/savegame.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::path::Path; -use tokio::fs::{self, OpenOptions}; -use tokio::io::AsyncWriteExt; - -use crate::env::*; - -pub async fn save(game_req: GameRequest, log_dir: &Path) { - if !log_dir.exists() { - fs::create_dir(&log_dir) - .await - .expect("Logging directory could not be created!"); - } - - let filename = format!("{}.{}.json", game_req.game.id, game_req.you.id); - let mut file = OpenOptions::new() - .write(true) - .create(true) - .append(true) - .open(log_dir.join(filename)) - .await - .expect("Could not create/open save game!"); - - let data = serde_json::to_vec(&game_req).unwrap(); - file.write_all(&data).await.unwrap(); -} diff --git a/src/search/alphabeta.rs b/src/search/alphabeta.rs index 9179ef2..7ab99e7 100644 --- a/src/search/alphabeta.rs +++ b/src/search/alphabeta.rs @@ -55,7 +55,7 @@ async fn async_alphabeta_rec( let mut value = (Direction::Up, LOSS); let mut futures = [None, None, None, None]; - for d in Direction::iter() { + for d in Direction::all() { let game = game.clone(); let heuristic = heuristic.clone(); let actions = [d, Direction::Up, Direction::Up, Direction::Up]; @@ -82,7 +82,7 @@ async fn async_alphabeta_rec( value } else { let mut value = (Direction::Up, WIN); - for d in Direction::iter() { + for d in Direction::all() { let mut actions = actions; actions[ply] = d; let newval = async_alphabeta_rec( @@ -152,7 +152,7 @@ fn alphabeta_rec( } } else if ply == 0 { let mut value = (Direction::Up, LOSS); - for d in Direction::iter() { + for d in Direction::all() { let mut actions = actions; actions[ply] = d; let newval = alphabeta_rec(game, actions, depth, ply + 1, alpha, beta, heuristic); @@ -169,7 +169,7 @@ fn alphabeta_rec( value } else { let mut value = (Direction::Up, WIN); - for d in Direction::iter() { + for d in Direction::all() { let mut actions = actions; actions[ply] = d; let newval = alphabeta_rec(game, actions, depth, ply + 1, alpha, beta, heuristic); diff --git a/src/search/minimax.rs b/src/search/minimax.rs index 7a32713..07001c5 100644 --- a/src/search/minimax.rs +++ b/src/search/minimax.rs @@ -59,7 +59,7 @@ async fn async_max_n_rec( let mut futures = [None, None, None, None]; - for d in Direction::iter() { + for d in Direction::all() { if !game.move_is_valid(0, d) { continue; } @@ -85,7 +85,7 @@ async fn async_max_n_rec( } else { let mut min = 2.0 * WIN; let mut moved = false; - for d in Direction::iter() { + for d in Direction::all() { if !game.move_is_valid(ply as u8, d) { continue; } @@ -158,7 +158,7 @@ fn max_n_rec( } else if ply == 0 { // collect all outcomes instead of max let mut result = [LOSS; 4]; - for d in Direction::iter() { + for d in Direction::all() { if !game.move_is_valid(0, d) { continue; } @@ -170,7 +170,7 @@ fn max_n_rec( } else { let mut min = 2.0 * WIN; let mut moved = false; - for d in Direction::iter() { + for d in Direction::all() { if !game.move_is_valid(ply as u8, d) { continue; } diff --git a/src/util.rs b/src/util.rs index 33631fa..bda5e9e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,4 +1,5 @@ use std::cmp::Ordering; +use std::fmt; use std::mem::MaybeUninit; use std::ops::{Deref, DerefMut}; @@ -95,13 +96,21 @@ impl Deref for FixedVec { fn deref(&self) -> &Self::Target { // TODO: replace with slice_assume_init_ref - unsafe { &*(&self.data as *const [MaybeUninit] as *const [T]) } + let slice = &self.data[..self.len]; + unsafe { &*(slice as *const [MaybeUninit] as *const [T]) } } } impl DerefMut for FixedVec { fn deref_mut(&mut self) -> &mut Self::Target { // TODO: replace with slice_assume_init_mut - unsafe { &mut *(&mut self.data as *mut [MaybeUninit] as *mut [T]) } + let slice = &mut self.data[..self.len]; + unsafe { &mut *(slice as *mut [MaybeUninit] as *mut [T]) } + } +} + +impl fmt::Debug for FixedVec { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_list().entries(self.iter()).finish() } }