Improved perf of removing item from unoccupied cells list
[entelect-challenge-tower-defence.git] / src / engine / mod.rs
1 pub mod command;
2 pub mod geometry;
3 pub mod settings;
4
5 use self::command::{Command, BuildingType};
6 use self::geometry::Point;
7 use self::settings::{GameSettings, BuildingSettings};
8
9 use std::ops::Fn;
10 use std::cmp;
11
12 #[derive(Debug, Clone, PartialEq)]
13 pub struct GameState {
14     pub status: GameStatus,
15     pub player: Player,
16     pub opponent: Player,
17     pub player_unconstructed_buildings: Vec<UnconstructedBuilding>,
18     pub player_buildings: Vec<Building>,
19     pub unoccupied_player_cells: Vec<Point>,
20     pub opponent_unconstructed_buildings: Vec<UnconstructedBuilding>,
21     pub opponent_buildings: Vec<Building>,
22     pub unoccupied_opponent_cells: Vec<Point>,
23     pub player_missiles: Vec<Missile>,
24     pub opponent_missiles: Vec<Missile>
25 }
26
27 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
28 pub enum GameStatus {
29     Continue,
30     PlayerWon,
31     OpponentWon,
32     Draw
33 }
34
35 #[derive(Debug, Clone, PartialEq)]
36 pub struct Player {
37     pub energy: u16,
38     pub health: u8,
39     pub energy_generated: u16,
40 }
41
42 #[derive(Debug, Clone, PartialEq)]
43 pub struct UnconstructedBuilding {
44     pub pos: Point,
45     pub health: u8,
46     pub construction_time_left: u8,
47     pub weapon_damage: u8,
48     pub weapon_speed: u8,
49     pub weapon_cooldown_period: u8,
50     pub energy_generated_per_turn: u16
51 }
52
53 #[derive(Debug, Clone, PartialEq)]
54 pub struct Building {
55     pub pos: Point,
56     pub health: u8,
57     pub weapon_damage: u8,
58     pub weapon_speed: u8,
59     pub weapon_cooldown_time_left: u8,
60     pub weapon_cooldown_period: u8,
61     pub energy_generated_per_turn: u16
62 }
63
64 #[derive(Debug, Clone, PartialEq)]
65 pub struct Missile {
66     pub pos: Point,
67     pub damage: u8,
68     pub speed: u8,
69 }
70
71 impl GameState {
72     pub fn new(
73         player: Player, opponent: Player,
74         player_unconstructed_buildings: Vec<UnconstructedBuilding>, player_buildings: Vec<Building>,
75         opponent_unconstructed_buildings: Vec<UnconstructedBuilding>, opponent_buildings: Vec<Building>,
76         player_missiles: Vec<Missile>, opponent_missiles: Vec<Missile>,
77         settings: &GameSettings) -> GameState {
78         
79         let unoccupied_player_cells = GameState::unoccupied_cells(
80             &player_buildings, &player_unconstructed_buildings, Point::new(0, 0), Point::new(settings.size.x/2, settings.size.y)
81         );
82         let unoccupied_opponent_cells = GameState::unoccupied_cells(
83             &opponent_buildings, &opponent_unconstructed_buildings, Point::new(settings.size.x/2, 0), Point::new(settings.size.x, settings.size.y)
84         );
85         GameState {
86             status: GameStatus::Continue,
87             player: player,
88             opponent: opponent,
89             player_unconstructed_buildings: player_unconstructed_buildings,
90             player_buildings: player_buildings,
91             unoccupied_player_cells: unoccupied_player_cells,
92             opponent_unconstructed_buildings: opponent_unconstructed_buildings,
93             opponent_buildings: opponent_buildings,
94             unoccupied_opponent_cells: unoccupied_opponent_cells,
95             player_missiles: player_missiles,
96             opponent_missiles: opponent_missiles
97         }
98     }
99
100     /**
101      * Sorts the various arrays. Generally not necessary, but useful
102      * for tests that check equality between states.
103      */
104     pub fn sort(&mut self) {
105         self.player_unconstructed_buildings.sort_by_key(|b| b.pos);
106         self.player_buildings.sort_by_key(|b| b.pos);
107         self.unoccupied_player_cells.sort();
108         self.opponent_unconstructed_buildings.sort_by_key(|b| b.pos);
109         self.opponent_buildings.sort_by_key(|b| b.pos);
110         self.unoccupied_opponent_cells.sort();
111         self.player_missiles.sort_by_key(|b| b.pos);
112         self.opponent_missiles.sort_by_key(|b| b.pos);
113     }
114
115     pub fn simulate(&self, settings: &GameSettings, player_command: Command, opponent_command: Command) -> GameState {
116         let mut state = self.clone();
117         state.simulate_mut(settings, player_command, opponent_command);
118         state
119     }
120
121     pub fn simulate_mut(&mut self, settings: &GameSettings, player_command: Command, opponent_command: Command) {
122         if self.status.is_complete() {
123             return;
124         }
125
126         GameState::update_construction(&mut self.player_unconstructed_buildings, &mut self.player_buildings, &mut self.player);
127         GameState::update_construction(&mut self.opponent_unconstructed_buildings, &mut self.opponent_buildings, &mut self.opponent);
128
129         GameState::add_missiles(&mut self.player_buildings, &mut self.player_missiles);
130         GameState::add_missiles(&mut self.opponent_buildings, &mut self.opponent_missiles);
131
132         GameState::move_missiles(&mut self.player_missiles, |p| p.move_right(&settings.size),
133                                  &mut self.opponent_buildings, &mut self.opponent,
134                                  &mut self.unoccupied_opponent_cells);
135         GameState::move_missiles(&mut self.opponent_missiles, |p| p.move_left(),
136                                  &mut self.player_buildings, &mut self.player,
137                                  &mut self.unoccupied_player_cells);
138
139         GameState::add_energy(&mut self.player);
140         GameState::add_energy(&mut self.opponent);
141
142         GameState::perform_command(&mut self.player_unconstructed_buildings, &mut self.player_buildings,  &mut self.player, &mut self.unoccupied_player_cells, settings, player_command, &settings.size);
143         GameState::perform_command(&mut self.opponent_unconstructed_buildings, &mut self.opponent_buildings, &mut self.opponent, &mut self.unoccupied_opponent_cells, settings, opponent_command, &settings.size);
144         
145         GameState::update_status(self);
146     }
147
148     fn perform_command(unconstructed_buildings: &mut Vec<UnconstructedBuilding>, buildings: &mut Vec<Building>, player: &mut Player, unoccupied_cells: &mut Vec<Point>, settings: &GameSettings, command: Command, size: &Point) {
149         match command {
150             Command::Nothing => { },
151             Command::Build(p, b) => {
152                 let blueprint = settings.building_settings(b);
153
154                 // This is used internally. I should not be making
155                 // invalid moves!
156                 debug_assert!(!buildings.iter().any(|b| b.pos == p));
157                 debug_assert!(p.x < size.x && p.y < size.y);
158                 debug_assert!(player.energy >= blueprint.price);
159
160                 player.energy -= blueprint.price;
161                 if blueprint.construction_time > 0 {
162                     unconstructed_buildings.push(UnconstructedBuilding::new(p, blueprint));
163                 } else {
164                     let building = Building::new(p, blueprint);
165                     player.energy_generated += building.energy_generated_per_turn;
166                     buildings.push(building);
167                 }
168
169                 let to_remove_index = unoccupied_cells.iter().position(|&pos| pos == p).unwrap();
170                 unoccupied_cells.swap_remove(to_remove_index);
171             },
172         }
173     }
174
175     fn update_construction(unconstructed_buildings: &mut Vec<UnconstructedBuilding>, buildings: &mut Vec<Building>, player: &mut Player) {
176         for building in unconstructed_buildings.iter_mut() {
177             building.construction_time_left -= 1;
178             if building.is_constructed() {
179                 player.energy_generated += building.energy_generated_per_turn;
180                 buildings.push(building.to_building());
181             }
182         }
183         unconstructed_buildings.retain(|b| !b.is_constructed());
184     }
185
186     fn add_missiles(buildings: &mut Vec<Building>, missiles: &mut Vec<Missile>) {
187         for building in buildings.iter_mut().filter(|b| b.is_shooty()) {
188             if building.weapon_cooldown_time_left > 0 {
189                 building.weapon_cooldown_time_left -= 1;
190             } else {
191                 missiles.push(Missile {
192                     pos: building.pos,
193                     speed: building.weapon_speed,
194                     damage: building.weapon_damage,
195                 });
196                 building.weapon_cooldown_time_left = building.weapon_cooldown_period;
197             }
198         }
199     }
200
201     fn move_missiles<F>(missiles: &mut Vec<Missile>, move_fn: F, opponent_buildings: &mut Vec<Building>, opponent: &mut Player, unoccupied_cells: &mut Vec<Point>,)
202     where F: Fn(Point) -> Option<Point> {
203         for missile in missiles.iter_mut() {
204             for _ in 0..missile.speed {
205                 match move_fn(missile.pos) {
206                     None => {
207                         let damage = cmp::min(missile.damage, opponent.health);
208                         opponent.health -= damage;
209                         missile.speed = 0;
210                     },
211                     Some(point) => {
212                         missile.pos = point;
213                         for hit in opponent_buildings.iter_mut().filter(|b| b.pos == point) {
214                             let damage = cmp::min(missile.damage, hit.health);
215                             hit.health -= damage;
216                             missile.speed = 0;
217                         }
218                     }
219                 }
220
221                 /*
222                 check is necessary if speed could be > 1, which isn't the case yet
223                 if missile.speed == 0 {
224                     break;
225                 }
226                  */
227             }
228         }
229         missiles.retain(|m| m.speed > 0);
230
231         for b in opponent_buildings.iter().filter(|b| b.health == 0) {
232             unoccupied_cells.push(b.pos);
233             opponent.energy_generated -= b.energy_generated_per_turn;
234         }
235         opponent_buildings.retain(|b| b.health > 0);
236     }
237
238     fn add_energy(player: &mut Player) {
239         player.energy += player.energy_generated;
240     }
241
242     fn update_status(state: &mut GameState) {
243         let player_dead = state.player.health == 0;
244         let opponent_dead = state.opponent.health == 0;
245         state.status = match (player_dead, opponent_dead) {
246             (true, true) => GameStatus::Draw,
247             (false, true) => GameStatus::PlayerWon,
248             (true, false) => GameStatus::OpponentWon,
249             (false, false) => GameStatus::Continue,
250         };
251     }
252
253     pub fn unoccupied_player_cells_in_row(&self, y: u8) -> Vec<Point> {
254         self.unoccupied_player_cells.iter().filter(|p| p.y == y).cloned().collect()
255     }
256
257     fn unoccupied_cells(buildings: &[Building], unconstructed_buildings: &[UnconstructedBuilding], bl: Point, tr: Point) -> Vec<Point> {
258         let mut result = Vec::with_capacity((tr.y-bl.y) as usize * (tr.x-bl.x) as usize);
259         for y in bl.y..tr.y {
260             for x in bl.x..tr.x {
261                 let pos = Point::new(x, y);
262                 if !buildings.iter().any(|b| b.pos == pos) && !unconstructed_buildings.iter().any(|b| b.pos == pos) {
263                     result.push(pos);
264                 }
265             }
266         }
267         result
268     }
269
270     pub fn player_affordable_buildings(&self, settings: &GameSettings) -> Vec<BuildingType> {
271         GameState::affordable_buildings(self.player.energy, settings)
272     }
273
274     pub fn opponent_affordable_buildings(&self, settings: &GameSettings) -> Vec<BuildingType> {
275         GameState::affordable_buildings(self.opponent.energy, settings)
276     }
277
278     fn affordable_buildings(energy: u16, settings: &GameSettings) -> Vec<BuildingType> {
279         BuildingType::all().iter()
280             .filter(|&b| settings.building_settings(*b).price <= energy)
281             .cloned()
282             .collect()
283     }
284 }
285
286 impl GameStatus {
287     fn is_complete(&self) -> bool {
288         *self != GameStatus::Continue
289     }
290 }
291
292 impl Player {
293     pub fn new(energy: u16, health: u8, settings: &GameSettings, buildings: &[Building]) -> Player {
294         Player {
295             energy: energy,
296             health: health,
297             energy_generated: settings.energy_income + buildings.iter().map(|b| b.energy_generated_per_turn).sum::<u16>()
298         }
299     }
300     
301     pub fn can_afford_all_buildings(&self, settings: &GameSettings) -> bool {
302         self.can_afford_attack_buildings(settings) &&
303             self.can_afford_defence_buildings(settings) &&
304             self.can_afford_energy_buildings(settings)
305     }
306
307     pub fn can_afford_attack_buildings(&self, settings: &GameSettings) -> bool {
308         self.energy >= settings.attack.price
309     }
310     pub fn can_afford_defence_buildings(&self, settings: &GameSettings) -> bool {
311         self.energy >= settings.defence.price
312     }
313     pub fn can_afford_energy_buildings(&self, settings: &GameSettings) -> bool {
314         self.energy >= settings.energy.price
315     }
316
317 }
318
319 impl UnconstructedBuilding {
320     pub fn new(pos: Point, blueprint: &BuildingSettings) -> UnconstructedBuilding {
321         UnconstructedBuilding {
322             pos: pos,
323             health: blueprint.health,
324             construction_time_left: blueprint.construction_time,
325             weapon_damage: blueprint.weapon_damage,
326             weapon_speed: blueprint.weapon_speed,
327             weapon_cooldown_period: blueprint.weapon_cooldown_period,
328             energy_generated_per_turn: blueprint.energy_generated_per_turn
329         }
330     }
331     
332     fn is_constructed(&self) -> bool {
333         self.construction_time_left == 0
334     }
335
336     fn to_building(&self) -> Building {
337         Building {
338             pos: self.pos,
339             health: self.health,
340             weapon_damage: self.weapon_damage,
341             weapon_speed: self.weapon_speed,
342             weapon_cooldown_time_left: 0,
343             weapon_cooldown_period: self.weapon_cooldown_period,
344             energy_generated_per_turn: self.energy_generated_per_turn
345         }
346     }
347 }
348
349 impl Building {
350     pub fn new(pos: Point, blueprint: &BuildingSettings) -> Building {
351         Building {
352             pos: pos,
353             health: blueprint.health,
354             weapon_damage: blueprint.weapon_damage,
355             weapon_speed: blueprint.weapon_speed,
356             weapon_cooldown_time_left: 0,
357             weapon_cooldown_period: blueprint.weapon_cooldown_period,
358             energy_generated_per_turn: blueprint.energy_generated_per_turn
359         }
360     }
361     
362     fn is_shooty(&self) -> bool {
363         self.weapon_damage > 0
364     }
365 }
366
367