Initial commit with sample bot and embedded game engine
authorJustin Worthe <justin@worthe-it.co.za>
Sat, 5 May 2018 18:37:53 +0000 (20:37 +0200)
committerJustin Worthe <justin@worthe-it.co.za>
Sat, 5 May 2018 18:37:53 +0000 (20:37 +0200)
.gitignore [new file with mode: 0644]
Cargo.toml [new file with mode: 0644]
README.md [new file with mode: 0644]
bot.json [new file with mode: 0644]
src/engine/command.rs [new file with mode: 0644]
src/engine/geometry.rs [new file with mode: 0644]
src/engine/mod.rs [new file with mode: 0644]
src/engine/settings.rs [new file with mode: 0644]
src/main.rs [new file with mode: 0644]
src/state_json.rs [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..44ba2ac
--- /dev/null
@@ -0,0 +1,10 @@
+target
+command.txt
+state.json
+
+# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
+# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
+Cargo.lock
+
+# These are backup files generated by rustfmt
+**/*.rs.bk
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644 (file)
index 0000000..7628f0f
--- /dev/null
@@ -0,0 +1,8 @@
+[package]
+name = "zombot"
+version = "1.0.0"
+
+[dependencies]
+serde_derive = "1.0.43"
+serde = "1.0.43"
+serde_json = "1.0.16"
diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..0b97c14
--- /dev/null
+++ b/README.md
@@ -0,0 +1,38 @@
+# Rust Sample Bot
+
+Rust is a systems programming language, giving programmers the low
+level control that they would usually associate with a programming
+langauge like C or C++, but modern high level programming features.
+
+Rust is a compiled language, which compiles to an
+architecture-specific binary.
+
+For getting started with this bot in particular, I've done a write up
+about [writing a Rust bot for the Entelect challenge](https://www.worthe-it.co.za/programming/2018/05/02/writing-an-entelect-challenge-bot-in-rust.html).
+
+## Environment Setup
+
+The Rust compiler toolchain can be downloaded from the Rust project
+website.
+
+https://www.rust-lang.org/en-US/install.html
+
+## Compilation
+
+The bot can be built using the Rust build tool, Cargo. For the sake of
+the competition, the `--release` flag should be used.
+
+```
+cargo build --release
+```
+
+## Running
+
+After compilation, there will be an executable in
+`target/release/`.
+
+For example, this sample bot's name is
+`entelect_challenge_rust_sample`, so the executable to be run is
+`target/release/entelect_challenge_rust_sample` on Linux or
+`target/release/entelect_challenge_rust_sample.exe` on Windows.
+
diff --git a/bot.json b/bot.json
new file mode 100644 (file)
index 0000000..14ed686
--- /dev/null
+++ b/bot.json
@@ -0,0 +1,8 @@
+{
+       "author": "Justin Worthe",
+       "email": "justin@worthe-it.co.za",
+       "nickName": "Justin",
+       "botLocation": "/target/release/",
+       "botFileName": "zombot",
+       "botLanguage": "rust"
+}
diff --git a/src/engine/command.rs b/src/engine/command.rs
new file mode 100644 (file)
index 0000000..603ee42
--- /dev/null
@@ -0,0 +1,27 @@
+use std::fmt;
+use super::geometry::Point;
+
+#[derive(Debug, Clone, Copy)]
+pub enum Command {
+    Nothing,
+    Build(Point, BuildingType),
+}
+
+impl fmt::Display for Command {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use Command::*;
+        
+        match self {
+            &Nothing => write!(f, ""),
+            &Build(p, b) => write!(f, "{},{},{}", p.x, p.y, b as u8),
+        }
+    }
+}
+
+#[repr(u8)]
+#[derive(Debug, Clone, Copy)]
+pub enum BuildingType {
+    Defense = 0,
+    Attack = 1,
+    Energy = 2,
+}
diff --git a/src/engine/geometry.rs b/src/engine/geometry.rs
new file mode 100644 (file)
index 0000000..f2a2522
--- /dev/null
@@ -0,0 +1,24 @@
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Point {
+    pub x: u8,
+    pub y: u8
+}
+
+impl Point {
+    pub fn move_left(&self) -> Option<Point> {
+        self.x.checked_sub(1).map(|x| Point {
+            x: x,
+            ..*self
+        })
+    }
+    pub fn move_right(&self, size: &Point) -> Option<Point> {
+        if self.x + 1 >= size.x {
+            None
+        } else {
+            Some(Point {
+                x: self.x + 1,
+                ..*self
+            })
+        }
+    }
+}
diff --git a/src/engine/mod.rs b/src/engine/mod.rs
new file mode 100644 (file)
index 0000000..d321572
--- /dev/null
@@ -0,0 +1,220 @@
+pub mod command;
+pub mod geometry;
+pub mod settings;
+
+use self::command::{BuildingType, Command};
+use self::geometry::Point;
+use self::settings::GameSettings;
+
+use std::ops::Fn;
+use std::cmp;
+
+#[derive(Debug, Clone)]
+struct GameState {
+    status: GameStatus,
+    player: Player,
+    opponent: Player,
+    player_buildings: Vec<Building>,
+    opponent_buildings: Vec<Building>,
+    player_missiles: Vec<Missile>,
+    opponent_missiles: Vec<Missile>
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum GameStatus {
+    Continue,
+    PlayerWon,
+    OpponentWon,
+    Draw,
+    InvalidMove
+}
+
+impl GameStatus {
+    fn is_complete(&self) -> bool {
+        *self != GameStatus::Continue
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct Player {
+    energy: u16,
+    health: u16
+}
+
+#[derive(Debug, Clone)]
+struct Building {
+    pos: Point,
+    health: u16,
+    construction_time_left: u8,
+    weapon_damage: u16,
+    weapon_speed: u8,
+    weapon_cooldown_time_left: u8,
+    weapon_cooldown_period: u8,
+    energy_generated_per_turn: u16
+}
+
+impl Building {
+    fn new(pos: Point, building: BuildingType) -> Building {
+        match building {
+            BuildingType::Defense => Building {
+                pos: pos,
+                health: 20,
+                construction_time_left: 3,
+                weapon_damage: 0,
+                weapon_speed: 0,
+                weapon_cooldown_time_left: 0,
+                weapon_cooldown_period: 0,
+                energy_generated_per_turn: 0
+            },
+            BuildingType::Attack => Building {
+                pos: pos,
+                health: 5,
+                construction_time_left: 1,
+                weapon_damage: 5,
+                weapon_speed: 1,
+                weapon_cooldown_time_left: 0,
+                weapon_cooldown_period: 3,
+                energy_generated_per_turn: 0
+            },
+            BuildingType::Energy => Building {
+                pos: pos,
+                health: 5,
+                construction_time_left: 1,
+                weapon_damage: 0,
+                weapon_speed: 0,
+                weapon_cooldown_time_left: 0,
+                weapon_cooldown_period: 0,
+                energy_generated_per_turn: 3
+            }
+        }
+        
+    }
+
+    fn is_constructed(&self) -> bool {
+        self.construction_time_left == 0
+    }
+
+    fn is_shooty(&self) -> bool {
+        self.is_constructed() && self.weapon_damage >= 0
+    }
+}
+
+#[derive(Debug, Clone)]
+struct Missile {
+    pos: Point,
+    damage: u16,
+    speed: u8,
+}
+
+impl Missile {
+    fn is_stopped(&self) -> bool {
+        self.speed == 0
+    }
+}
+
+impl GameState {
+    pub fn simulate(&self, settings: &GameSettings, player_command: Command, opponent_command: Command) -> GameState {
+        if self.status.is_complete() {
+            return self.clone();
+        }
+        
+        let mut state = self.clone();
+        GameState::perform_command(&mut state.player_buildings, player_command, &settings.size);
+        GameState::perform_command(&mut state.opponent_buildings, opponent_command, &settings.size);
+
+        GameState::update_construction(&mut state.player_buildings);
+        GameState::update_construction(&mut state.opponent_buildings);
+
+        GameState::add_missiles(&mut state.player_buildings, &mut state.player_missiles);
+        GameState::add_missiles(&mut state.opponent_buildings, &mut state.opponent_missiles);
+
+        GameState::move_missiles(&mut state.player_missiles, |p| p.move_right(&settings.size),
+                                 &mut state.opponent_buildings, &mut state.opponent);
+        GameState::move_missiles(&mut state.opponent_missiles, |p| p.move_left(),
+                                 &mut state.player_buildings, &mut state.player);
+
+        GameState::add_energy(&mut state.player, settings, &state.player_buildings);
+        GameState::add_energy(&mut state.opponent, settings, &state.opponent_buildings);
+
+        GameState::update_status(&mut state);
+        state
+    }
+
+    fn perform_command(buildings: &mut Vec<Building>, command: Command, size: &Point) -> bool {
+        match command {
+            Command::Nothing => { true },
+            Command::Build(p, b) => {
+                let occupied = buildings.iter().any(|b| b.pos == p);
+                let in_range = p.x < size.x && p.y < size.y;
+                buildings.push(Building::new(p, b));
+                !occupied && in_range
+            },
+        }
+    }
+
+    fn update_construction(buildings: &mut Vec<Building>) {
+        for building in buildings.iter_mut().filter(|b| !b.is_constructed()) {
+            building.construction_time_left -= 1;
+        }
+    }
+
+    fn add_missiles(buildings: &mut Vec<Building>, missiles: &mut Vec<Missile>) {
+        for building in buildings.iter_mut().filter(|b| b.is_shooty()) {
+            if building.weapon_cooldown_time_left > 0 {
+                building.weapon_cooldown_time_left -= 1;
+            } else {
+                missiles.push(Missile {
+                    pos: building.pos,
+                    speed: building.weapon_speed,
+                    damage: building.weapon_damage,
+                });
+                building.weapon_cooldown_time_left = building.weapon_cooldown_period;
+            }
+        }
+    }
+
+    fn move_missiles<F>(missiles: &mut Vec<Missile>, move_fn: F, opponent_buildings: &mut Vec<Building>, opponent: &mut Player)
+    where F: Fn(Point) -> Option<Point> {
+        for missile in missiles.iter_mut() {
+            for _ in 0..missile.speed {
+                match move_fn(missile.pos) {
+                    None => {
+                        let damage = cmp::min(missile.damage, opponent.health);
+                        opponent.health -= damage;
+                        missile.speed = 0;
+                    },
+                    Some(point) => {
+                        missile.pos = point;
+                        for hit in opponent_buildings.iter_mut().filter(|b| b.is_constructed() && b.pos == point && b.health > 0) {
+                            let damage = cmp::min(missile.damage, hit.health);
+                            hit.health -= damage;
+                            missile.speed = 0;                    
+                        }
+                    }
+                }
+                
+                if missile.speed == 0 {
+                    break;
+                }
+            }
+        }
+        missiles.retain(|m| m.speed > 0);
+        opponent_buildings.retain(|b| b.health > 0);
+    }
+
+    fn add_energy(player: &mut Player, settings: &GameSettings, buildings: &Vec<Building>) {
+        player.energy += settings.energy_income;
+        player.energy += buildings.iter().map(|b| b.energy_generated_per_turn).sum::<u16>();
+    }
+
+    fn update_status(state: &mut GameState) {
+        let player_dead = state.player.health == 0;
+        let opponent_dead = state.player.health == 0;
+        state.status = match (player_dead, opponent_dead) {
+            (true, true) => GameStatus::Draw,
+            (true, false) => GameStatus::PlayerWon,
+            (false, true) => GameStatus::OpponentWon,
+            (false, false) => GameStatus::Continue,
+        };
+    }
+}
diff --git a/src/engine/settings.rs b/src/engine/settings.rs
new file mode 100644 (file)
index 0000000..a6691d7
--- /dev/null
@@ -0,0 +1,10 @@
+use super::geometry::Point;
+
+#[derive(Debug)]
+pub struct GameSettings {
+    pub size: Point,
+    pub energy_income: u16,
+    pub energy_price: u16,
+    pub defence_price: u16,
+    pub attack_price: u16
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644 (file)
index 0000000..d964fee
--- /dev/null
@@ -0,0 +1,53 @@
+extern crate serde;
+extern crate serde_json;
+
+#[macro_use]
+extern crate serde_derive;
+
+use std::error::Error;
+
+const STATE_PATH: &str = "state.json";
+
+const COMMAND_PATH: &str = "command.txt";
+
+use std::fs::File;
+use std::io::prelude::*;
+use std::process;
+
+mod state_json;
+mod engine;
+use engine::command::Command;
+
+fn choose_move(_state: &state_json::State) -> Option<Command> {
+    None
+}
+
+
+fn write_command(filename: &str, command: Option<Command>) -> Result<(), Box<Error> > {
+    let mut file = File::create(filename)?;
+    if let Some(command) = command {
+        write!(file, "{}", command)?;
+    }
+
+    Ok(())
+}
+
+
+fn main() {
+    let state = match state_json::read_state_from_file(STATE_PATH) {
+        Ok(state) => state,
+        Err(error) => {
+            eprintln!("Failed to read the {} file. {}", STATE_PATH, error);
+            process::exit(1);
+        }
+    };
+    let command = choose_move(&state);
+
+    match write_command(COMMAND_PATH, command) {
+        Ok(()) => {}
+        Err(error) => {
+            eprintln!("Failed to write the {} file. {}", COMMAND_PATH, error);
+            process::exit(1);
+        }
+    }
+}
diff --git a/src/state_json.rs b/src/state_json.rs
new file mode 100644 (file)
index 0000000..429db6d
--- /dev/null
@@ -0,0 +1,86 @@
+use std::fs::File;
+use std::io::prelude::*;
+use serde_json;
+use std::error::Error;
+
+pub fn read_state_from_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 = serde_json::from_str(content.as_ref())?;
+    Ok(state)
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct State {
+    pub game_details: GameDetails,
+    pub players: Vec<Player>,
+    pub game_map: Vec<Vec<GameCell>>,
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GameDetails {
+    pub round: u32,
+    pub map_width: u32,
+    pub map_height: u32,
+    pub building_prices: BuildingPrices
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
+pub struct BuildingPrices {
+    pub energy: u32,
+    pub defense: u32,
+    pub attack: u32
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Player {
+    pub player_type: char,
+    pub energy: u32,
+    pub health: u32,
+    pub hits_taken: u32,
+    pub score: u32
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct GameCell {
+    pub x: u32,
+    pub y: u32,
+    pub buildings: Vec<BuildingState>,
+    pub missiles: Vec<MissileState>,
+    pub cell_owner: char
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct BuildingState {
+    pub health: u32,
+    pub construction_time_left: i32,
+    pub price: u32,
+    pub weapon_damage: u32,
+    pub weapon_speed: u32,
+    pub weapon_cooldown_time_left: u32,
+    pub weapon_cooldown_period: u32,
+    pub destroy_multiplier: u32,
+    pub construction_score: u32,
+    pub energy_generated_per_turn: u32,
+    pub building_type: String,
+    pub x: u32,
+    pub y: u32,
+    pub player_type: char
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct MissileState {
+    pub damage: u32,
+    pub speed: u32,
+    pub x: u32,
+    pub y: u32,
+    pub player_type: char
+}