summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorJustin Worthe <justin@worthe-it.co.za>2019-04-22 11:19:16 +0200
committerJustin Worthe <justin@worthe-it.co.za>2019-04-22 11:19:16 +0200
commit29a323e0a3bd3ab3e6109b23e15bb5f9e88398e3 (patch)
treea151c612b5993f127d99c29d4c4fdcf252528436 /src
Start the project from the starter bot
Diffstat (limited to 'src')
-rw-r--r--src/command.rs61
-rw-r--r--src/json.rs441
-rw-r--r--src/main.rs296
3 files changed, 798 insertions, 0 deletions
diff --git a/src/command.rs b/src/command.rs
new file mode 100644
index 0000000..06dd400
--- /dev/null
+++ b/src/command.rs
@@ -0,0 +1,61 @@
+use std::fmt;
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Command {
+ Move(u32, u32),
+ Dig(u32, u32),
+ Shoot(Direction),
+ DoNothing,
+}
+
+impl fmt::Display for Command {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ use Command::*;
+ match self {
+ Move(x, y) => write!(f, "move {} {}", x, y),
+ Dig(x, y) => write!(f, "dig {} {}", x, y),
+ Shoot(dir) => write!(f, "shoot {}", dir),
+ DoNothing => write!(f, "nothing"),
+ }
+ }
+}
+
+#[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/json.rs b/src/json.rs
new file mode 100644
index 0000000..3046b7c
--- /dev/null
+++ b/src/json.rs
@@ -0,0 +1,441 @@
+use std::error::Error;
+use std::fs::File;
+use std::io::prelude::*;
+
+use serde::{Deserialize, Serialize};
+use serde_json;
+
+pub fn read_state_from_json_file(filename: &str) -> Result<State, Box<Error>> {
+ let mut file = File::open(filename)?;
+ let mut content = String::new();
+ file.read_to_string(&mut content)?;
+ let state: State = serde_json::from_str(content.as_ref())?;
+
+ Ok(state)
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct State {
+ pub current_round: u32,
+ pub max_rounds: u32,
+ pub map_size: u32,
+ pub current_worm_id: i32,
+ pub consecutive_do_nothing_count: u32,
+ pub my_player: Player,
+ pub opponents: Vec<Opponent>,
+ pub map: Vec<Vec<Cell>>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Player {
+ pub id: i32,
+ pub score: i32,
+ pub health: i32,
+ pub worms: Vec<PlayerWorm>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct PlayerWorm {
+ pub id: i32,
+ pub health: i32,
+ pub position: Position,
+ pub digging_range: u32,
+ pub movement_range: u32,
+ pub weapon: Weapon,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Opponent {
+ pub id: i32,
+ pub score: i32,
+ pub worms: Vec<OpponentWorm>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct OpponentWorm {
+ pub id: i32,
+ pub health: i32,
+ pub position: Position,
+ pub digging_range: u32,
+ pub movement_range: u32,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Cell {
+ pub x: u32,
+ pub y: u32,
+ #[serde(rename = "type")]
+ pub cell_type: CellType,
+ pub occupier: Option<CellWorm>,
+ pub powerup: Option<Powerup>,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+pub enum CellType {
+ Air,
+ Dirt,
+ DeepSpace,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(untagged)]
+#[serde(rename_all = "camelCase")]
+pub enum CellWorm {
+ #[serde(rename_all = "camelCase")]
+ PlayerWorm {
+ id: i32,
+ player_id: i32,
+ health: i32,
+ position: Position,
+ digging_range: u32,
+ movement_range: u32,
+ weapon: Weapon,
+ },
+ #[serde(rename_all = "camelCase")]
+ OpponentWorm {
+ id: i32,
+ player_id: i32,
+ health: i32,
+ position: Position,
+ digging_range: u32,
+ movement_range: u32,
+ },
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Powerup {
+ #[serde(rename = "type")]
+ pub powerup_type: PowerupType,
+ pub value: i32,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+pub enum PowerupType {
+ HealthPack,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Position {
+ pub x: u32,
+ pub y: u32,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Weapon {
+ pub damage: u32,
+ pub range: u32,
+}
+
+impl State {
+ pub fn active_worm(&self) -> Option<&PlayerWorm> {
+ 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)
+ }
+}
+
+impl Position {
+ pub fn west(&self, distance: u32) -> Option<Position> {
+ self.x
+ .checked_sub(distance)
+ .map(|x| Position { x, y: self.y })
+ }
+ pub fn east(&self, distance: u32, max: u32) -> Option<Position> {
+ self.x
+ .checked_add(distance)
+ .filter(|&x| x < max)
+ .map(|x| Position { x, y: self.y })
+ }
+ pub fn north(&self, distance: u32) -> Option<Position> {
+ self.y
+ .checked_sub(distance)
+ .map(|y| Position { x: self.x, y })
+ }
+ pub fn south(&self, distance: u32, max: u32) -> Option<Position> {
+ self.y
+ .checked_add(distance)
+ .filter(|&y| y < max)
+ .map(|y| Position { x: self.x, y })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn example_parses_correctly() {
+ let example = r#"
+{
+ "currentRound": 0,
+ "maxRounds": 200,
+ "mapSize": 33,
+ "currentWormId": 1,
+ "consecutiveDoNothingCount": 0,
+ "myPlayer": {
+ "id": 1,
+ "score": 100,
+ "health": 300,
+ "worms": [
+ {
+ "id": 1,
+ "health": 100,
+ "position": {
+ "x": 24,
+ "y": 29
+ },
+ "weapon": {
+ "damage": 1,
+ "range": 3
+ },
+ "diggingRange": 1,
+ "movementRange": 1
+ }
+ ]
+ },
+ "opponents": [
+ {
+ "id": 2,
+ "score": 100,
+ "worms": [
+ {
+ "id": 1,
+ "health": 100,
+ "position": {
+ "x": 31,
+ "y": 16
+ },
+ "diggingRange": 1,
+ "movementRange": 1
+ }
+ ]
+ }
+ ],
+ "map": [
+ [
+ {
+ "x": 0,
+ "y": 0,
+ "type": "DEEP_SPACE"
+ },
+ {
+ "x": 1,
+ "y": 0,
+ "type": "AIR"
+ },
+ {
+ "x": 2,
+ "y": 0,
+ "type": "DIRT"
+ }
+ ],
+ [
+ {
+ "x": 0,
+ "y": 1,
+ "type": "AIR",
+ "powerup": {
+ "type": "HEALTH_PACK",
+ "value": 5
+ }
+ },
+ {
+ "x": 1,
+ "y": 1,
+ "type": "AIR",
+ "occupier": {
+ "id": 1,
+ "playerId": 2,
+ "health": 100,
+ "position": {
+ "x": 1,
+ "y": 1
+ },
+ "diggingRange": 1,
+ "movementRange": 1
+ }
+ },
+ {
+ "x": 2,
+ "y": 1,
+ "type": "AIR",
+ "occupier": {
+ "id": 1,
+ "playerId": 1,
+ "health": 100,
+ "position": {
+ "x": 2,
+ "y": 1
+ },
+ "weapon": {
+ "damage": 1,
+ "range": 3
+ },
+ "diggingRange": 1,
+ "movementRange": 1
+ }
+ }
+ ]
+ ]
+}"#;
+
+ let expected = State {
+ current_round: 0,
+ max_rounds: 200,
+ map_size: 33,
+ current_worm_id: 1,
+ consecutive_do_nothing_count: 0,
+ my_player: Player {
+ id: 1,
+ score: 100,
+ health: 300,
+ worms: vec![PlayerWorm {
+ id: 1,
+ health: 100,
+ position: Position { x: 24, y: 29 },
+ weapon: Weapon {
+ damage: 1,
+ range: 3,
+ },
+ digging_range: 1,
+ movement_range: 1,
+ }],
+ },
+ opponents: vec![Opponent {
+ id: 2,
+ score: 100,
+ worms: vec![OpponentWorm {
+ id: 1,
+ health: 100,
+ position: Position { x: 31, y: 16 },
+ digging_range: 1,
+ movement_range: 1,
+ }],
+ }],
+ map: vec![
+ vec![
+ Cell {
+ x: 0,
+ y: 0,
+ cell_type: CellType::DeepSpace,
+ occupier: None,
+ powerup: None,
+ },
+ Cell {
+ x: 1,
+ y: 0,
+ cell_type: CellType::Air,
+ occupier: None,
+ powerup: None,
+ },
+ Cell {
+ x: 2,
+ y: 0,
+ cell_type: CellType::Dirt,
+ occupier: None,
+ powerup: None,
+ },
+ ],
+ vec![
+ Cell {
+ x: 0,
+ y: 1,
+ cell_type: CellType::Air,
+ occupier: None,
+ powerup: Some(Powerup {
+ powerup_type: PowerupType::HealthPack,
+ value: 5,
+ }),
+ },
+ Cell {
+ x: 1,
+ y: 1,
+ cell_type: CellType::Air,
+ occupier: Some(CellWorm::OpponentWorm {
+ id: 1,
+ player_id: 2,
+ health: 100,
+ position: Position { x: 1, y: 1 },
+ digging_range: 1,
+ movement_range: 1,
+ }),
+ powerup: None,
+ },
+ Cell {
+ x: 2,
+ y: 1,
+ cell_type: CellType::Air,
+ occupier: Some(CellWorm::PlayerWorm {
+ id: 1,
+ player_id: 1,
+ health: 100,
+ position: Position { x: 2, y: 1 },
+ digging_range: 1,
+ movement_range: 1,
+ weapon: Weapon {
+ damage: 1,
+ range: 3,
+ },
+ }),
+ powerup: None,
+ },
+ ],
+ ],
+ };
+
+ let parsed: State = serde_json::from_str(example).unwrap();
+
+ assert_eq!(
+ parsed, expected,
+ "Parsed value did not match the expected value.\nParsed = {:#?}\nExpected = {:#?}",
+ parsed, expected
+ );
+ }
+
+ #[test]
+ fn west_moving_stays_in_bounds() {
+ let pos = Position { x: 1, y: 1 };
+ assert_eq!(pos.west(1), Some(Position { x: 0, y: 1 }));
+ assert_eq!(pos.west(2), None);
+ }
+
+ #[test]
+ fn east_moving_stays_in_bounds() {
+ let pos = Position { x: 1, y: 1 };
+ assert_eq!(pos.east(1, 3), Some(Position { x: 2, y: 1 }));
+ assert_eq!(pos.east(2, 3), None);
+ }
+
+ #[test]
+ fn north_moving_stays_in_bounds() {
+ let pos = Position { x: 1, y: 1 };
+ assert_eq!(pos.north(1), Some(Position { x: 1, y: 0 }));
+ assert_eq!(pos.north(2), None);
+ }
+
+ #[test]
+ fn south_moving_stays_in_bounds() {
+ let pos = Position { x: 1, y: 1 };
+ assert_eq!(pos.south(1, 3), Some(Position { x: 1, y: 2 }));
+ assert_eq!(pos.south(2, 3), None);
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..cc39b63
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,296 @@
+use std::io::prelude::*;
+use std::io::stdin;
+
+use rand::prelude::*;
+
+mod command;
+mod json;
+
+use command::*;
+use json::*;
+
+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),
+ Err(e) => {
+ eprintln!("WARN: State file could not be parsed: {}", e);
+ Command::DoNothing
+ }
+ };
+ 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<Direction> {
+ let directions: [(Direction, Box<dyn Fn(&Position, u32) -> Option<Position>>); 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<Position> {
+ 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);
+ }
+ }
+}