From 28af1786f35801c375a870b9bfbbfe3640aa872d Mon Sep 17 00:00:00 2001 From: Justin Worthe Date: Sat, 30 Jun 2018 15:09:25 +0200 Subject: Initial stab at putting game engine behind a trait --- src/engine/mod.rs | 436 ++---------------------------------------------------- 1 file changed, 14 insertions(+), 422 deletions(-) (limited to 'src/engine/mod.rs') diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 588724a..65d6e24 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -1,29 +1,22 @@ pub mod command; pub mod geometry; pub mod settings; +pub mod expressive_engine; -use self::command::{Command, BuildingType}; -use self::geometry::Point; -use self::settings::{GameSettings, BuildingSettings}; - -use std::ops::FnMut; -#[cfg(feature = "energy-cutoff")] pub const ENERGY_PRODUCTION_CUTOFF: f32 = 1.2; -#[cfg(feature = "energy-cutoff")] pub const ENERGY_STORAGE_CUTOFF: f32 = 1.5; +use self::command::{Command}; +use self::geometry::Point; +use self::settings::{GameSettings}; -#[derive(Debug, Clone, PartialEq)] -pub struct GameState { - pub status: GameStatus, - pub player: Player, - pub opponent: Player, - pub player_unconstructed_buildings: Vec, - pub player_buildings: Vec, - pub unoccupied_player_cells: Vec, - pub opponent_unconstructed_buildings: Vec, - pub opponent_buildings: Vec, - pub unoccupied_opponent_cells: Vec, - pub player_missiles: Vec, - pub opponent_missiles: Vec +pub trait GameState: Clone + Sync { + fn simulate(&mut self, settings: &GameSettings, player_command: Command, opponent_command: Command) -> GameStatus; + + fn player(&self) -> &Player; + fn opponent(&self) -> &Player; + fn player_has_max_teslas(&self) -> bool; + fn opponent_has_max_teslas(&self) -> bool; + fn unoccupied_player_cells(&self) -> &Vec; + fn unoccupied_opponent_cells(&self) -> &Vec; } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -41,413 +34,12 @@ pub struct Player { pub energy_generated: u16, } -#[derive(Debug, Clone, PartialEq)] -pub struct UnconstructedBuilding { - pub pos: Point, - pub health: u8, - pub construction_time_left: u8, - pub weapon_damage: u8, - pub weapon_speed: u8, - pub weapon_cooldown_period: u8, - pub energy_generated_per_turn: u16 -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Building { - pub pos: Point, - pub health: u8, - pub weapon_damage: u8, - pub weapon_speed: u8, - pub weapon_cooldown_time_left: u8, - pub weapon_cooldown_period: u8, - pub energy_generated_per_turn: u16 -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Missile { - pub pos: Point, - pub damage: u8, - pub speed: u8, -} - -impl GameState { - pub fn new( - player: Player, opponent: Player, - player_unconstructed_buildings: Vec, player_buildings: Vec, - opponent_unconstructed_buildings: Vec, opponent_buildings: Vec, - player_missiles: Vec, opponent_missiles: Vec, - settings: &GameSettings) -> GameState { - - let unoccupied_player_cells = GameState::unoccupied_cells( - &player_buildings, &player_unconstructed_buildings, Point::new(0, 0), Point::new(settings.size.x/2, settings.size.y) - ); - let unoccupied_opponent_cells = GameState::unoccupied_cells( - &opponent_buildings, &opponent_unconstructed_buildings, Point::new(settings.size.x/2, 0), Point::new(settings.size.x, settings.size.y) - ); - GameState { - status: GameStatus::Continue, - player, opponent, - player_unconstructed_buildings, player_buildings, unoccupied_player_cells, - opponent_unconstructed_buildings, opponent_buildings, unoccupied_opponent_cells, - player_missiles, opponent_missiles - } - } - - /** - * Sorts the various arrays. Generally not necessary, but useful - * for tests that check equality between states. - */ - pub fn sort(&mut self) { - self.player_unconstructed_buildings.sort_by_key(|b| b.pos); - self.player_buildings.sort_by_key(|b| b.pos); - self.unoccupied_player_cells.sort(); - self.opponent_unconstructed_buildings.sort_by_key(|b| b.pos); - self.opponent_buildings.sort_by_key(|b| b.pos); - self.unoccupied_opponent_cells.sort(); - self.player_missiles.sort_by_key(|b| b.pos); - self.opponent_missiles.sort_by_key(|b| b.pos); - } - - pub fn simulate(&self, settings: &GameSettings, player_command: Command, opponent_command: Command) -> GameState { - let mut state = self.clone(); - state.simulate_mut(settings, player_command, opponent_command); - state - } - - pub fn simulate_mut(&mut self, settings: &GameSettings, player_command: Command, opponent_command: Command) { - if self.status.is_complete() { - return; - } - - GameState::perform_construct_command(&mut self.player_unconstructed_buildings, &mut self.player_buildings, &mut self.player, &mut self.unoccupied_player_cells, settings, player_command, &settings.size); - GameState::perform_construct_command(&mut self.opponent_unconstructed_buildings, &mut self.opponent_buildings, &mut self.opponent, &mut self.unoccupied_opponent_cells, settings, opponent_command, &settings.size); - GameState::perform_deconstruct_command(&mut self.player_unconstructed_buildings, &mut self.player_buildings, &mut self.player, &mut self.unoccupied_player_cells, player_command); - GameState::perform_deconstruct_command(&mut self.opponent_unconstructed_buildings, &mut self.opponent_buildings, &mut self.opponent, &mut self.unoccupied_opponent_cells, opponent_command); - - GameState::update_construction(&mut self.player_unconstructed_buildings, &mut self.player_buildings, &mut self.player); - GameState::update_construction(&mut self.opponent_unconstructed_buildings, &mut self.opponent_buildings, &mut self.opponent); - - GameState::fire_teslas(&mut self.player, &mut self.player_buildings, &mut self.unoccupied_player_cells, &mut self.opponent, &mut self.opponent_buildings, &mut self.unoccupied_opponent_cells, &settings); - - GameState::add_missiles(&mut self.player_buildings, &mut self.player_missiles); - GameState::add_missiles(&mut self.opponent_buildings, &mut self.opponent_missiles); - - GameState::move_missiles(&mut self.player_missiles, |p| p.wrapping_move_right(), - &mut self.opponent_buildings, &mut self.opponent, - &mut self.unoccupied_opponent_cells, - &settings); - GameState::move_missiles(&mut self.opponent_missiles, |p| p.wrapping_move_left(), - &mut self.player_buildings, &mut self.player, - &mut self.unoccupied_player_cells, - &settings); - - GameState::add_energy(&mut self.player); - GameState::add_energy(&mut self.opponent); - - GameState::update_status(self); - } - - fn perform_construct_command(unconstructed_buildings: &mut Vec, buildings: &mut Vec, player: &mut Player, unoccupied_cells: &mut Vec, settings: &GameSettings, command: Command, size: &Point) { - if let Command::Build(p, b) = command { - let blueprint = settings.building_settings(b); - - // This is used internally. I should not be making - // invalid moves! - debug_assert!(!buildings.iter().any(|b| b.pos == p)); - debug_assert!(p.x < size.x && p.y < size.y); - debug_assert!(player.energy >= blueprint.price); - debug_assert!(b != BuildingType::Tesla || - unconstructed_buildings.iter().filter(|b| b.weapon_damage == 20).count() + - buildings.iter().filter(|b| b.weapon_damage == 20).count() < 2); - - player.energy -= blueprint.price; - unconstructed_buildings.push(UnconstructedBuilding::new(p, blueprint)); - - let to_remove_index = unoccupied_cells.iter().position(|&pos| pos == p).unwrap(); - unoccupied_cells.swap_remove(to_remove_index); - } - } - fn perform_deconstruct_command(unconstructed_buildings: &mut Vec, buildings: &mut Vec, player: &mut Player, unoccupied_cells: &mut Vec, command: Command) { - if let Command::Deconstruct(p) = command { - let to_remove_index = buildings.iter().position(|ref b| b.pos == p); - let unconstructed_to_remove_index = unconstructed_buildings.iter().position(|ref b| b.pos == p); - debug_assert!(to_remove_index.is_some() || unconstructed_to_remove_index.is_some()); - - if let Some(i) = to_remove_index { - player.energy_generated -= buildings[i].energy_generated_per_turn; - buildings.swap_remove(i); - } - if let Some(i) = unconstructed_to_remove_index { - unconstructed_buildings.swap_remove(i); - } - - player.energy += 5; - - unoccupied_cells.push(p); - } - } - - fn update_construction(unconstructed_buildings: &mut Vec, buildings: &mut Vec, player: &mut Player) { - let mut buildings_len = unconstructed_buildings.len(); - for i in (0..buildings_len).rev() { - if unconstructed_buildings[i].is_constructed() { - player.energy_generated += unconstructed_buildings[i].energy_generated_per_turn; - buildings.push(unconstructed_buildings[i].to_building()); - buildings_len -= 1; - unconstructed_buildings.swap(i, buildings_len); - } else { - unconstructed_buildings[i].construction_time_left -= 1 - } - } - unconstructed_buildings.truncate(buildings_len); - } - - fn fire_teslas(player: &mut Player, player_buildings: &mut Vec, player_unoccupied_cells: &mut Vec, opponent: &mut Player, opponent_buildings: &mut Vec, opponent_unoccupied_cells: &mut Vec,settings: &GameSettings) { - for tesla in player_buildings.iter_mut().filter(|b| b.weapon_damage == 20) { - if tesla.weapon_cooldown_time_left > 0 { - tesla.weapon_cooldown_time_left -= 1; - } else if player.energy >= 100 { - player.energy -= 100; - tesla.weapon_cooldown_time_left = tesla.weapon_cooldown_period; - - if tesla.pos.x + 1 >= settings.size.x/2 { - opponent.health = opponent.health.saturating_sub(settings.tesla.weapon_damage); - } - 'player_col_loop: for x in tesla.pos.x+1..tesla.pos.x+(settings.size.x/2)+2 { - for &y in [tesla.pos.y.saturating_sub(1), tesla.pos.y, tesla.pos.y.saturating_add(1)].iter() { - let target_point = Point::new(x, y); - for b in 0..opponent_buildings.len() { - if opponent_buildings[b].pos == target_point && opponent_buildings[b].health > 0 { - opponent_buildings[b].health = opponent_buildings[b].health.saturating_sub(settings.tesla.weapon_damage); - continue 'player_col_loop; - } - } - } - } - } - } - - for tesla in opponent_buildings.iter_mut().filter(|b| b.weapon_damage == 20) { - if tesla.weapon_cooldown_time_left > 0 { - tesla.weapon_cooldown_time_left -= 1; - } else if opponent.energy >= 100 { - opponent.energy -= 100; - tesla.weapon_cooldown_time_left = tesla.weapon_cooldown_period; - - if tesla.pos.x <= settings.size.x/2 { - player.health = player.health.saturating_sub(settings.tesla.weapon_damage); - } - 'opponent_col_loop: for x in tesla.pos.x.saturating_sub((settings.size.x/2)+1)..tesla.pos.x { - for &y in [tesla.pos.y.saturating_sub(1), tesla.pos.y, tesla.pos.y.saturating_add(1)].iter() { - let target_point = Point::new(x, y); - for b in 0..player_buildings.len() { - if player_buildings[b].pos == target_point && player_buildings[b].health > 0 { - player_buildings[b].health = player_buildings[b].health.saturating_sub(settings.tesla.weapon_damage); - continue 'opponent_col_loop; - } - } - } - } - } - } - - for building in player_buildings.iter().filter(|b| b.health == 0) { - player_unoccupied_cells.push(building.pos); - player.energy_generated -= building.energy_generated_per_turn; - } - player_buildings.retain(|b| b.health > 0); - - for building in opponent_buildings.iter().filter(|b| b.health == 0) { - opponent_unoccupied_cells.push(building.pos); - opponent.energy_generated -= building.energy_generated_per_turn; - } - opponent_buildings.retain(|b| b.health > 0); - } - - fn add_missiles(buildings: &mut Vec, missiles: &mut Vec) { - 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(missiles: &mut Vec, mut wrapping_move_fn: F, opponent_buildings: &mut Vec, opponent: &mut Player, unoccupied_cells: &mut Vec, settings: &GameSettings) - where F: FnMut(&mut Point) { - let mut missiles_len = missiles.len(); - 'speed_loop: for _ in 0..settings.attack.weapon_speed { - 'missile_loop: for m in (0..missiles.len()).rev() { - wrapping_move_fn(&mut missiles[m].pos); - if missiles[m].pos.x >= settings.size.x { - opponent.health = opponent.health.saturating_sub(missiles[m].damage); - - missiles_len -= 1; - missiles.swap(m, missiles_len); - - continue 'missile_loop; - } - else { - for b in 0..opponent_buildings.len() { - if opponent_buildings[b].pos == missiles[m].pos { - opponent_buildings[b].health = opponent_buildings[b].health.saturating_sub(missiles[m].damage); - - missiles_len -= 1; - missiles.swap(m, missiles_len); - - if opponent_buildings[b].health == 0 { - unoccupied_cells.push(opponent_buildings[b].pos); - opponent.energy_generated -= opponent_buildings[b].energy_generated_per_turn; - opponent_buildings.swap_remove(b); - } - //after game engine bug fix, this should go back to missile_loop - continue 'missile_loop; - } - } - } - } - missiles.truncate(missiles_len); - } - } - - fn add_energy(player: &mut Player) { - player.energy += player.energy_generated; - } - - fn update_status(state: &mut GameState) { - let player_dead = state.player.health == 0; - let opponent_dead = state.opponent.health == 0; - state.status = match (player_dead, opponent_dead) { - (true, true) => GameStatus::Draw, - (false, true) => GameStatus::PlayerWon, - (true, false) => GameStatus::OpponentWon, - (false, false) => GameStatus::Continue, - }; - } - - fn unoccupied_cells(buildings: &[Building], unconstructed_buildings: &[UnconstructedBuilding], bl: Point, tr: Point) -> Vec { - let mut result = Vec::with_capacity((tr.y-bl.y) as usize * (tr.x-bl.x) as usize); - for y in bl.y..tr.y { - for x in bl.x..tr.x { - let pos = Point::new(x, y); - if !buildings.iter().any(|b| b.pos == pos) && !unconstructed_buildings.iter().any(|b| b.pos == pos) { - result.push(pos); - } - } - } - result - } - - pub fn count_player_teslas(&self) -> usize { - self.player_unconstructed_buildings.iter().filter(|b| b.weapon_damage == 20).count() + - self.player_buildings.iter().filter(|b| b.weapon_damage == 20).count() - } - - pub fn count_opponent_teslas(&self) -> usize { - self.opponent_unconstructed_buildings.iter().filter(|b| b.weapon_damage == 20).count() + - self.opponent_buildings.iter().filter(|b| b.weapon_damage == 20).count() - } -} - -impl GameStatus { - fn is_complete(&self) -> bool { - *self != GameStatus::Continue - } -} - impl Player { - pub fn new(energy: u16, health: u8, settings: &GameSettings, buildings: &[Building]) -> Player { + pub fn new(energy: u16, health: u8, settings: &GameSettings, buildings: &[expressive_engine::Building]) -> Player { Player { energy, health, energy_generated: settings.energy_income + buildings.iter().map(|b| b.energy_generated_per_turn).sum::() } } - - #[cfg(not(feature = "energy-cutoff"))] - pub fn sensible_buildings(&self, tesla_allowed: bool, settings: &GameSettings) -> Vec { - let mut result = Vec::with_capacity(3); - for b in BuildingType::all().iter() { - let building_setting = settings.building_settings(*b); - let affordable = building_setting.price <= self.energy; - let is_tesla = building_setting.weapon_damage == 20; - if affordable && (!is_tesla || tesla_allowed) { - result.push(*b); - } - } - result - } - - #[cfg(feature = "energy-cutoff")] - pub fn sensible_buildings(&self, tesla_allowed: bool, settings: &GameSettings) -> Vec { - let mut result = Vec::with_capacity(3); - let needs_energy = self.energy_generated as f32 <= ENERGY_PRODUCTION_CUTOFF * settings.max_building_price as f32 && - self.energy as f32 <= ENERGY_STORAGE_CUTOFF * settings.max_building_price as f32; - - for b in BuildingType::all().iter() { - let building_setting = settings.building_settings(*b); - let affordable = building_setting.price <= self.energy; - let energy_producing = building_setting.energy_generated_per_turn > 0; - let is_tesla = building_setting.weapon_damage == 20; - if affordable && (!energy_producing || needs_energy) && (!is_tesla || tesla_allowed) { - result.push(*b); - } - } - result - } } - -impl UnconstructedBuilding { - pub fn new(pos: Point, blueprint: &BuildingSettings) -> UnconstructedBuilding { - UnconstructedBuilding { - pos, - health: blueprint.health, - construction_time_left: blueprint.construction_time, - weapon_damage: blueprint.weapon_damage, - weapon_speed: blueprint.weapon_speed, - weapon_cooldown_period: blueprint.weapon_cooldown_period, - energy_generated_per_turn: blueprint.energy_generated_per_turn - } - } - - fn is_constructed(&self) -> bool { - self.construction_time_left == 0 - } - - fn to_building(&self) -> Building { - Building { - pos: self.pos, - health: self.health, - weapon_damage: self.weapon_damage, - weapon_speed: self.weapon_speed, - weapon_cooldown_time_left: 0, - weapon_cooldown_period: self.weapon_cooldown_period, - energy_generated_per_turn: self.energy_generated_per_turn - } - } -} - -impl Building { - pub fn new(pos: Point, blueprint: &BuildingSettings) -> Building { - Building { - pos, - health: blueprint.health, - weapon_damage: blueprint.weapon_damage, - weapon_speed: blueprint.weapon_speed, - weapon_cooldown_time_left: 0, - weapon_cooldown_period: blueprint.weapon_cooldown_period, - energy_generated_per_turn: blueprint.energy_generated_per_turn - } - } - - fn is_shooty(&self) -> bool { - self.weapon_damage > 0 && self.weapon_damage < 20 - } -} - -- cgit v1.2.3