From 88430f31c73f469086b68f2b77d1e1ba5f9178e7 Mon Sep 17 00:00:00 2001 From: Justin Worthe Date: Mon, 22 Apr 2019 21:50:00 +0200 Subject: More minimal game state I'd prefer to start with just the state that I need, and progressively readd the bits that I've skipped as I find I need them, or as the competition evolves. --- src/command.rs | 41 +------ src/game.rs | 61 +++++++--- src/geometry.rs | 2 + src/geometry/direction.rs | 56 +++++++++ src/json.rs | 35 +++--- src/main.rs | 284 ++-------------------------------------------- src/strategy.rs | 6 + 7 files changed, 135 insertions(+), 350 deletions(-) create mode 100644 src/geometry/direction.rs create mode 100644 src/strategy.rs (limited to 'src') diff --git a/src/command.rs b/src/command.rs index 06dd400..99de608 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,4 +1,5 @@ use std::fmt; +use crate::geometry::Direction; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Command { @@ -19,43 +20,3 @@ impl fmt::Display for Command { } } } - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum Direction { - North, - NorthEast, - East, - SouthEast, - South, - SouthWest, - West, - NorthWest, -} - -impl fmt::Display for Direction { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - use Direction::*; - let s = match self { - North => "N", - NorthEast => "NE", - East => "E", - SouthEast => "SE", - South => "S", - SouthWest => "SW", - West => "W", - NorthWest => "NW", - }; - f.write_str(s) - } -} - -impl Direction { - pub fn is_diagonal(&self) -> bool { - use Direction::*; - - match self { - NorthEast | SouthEast | SouthWest | NorthWest => true, - _ => false, - } - } -} diff --git a/src/game.rs b/src/game.rs index 492eaf3..70960aa 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,6 +1,7 @@ use crate::geometry::*; +use crate::json; -struct GameBoard { +pub struct GameBoard { player: Player, opponent: Player, powerups: Vec, @@ -13,24 +14,14 @@ struct Player { } struct Worm { - id: i32, health: i32, - position: Point2d, - digging_range: u32, - movement_range: u32, - // This is unnecessary for now, but necessary later. I know - // for sure for the first round that all the worms will do the - // same damage and there isn't any way to change it. - weapon: Option, -} - -struct Weapon { - damage: u32, - range: u32, + position: Point2d, + weapon_damage: i32, + weapon_range: u8 } enum Powerup { - Health(Point2d, i32) + Health(Point2d, i32) } struct Map { @@ -44,3 +35,43 @@ enum CellType { Dirt, DeepSpace, } + + +impl GameBoard { + pub fn new(json: json::State) -> GameBoard { + let commando_damage = json.my_player.worms[0].weapon.damage; + let commando_range = json.my_player.worms[0].weapon.range; + + GameBoard { + player: Player { + active_worm: json.active_worm_index().unwrap(), + worms: json.my_player.worms.iter().map(|w| Worm { + health: w.health, + position: Point2d::new(w.position.x, w.position.y), + weapon_damage: commando_damage, + weapon_range: commando_range + }).collect() + }, + opponent: Player { + active_worm: 0, + worms: json.opponents.iter().flat_map(|o| &o.worms).map(|w| Worm { + health: w.health, + position: Point2d::new(w.position.x, w.position.y), + weapon_damage: commando_damage, + weapon_range: commando_range + }).collect() + }, + powerups: json.map.iter().flatten().filter_map(|c| { + c.powerup.clone().map(|p| Powerup::Health(Point2d::new(c.x, c.y), p.value)) + }).collect(), + map: Map { + size: json.map_size, + cells: json.map.iter().flatten().map(|c| match c.cell_type { + json::CellType::Air => CellType::Air, + json::CellType::Dirt => CellType::Dirt, + json::CellType::DeepSpace => CellType::DeepSpace, + }).collect() + } + } + } +} diff --git a/src/geometry.rs b/src/geometry.rs index 9e541ef..dc8b096 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -4,3 +4,5 @@ mod point; pub use self::point::*; mod rect; pub use self::rect::*; +mod direction; +pub use self::direction::*; diff --git a/src/geometry/direction.rs b/src/geometry/direction.rs new file mode 100644 index 0000000..84fe785 --- /dev/null +++ b/src/geometry/direction.rs @@ -0,0 +1,56 @@ +use std::fmt; +use crate::geometry::vec::Vec2d; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Direction { + North, + NorthEast, + East, + SouthEast, + South, + SouthWest, + West, + NorthWest, +} + +impl fmt::Display for Direction { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + use Direction::*; + let s = match self { + North => "N", + NorthEast => "NE", + East => "E", + SouthEast => "SE", + South => "S", + SouthWest => "SW", + West => "W", + NorthWest => "NW", + }; + f.write_str(s) + } +} + +impl Direction { + pub fn is_diagonal(&self) -> bool { + use Direction::*; + + match self { + NorthEast | SouthEast | SouthWest | NorthWest => true, + _ => false, + } + } + + pub fn as_vec(&self) -> Vec2d { + use Direction::*; + match self { + North => Vec2d::new(0, -1), + NorthEast => Vec2d::new(1, -1), + East => Vec2d::new(1, 0), + SouthEast => Vec2d::new(1, 1), + South => Vec2d::new(0, 1), + SouthWest => Vec2d::new(-1, 1), + West => Vec2d::new(-1, 0), + NorthWest => Vec2d::new(-1, -1), + } + } +} diff --git a/src/json.rs b/src/json.rs index 3046b7c..5a8267f 100644 --- a/src/json.rs +++ b/src/json.rs @@ -19,7 +19,7 @@ pub fn read_state_from_json_file(filename: &str) -> Result> { pub struct State { pub current_round: u32, pub max_rounds: u32, - pub map_size: u32, + pub map_size: u8, pub current_worm_id: i32, pub consecutive_do_nothing_count: u32, pub my_player: Player, @@ -68,8 +68,8 @@ pub struct OpponentWorm { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Cell { - pub x: u32, - pub y: u32, + pub x: i8, + pub y: i8, #[serde(rename = "type")] pub cell_type: CellType, pub occupier: Option, @@ -126,51 +126,46 @@ pub enum PowerupType { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Position { - pub x: u32, - pub y: u32, + pub x: i8, + pub y: i8, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Weapon { - pub damage: u32, - pub range: u32, + pub damage: i32, + pub range: u8, } impl State { - pub fn active_worm(&self) -> Option<&PlayerWorm> { + pub fn active_worm_index(&self) -> Option { self.my_player .worms .iter() - .find(|w| w.id == self.current_worm_id) - } - - pub fn cell_at(&self, pos: &Position) -> Option<&Cell> { - self.map - .iter() - .flatten() - .find(|c| c.x == pos.x && c.y == pos.y) + .position(|w| w.id == self.current_worm_id) } } impl Position { - pub fn west(&self, distance: u32) -> Option { + pub fn west(&self, distance: i8) -> Option { self.x .checked_sub(distance) + .filter(|&x| x >= 0) .map(|x| Position { x, y: self.y }) } - pub fn east(&self, distance: u32, max: u32) -> Option { + pub fn east(&self, distance: i8, max: i8) -> Option { self.x .checked_add(distance) .filter(|&x| x < max) .map(|x| Position { x, y: self.y }) } - pub fn north(&self, distance: u32) -> Option { + pub fn north(&self, distance: i8) -> Option { self.y .checked_sub(distance) + .filter(|&y| y >= 0) .map(|y| Position { x: self.x, y }) } - pub fn south(&self, distance: u32, max: u32) -> Option { + pub fn south(&self, distance: i8, max: i8) -> Option { self.y .checked_add(distance) .filter(|&y| y < max) diff --git a/src/main.rs b/src/main.rs index cba96f5..216915c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,16 +7,21 @@ mod command; mod json; mod geometry; mod game; +mod strategy; -use command::*; -use json::*; +use command::Command; +use strategy::choose_move; fn main() { for line in stdin().lock().lines() { let round_number = line.expect("Failed to read line from stdin: {}"); + let command = - match read_state_from_json_file(&format!("./rounds/{}/state.json", round_number)) { - Ok(state) => choose_command(state), + match json::read_state_from_json_file(&format!("./rounds/{}/state.json", round_number)) { + Ok(json_state) => { + let game_board = game::GameBoard::new(json_state); + choose_move(&game_board) + }, Err(e) => { eprintln!("WARN: State file could not be parsed: {}", e); Command::DoNothing @@ -25,274 +30,3 @@ fn main() { println!("C;{};{}", round_number, command); } } - -fn choose_command(state: State) -> Command { - match state.active_worm() { - Some(worm) => { - if let Some(direction) = find_worm_in_firing_distance(&state, worm) { - Command::Shoot(direction) - } else { - let choices = valid_adjacent_positions(&state, &worm.position); - let choice = choices - .choose(&mut rand::thread_rng()) - .expect("No valid directions to move in"); - let chosen_cell = state.cell_at(&choice); - - match chosen_cell.map(|c| &c.cell_type) { - Some(CellType::Air) => Command::Move(choice.x, choice.y), - Some(CellType::Dirt) => Command::Dig(choice.x, choice.y), - Some(CellType::DeepSpace) | None => Command::DoNothing, - } - } - } - None => { - eprintln!("WARN: The active worm did not appear in the state file"); - Command::DoNothing - } - } -} - -fn find_worm_in_firing_distance(state: &State, worm: &PlayerWorm) -> Option { - let directions: [(Direction, Box Option>); 8] = [ - (Direction::West, Box::new(|p, d| p.west(d))), - (Direction::NorthWest, Box::new(|p, d| p.north(d).and_then(|p| p.west(d)))), - (Direction::North, Box::new(|p, d| p.north(d))), - (Direction::NorthEast, Box::new(|p, d| p.north(d).and_then(|p| p.east(d, state.map_size)))), - (Direction::East, Box::new(|p, d| p.east(d, state.map_size))), - (Direction::SouthEast, Box::new(|p, d| p.south(d, state.map_size).and_then(|p| p.east(d, state.map_size)))), - (Direction::South, Box::new(|p, d| p.south(d, state.map_size))), - (Direction::SouthWest, Box::new(|p, d| p.south(d, state.map_size).and_then(|p| p.west(d)))), - ]; - - for (dir, dir_fn) in &directions { - let range = adjust_range_for_diagonals(dir, worm.weapon.range); - - for distance in 1..=range { - let target = dir_fn(&worm.position, distance); - match target.and_then(|t| state.cell_at(&t)) { - Some(Cell { - occupier: Some(CellWorm::OpponentWorm { .. }), - .. - }) => return Some(*dir), - Some(Cell { - cell_type: CellType::Air, - .. - }) => continue, - _ => break, - } - } - } - None -} - -fn adjust_range_for_diagonals(dir: &Direction, straight_range: u32) -> u32 { - if dir.is_diagonal() { - ((straight_range as f32 + 1.) / 2f32.sqrt()).floor() as u32 - } else { - straight_range - } -} - -fn valid_adjacent_positions(state: &State, pos: &Position) -> Vec { - let choices = [ - pos.west(1), - pos.west(1).and_then(|p| p.north(1)), - pos.north(1), - pos.north(1).and_then(|p| p.east(1, state.map_size)), - pos.east(1, state.map_size), - pos.east(1, state.map_size) - .and_then(|p| p.south(1, state.map_size)), - pos.south(1, state.map_size), - pos.south(1, state.map_size).and_then(|p| p.west(1)), - ]; - choices.iter().flatten().cloned().collect() -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn adjacent_positions_give_valid_positions() { - let dummy_state = State { - current_round: 0, - max_rounds: 0, - map_size: 3, - current_worm_id: 0, - consecutive_do_nothing_count: 0, - my_player: Player { - id: 0, - score: 0, - health: 0, - worms: Vec::new(), - }, - opponents: Vec::new(), - map: Vec::new(), - }; - - assert_eq!( - 3, - valid_adjacent_positions(&dummy_state, &Position { x: 0, y: 0 }).len() - ); - assert_eq!( - 5, - valid_adjacent_positions(&dummy_state, &Position { x: 1, y: 0 }).len() - ); - assert_eq!( - 3, - valid_adjacent_positions(&dummy_state, &Position { x: 2, y: 0 }).len() - ); - assert_eq!( - 5, - valid_adjacent_positions(&dummy_state, &Position { x: 0, y: 1 }).len() - ); - assert_eq!( - 8, - valid_adjacent_positions(&dummy_state, &Position { x: 1, y: 1 }).len() - ); - assert_eq!( - 5, - valid_adjacent_positions(&dummy_state, &Position { x: 2, y: 1 }).len() - ); - assert_eq!( - 3, - valid_adjacent_positions(&dummy_state, &Position { x: 0, y: 2 }).len() - ); - assert_eq!( - 5, - valid_adjacent_positions(&dummy_state, &Position { x: 1, y: 2 }).len() - ); - assert_eq!( - 3, - valid_adjacent_positions(&dummy_state, &Position { x: 2, y: 2 }).len() - ); - } - - #[test] - fn range_adjustment_matches_examples() { - assert_eq!(1, adjust_range_for_diagonals(&Direction::East, 1)); - assert_eq!(2, adjust_range_for_diagonals(&Direction::East, 2)); - assert_eq!(3, adjust_range_for_diagonals(&Direction::East, 3)); - assert_eq!(4, adjust_range_for_diagonals(&Direction::East, 4)); - - assert_eq!(1, adjust_range_for_diagonals(&Direction::SouthEast, 1)); - assert_eq!(2, adjust_range_for_diagonals(&Direction::SouthEast, 2)); - assert_eq!(2, adjust_range_for_diagonals(&Direction::SouthEast, 3)); - assert_eq!(3, adjust_range_for_diagonals(&Direction::SouthEast, 4)); - } - - mod find_worm_in_firing_distance { - use super::super::*; - - fn worm_shooting_dummy_state() -> (State, PlayerWorm) { - let dummy_state = State { - current_round: 0, - max_rounds: 0, - map_size: 5, - current_worm_id: 0, - consecutive_do_nothing_count: 0, - my_player: Player { - id: 0, - score: 0, - health: 0, - worms: Vec::new(), - }, - opponents: Vec::new(), - map: vec![Vec::new()], - }; - let active_worm = PlayerWorm { - id: 0, - health: 100, - position: Position { x: 2, y: 2 }, - digging_range: 1, - movement_range: 1, - weapon: Weapon { - range: 3, - damage: 1, - }, - }; - - (dummy_state, active_worm) - } - - #[test] - fn finds_a_worm_that_can_be_shot() { - let (mut dummy_state, active_worm) = worm_shooting_dummy_state(); - dummy_state.map[0].push(Cell { - x: 3, - y: 2, - cell_type: CellType::Air, - occupier: None, - powerup: None, - }); - dummy_state.map[0].push(Cell { - x: 4, - y: 2, - cell_type: CellType::Air, - occupier: Some(CellWorm::OpponentWorm { - id: 0, - player_id: 1, - health: 0, - position: Position { x: 4, y: 2 }, - digging_range: 1, - movement_range: 1, - }), - powerup: None, - }); - - let firing_dir = find_worm_in_firing_distance(&dummy_state, &active_worm); - assert_eq!(Some(Direction::East), firing_dir); - } - - #[test] - fn worm_cant_shoot_through_dirt() { - let (mut dummy_state, active_worm) = worm_shooting_dummy_state(); - dummy_state.map[0].push(Cell { - x: 3, - y: 2, - cell_type: CellType::Dirt, - occupier: None, - powerup: None, - }); - dummy_state.map[0].push(Cell { - x: 4, - y: 2, - cell_type: CellType::Air, - occupier: Some(CellWorm::OpponentWorm { - id: 0, - player_id: 1, - health: 0, - position: Position { x: 4, y: 2 }, - digging_range: 1, - movement_range: 1, - }), - powerup: None, - }); - - let firing_dir = find_worm_in_firing_distance(&dummy_state, &active_worm); - assert_eq!(None, firing_dir); - } - - #[test] - fn identifies_lack_of_worms_to_shoot() { - let (mut dummy_state, active_worm) = worm_shooting_dummy_state(); - dummy_state.map[0].push(Cell { - x: 3, - y: 2, - cell_type: CellType::Air, - occupier: None, - powerup: None, - }); - dummy_state.map[0].push(Cell { - x: 4, - y: 2, - cell_type: CellType::Air, - occupier: None, - powerup: None, - }); - - let firing_dir = find_worm_in_firing_distance(&dummy_state, &active_worm); - assert_eq!(None, firing_dir); - } - } -} diff --git a/src/strategy.rs b/src/strategy.rs new file mode 100644 index 0000000..6ee0dce --- /dev/null +++ b/src/strategy.rs @@ -0,0 +1,6 @@ +use crate::command::Command; +use crate::game::GameBoard; + +pub fn choose_move(state: &GameBoard) -> Command { + Command::DoNothing +} -- cgit v1.2.3