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> { 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, pub map: Vec>, } #[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, 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, pub snowballs: Option, 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, pub remaining_worm_selections: u8, } impl Opponent { pub fn health_score(&self) -> i32 { self.worms.iter().map(|w| w.health).sum::() / 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, pub powerup: Option, } #[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 { 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 { 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 ); } }