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