summaryrefslogtreecommitdiff
path: root/2019-worms/src/json.rs
diff options
context:
space:
mode:
Diffstat (limited to '2019-worms/src/json.rs')
-rw-r--r--2019-worms/src/json.rs554
1 files changed, 554 insertions, 0 deletions
diff --git a/2019-worms/src/json.rs b/2019-worms/src/json.rs
new file mode 100644
index 0000000..a83f102
--- /dev/null
+++ b/2019-worms/src/json.rs
@@ -0,0 +1,554 @@
+use std::error::Error;
+use std::fs::File;
+use std::io::prelude::*;
+use std::path::Path;
+
+use serde::{Deserialize, Serialize};
+use serde_json;
+
+pub fn read_state_from_json_file(filename: &Path) -> Result<State, Box<dyn 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)
+}
+
+// TODO: Narrow numeric types
+// TODO: comment out stuff I don't want / need
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct State {
+ pub current_round: u16,
+ pub max_rounds: u16,
+ pub pushback_damage: i32,
+ pub lava_damage: i32,
+ pub map_size: u8,
+ 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>,
+ pub remaining_worm_selections: u8,
+}
+
+impl Player {
+ pub fn health_score(&self) -> i32 {
+ self.health / 3
+ }
+}
+
+#[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 rounds_until_unfrozen: u8,
+ pub weapon: Weapon,
+ pub banana_bombs: Option<Bomb>,
+ pub snowballs: Option<Snowball>,
+ pub profession: WormType,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Opponent {
+ pub id: i32,
+ pub score: i32,
+ pub current_worm_id: i32,
+ pub previous_command: String,
+ pub worms: Vec<OpponentWorm>,
+ pub remaining_worm_selections: u8,
+}
+
+impl Opponent {
+ pub fn health_score(&self) -> i32 {
+ self.worms.iter().map(|w| w.health).sum::<i32>() / 3
+ }
+}
+
+#[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,
+ pub rounds_until_unfrozen: u8,
+ pub profession: WormType,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "PascalCase")]
+pub enum WormType {
+ Commando,
+ Agent,
+ Technologist,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Cell {
+ pub x: i8,
+ pub y: i8,
+ #[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,
+ Lava,
+ 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: i8,
+ pub y: i8,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Weapon {
+ pub damage: i32,
+ pub range: u8,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Bomb {
+ pub damage: i32,
+ pub range: u8,
+ pub count: u8,
+ pub damage_radius: u8,
+}
+
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
+#[serde(rename_all = "camelCase")]
+pub struct Snowball {
+ pub freeze_duration: u8,
+ pub range: u8,
+ pub count: u8,
+ pub freeze_radius: u8,
+}
+
+impl State {
+ pub fn active_worm_index(&self) -> Option<usize> {
+ self.my_player
+ .worms
+ .iter()
+ .filter(|w| w.health > 0)
+ .position(|w| w.id == self.current_worm_id)
+ }
+
+ pub fn opponent_active_worm_index(&self) -> Option<usize> {
+ self.opponents[0]
+ .worms
+ .iter()
+ .filter(|w| w.health > 0)
+ .position(|w| w.id == self.opponents[0].current_worm_id)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn example_parses_correctly() {
+ let example = r#"
+{
+ "currentRound": 0,
+ "maxRounds": 200,
+ "pushbackDamage": 20,
+ "lavaDamage": 3,
+ "mapSize": 33,
+ "currentWormId": 1,
+ "consecutiveDoNothingCount": 0,
+ "myPlayer": {
+ "id": 1,
+ "score": 100,
+ "health": 300,
+ "currentWormId": 1,
+ "remainingWormSelections": 1,
+ "worms": [
+ {
+ "id": 1,
+ "health": 100,
+ "position": {
+ "x": 24,
+ "y": 29
+ },
+ "weapon": {
+ "damage": 1,
+ "range": 3
+ },
+ "bananaBombs": {
+ "damage": 20,
+ "range": 5,
+ "count": 3,
+ "damageRadius": 2
+ },
+ "diggingRange": 1,
+ "movementRange": 1,
+ "roundsUntilUnfrozen": 0,
+ "profession": "Agent"
+ },
+ {
+ "id": 2,
+ "health": 150,
+ "position": {
+ "x": 1,
+ "y": 16
+ },
+ "weapon": {
+ "damage": 1,
+ "range": 3
+ },
+ "diggingRange": 1,
+ "movementRange": 1,
+ "roundsUntilUnfrozen": 0,
+ "profession": "Commando"
+ },
+ {
+ "id": 3,
+ "health": 100,
+ "position": {
+ "x": 24,
+ "y": 4
+ },
+ "weapon": {
+ "damage": 8,
+ "range": 4
+ },
+ "snowballs": {
+ "freezeDuration": 5,
+ "range": 5,
+ "count": 3,
+ "freezeRadius": 1
+ },
+ "diggingRange": 1,
+ "movementRange": 1,
+ "roundsUntilUnfrozen": 3,
+ "profession": "Technologist"
+ }
+ ]
+ },
+ "opponents": [
+ {
+ "id": 2,
+ "score": 100,
+ "currentWormId": 3,
+ "remainingWormSelections": 2,
+ "previousCommand": "nothing",
+ "worms": [
+ {
+ "id": 1,
+ "health": 100,
+ "position": {
+ "x": 31,
+ "y": 16
+ },
+ "diggingRange": 1,
+ "movementRange": 1,
+ "roundsUntilUnfrozen": 0,
+ "profession": "Commando"
+ }
+ ]
+ }
+ ],
+ "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,
+ pushback_damage: 20,
+ lava_damage: 3,
+ map_size: 33,
+ current_worm_id: 1,
+ consecutive_do_nothing_count: 0,
+ my_player: Player {
+ id: 1,
+ score: 100,
+ health: 300,
+ remaining_worm_selections: 1,
+ 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,
+ banana_bombs: Some(Bomb {
+ damage: 20,
+ range: 5,
+ count: 3,
+ damage_radius: 2,
+ }),
+ snowballs: None,
+ rounds_until_unfrozen: 0,
+ profession: WormType::Agent,
+ },
+ PlayerWorm {
+ id: 2,
+ health: 150,
+ position: Position { x: 1, y: 16 },
+ weapon: Weapon {
+ damage: 1,
+ range: 3,
+ },
+ digging_range: 1,
+ movement_range: 1,
+ banana_bombs: None,
+ snowballs: None,
+ rounds_until_unfrozen: 0,
+ profession: WormType::Commando,
+ },
+ PlayerWorm {
+ id: 3,
+ health: 100,
+ position: Position { x: 24, y: 4 },
+ weapon: Weapon {
+ damage: 8,
+ range: 4,
+ },
+ digging_range: 1,
+ movement_range: 1,
+ banana_bombs: None,
+ snowballs: Some(Snowball {
+ freeze_duration: 5,
+ range: 5,
+ count: 3,
+ freeze_radius: 1,
+ }),
+ rounds_until_unfrozen: 3,
+ profession: WormType::Technologist,
+ },
+ ],
+ },
+ opponents: vec![Opponent {
+ id: 2,
+ score: 100,
+ remaining_worm_selections: 2,
+ current_worm_id: 3,
+ previous_command: "nothing".into(),
+ worms: vec![OpponentWorm {
+ id: 1,
+ health: 100,
+ position: Position { x: 31, y: 16 },
+ digging_range: 1,
+ movement_range: 1,
+ rounds_until_unfrozen: 0,
+ profession: WormType::Commando,
+ }],
+ }],
+ 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
+ );
+ }
+}