diff options
author | Justin Worthe <justin@worthe-it.co.za> | 2019-04-22 11:19:16 +0200 |
---|---|---|
committer | Justin Worthe <justin@worthe-it.co.za> | 2019-04-22 11:19:16 +0200 |
commit | 29a323e0a3bd3ab3e6109b23e15bb5f9e88398e3 (patch) | |
tree | a151c612b5993f127d99c29d4c4fdcf252528436 |
Start the project from the starter bot
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.lock | 258 | ||||
-rw-r--r-- | Cargo.toml | 9 | ||||
-rw-r--r-- | README.md | 81 | ||||
-rw-r--r-- | bot.json | 8 | ||||
-rw-r--r-- | src/command.rs | 61 | ||||
-rw-r--r-- | src/json.rs | 441 | ||||
-rw-r--r-- | src/main.rs | 296 |
8 files changed, 1156 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..84a323e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +target +**/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..054485b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,258 @@ +[[package]] +name = "autocfg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "itoa" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_jitter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_derive" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.31 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "steam-powered-wyrm" +version = "0.1.0" +dependencies = [ + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "syn" +version = "0.15.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" +"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" +"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +"checksum itoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1306f3464951f30e30d12373d31c79fbd52d236e5e896fd92f96ec7babbbe60b" +"checksum libc 0.2.51 (registry+https://github.com/rust-lang/crates.io-index)" = "bedcc7a809076656486ffe045abeeac163da1b558e963a31e29fbfbeba916917" +"checksum proc-macro2 0.4.27 (registry+https://github.com/rust-lang/crates.io-index)" = "4d317f9caece796be1980837fd5cb3dfec5613ebdb04ad0956deea83ce168915" +"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" +"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +"checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" +"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +"checksum rand_jitter 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b9ea758282efe12823e0d952ddb269d2e1897227e464919a554f2a03ef1b832" +"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +"checksum ryu 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)" = "eb9e9b8cde282a9fe6a42dd4681319bfb63f121b8a8ee9439c6f4107e58a46f7" +"checksum serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "aa5f7c20820475babd2c077c3ab5f8c77a31c15e16ea38687b4c02d3e48680f4" +"checksum serde_derive 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)" = "58fc82bec244f168b23d1963b45c8bf5726e9a15a9d146a067f9081aeed2de79" +"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" +"checksum syn 0.15.31 (registry+https://github.com/rust-lang/crates.io-index)" = "d2b4cfac95805274c6afdb12d8f770fa2d27c045953e7b630a81801953699a9a" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c18aa9d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "steam-powered-wyrm" +version = "0.1.0" +edition = "2018" + +[dependencies] +serde = { version = "1.0.90", features = ["derive"] } +serde_json = "1.0.39" +rand = "0.6.5" diff --git a/README.md b/README.md new file mode 100644 index 0000000..a0c3008 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# Steam Powered Wyrm + +This is an entry to the 2019 Entelect Challenge. + +## Environment Setup + +The Rust compiler toolchain can be downloaded from the Rust project +website. + +https://www.rust-lang.org/en-US/install.html + +The project requires these versions of the toolchain (or later). + +- cargo 1.34.0 +- rustc 1.34.0 + +## Building + +Rust's official build tool is called Cargo. It will download +dependencies and call the Rust compiler as required. Dependencies are +configured in [Cargo.toml](./Cargo.toml). + +Cargo needs to be called from the root of the bot (the folder with the +Cargo.toml file). + +```sh +cargo build --release +``` + +## Running Tests + +Rust has support for unit testing built into the language. Any +functions marked with the `#[test]` annotation are considered tests. + +Tests can be run using Cargo: + +```sh +cargo test +``` + +More information on how to write tests is available in +[The Rust Programming Language](https://doc.rust-lang.org/book/ch11-00-testing.html). + +## Exporting the compiled executable + +By default, Rust produces statically linked binaries, so you can just +copy out the executable file from the target directory and put it +wherever you want. + +The name of the binary will match the name of the binary crate in +Cargo.toml. + +Note: This binary has been built for the platform that it was compiled +on. In other words, if it was compiled on 64 bit Linux, you cannot +copy the binary to a Windows machine and run it. You WILL be able to +copy the binary between similar 64 bit Linux machines. + +The machine that the compiled binary is run on does not need to have +the Rust toolchain installed. + +```sh +cp ./target/release/<botFileName> <dest> +``` + +## Running + +The compiled binary can be executed directly. + +```sh +./target/release/<botFileName> +``` + +For convenience in development, you can compile and run through Cargo. + +Note: This is not recommended for the tournament servers, since there +is a small runtime cost in Cargo checking that the compiled binary is +up to date before running it. + +```sh +cargo run --release +``` diff --git a/bot.json b/bot.json new file mode 100644 index 0000000..5578730 --- /dev/null +++ b/bot.json @@ -0,0 +1,8 @@ +{ + "author": "Justin Wernick", + "email": "justin@worthe-it.co.za", + "nickName": "Steam Powered Wyrm", + "botLocation": "/target/release/", + "botFileName": "steam-powered-wyrm", + "botLanguage": "rust" +} 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); + } + } +} |