3e6cb7a1d6f6574f37a365797a9eadb3df26ea57
[entelect-challenge-tower-defence.git] / src / strategy / monte_carlo.rs
1 use engine::command::*;
2 use engine::geometry::*;
3 use engine::status::GameStatus;
4 use engine::bitwise_engine::{Player, BitwiseGameState};
5 use engine::constants::*;
6
7 use rand::{Rng, XorShiftRng, SeedableRng};
8
9 const MAX_MOVES: u16 = 400;
10
11 use time::{Duration, PreciseTime};
12
13 #[cfg(not(feature = "single-threaded"))]
14 use rayon::prelude::*;
15
16 #[cfg(feature = "energy-cutoff")] pub const ENERGY_PRODUCTION_CUTOFF: u16 = 100;
17 #[cfg(feature = "energy-cutoff")] pub const ENERGY_STORAGE_CUTOFF: u16 = 100;
18
19 pub fn choose_move(state: &BitwiseGameState, start_time: PreciseTime, max_time: Duration) -> Command {
20     let mut command_scores = CommandScore::init_command_scores(state);
21     let command = simulate_options_to_timeout(&mut command_scores, state, start_time, max_time);
22     
23     match command {
24         Some(command) => command.command,
25         _ => Command::Nothing
26     }
27 }
28
29 #[cfg(not(feature = "discard-poor-performers"))]
30 fn simulate_options_to_timeout(command_scores: &'a mut Vec<CommandScore>, settings: &GameSettings, state: &BitwiseGameState, start_time: PreciseTime, max_time: Duration) -> Option<&'a CommandScore> {
31     loop {
32         simulate_all_options_once(command_scores, settings, state);
33         if start_time.to(PreciseTime::now()) > max_time {
34             break;
35         }
36     }
37
38     #[cfg(feature = "benchmarking")]
39     {
40         let total_iterations: u32 = command_scores.iter().map(|c| c.attempts).sum();
41         println!("Iterations: {}", total_iterations);
42     }
43
44     command_scores.iter().max_by_key(|&c| c.win_ratio())
45 }
46
47 #[cfg(feature = "discard-poor-performers")]
48 fn simulate_options_to_timeout<'a>(command_scores: &'a mut Vec<CommandScore>, state: &BitwiseGameState, start_time: PreciseTime, max_time: Duration) -> Option<&'a CommandScore> {
49     use std::cmp;
50     let min_options = cmp::min(command_scores.len(), 5);
51     
52     let maxes = [max_time / 4, max_time / 2, max_time * 3 / 4, max_time];
53     for (i, &max) in maxes.iter().enumerate() {
54         let new_length = cmp::max(min_options, command_scores.len() / (2usize.pow(i as u32)));
55         let active_scores = &mut command_scores[0..new_length];
56         loop {
57             simulate_all_options_once(active_scores, state);
58             if start_time.to(PreciseTime::now()) > max {
59                 break;
60             }
61         }
62         active_scores.sort_unstable_by_key(|c| -c.win_ratio());
63     }
64
65     #[cfg(feature = "benchmarking")]
66     {
67         let total_iterations: u32 = command_scores.iter().map(|c| c.attempts).sum();
68         println!("Iterations: {}", total_iterations);
69     }
70
71     command_scores.first()
72 }
73
74 #[cfg(feature = "single-threaded")]
75 fn simulate_all_options_once(command_scores: &mut[CommandScore], state: &BitwiseGameState) {
76     command_scores.iter_mut()
77         .for_each(|score| {
78             let mut rng = XorShiftRng::from_seed(score.next_seed);
79             simulate_to_endstate(score, state, &mut rng);
80         });
81 }
82
83 #[cfg(not(feature = "single-threaded"))]
84 fn simulate_all_options_once(command_scores: &mut[CommandScore], state: &BitwiseGameState) {
85     command_scores.par_iter_mut()
86         .for_each(|score| {
87             let mut rng = XorShiftRng::from_seed(score.next_seed);
88             simulate_to_endstate(score, state, &mut rng);
89         });
90 }
91
92 fn simulate_to_endstate<R: Rng>(command_score: &mut CommandScore, state: &BitwiseGameState, rng: &mut R) {
93     let mut state_mut = state.clone();
94     
95     let opponent_first = random_move(&state_mut.opponent, rng);
96     let mut status = state_mut.simulate(command_score.command, opponent_first);
97     
98     for _ in 0..MAX_MOVES {
99         if status != GameStatus::Continue {
100             break;
101         }
102
103         let player_command = random_move(&state_mut.player, rng);
104         let opponent_command = random_move(&state_mut.opponent, rng);
105         status = state_mut.simulate(player_command, opponent_command);
106     }
107
108     let next_seed = [rng.next_u32(), rng.next_u32(), rng.next_u32(), rng.next_u32()];
109     match status {
110         GameStatus::PlayerWon => command_score.add_victory(next_seed),
111         GameStatus::OpponentWon => command_score.add_defeat(next_seed),
112         GameStatus::Continue => command_score.add_stalemate(next_seed),
113         GameStatus::Draw => command_score.add_draw(next_seed)
114     }
115 }
116
117 // TODO: Given enough energy, most opponents won't do nothing
118 fn random_move<R: Rng>(player: &Player, rng: &mut R) -> Command {
119     let all_buildings = sensible_buildings(player);
120     let nothing_count = 1;
121     let iron_curtain_count = if player.can_build_iron_curtain() { 1 } else { 0 };
122     let free_positions_count = player.unoccupied_cell_count();
123         
124     let building_choice_index = rng.gen_range(0, all_buildings.len() + nothing_count + iron_curtain_count);
125     
126     if building_choice_index == all_buildings.len() {
127         Command::Nothing
128     } else if iron_curtain_count > 0 && building_choice_index == all_buildings.len() + 1 {
129         Command::IronCurtain
130     } else if free_positions_count > 0 {
131         let position_choice = rng.gen_range(0, free_positions_count);
132         Command::Build(
133             player.location_of_unoccupied_cell(position_choice),
134             all_buildings[building_choice_index]
135         )
136     } else {
137         Command::Nothing
138     }
139 }
140
141 #[derive(Debug)]
142 struct CommandScore {
143     command: Command,
144     victories: u32,
145     defeats: u32,
146     draws: u32,
147     stalemates: u32,
148     attempts: u32,
149     next_seed: [u32; 4]
150 }
151
152 impl CommandScore {
153     fn new(command: Command) -> CommandScore {
154         CommandScore {
155             command,
156             victories: 0,
157             defeats: 0,
158             draws: 0,
159             stalemates: 0,
160             attempts: 0,
161             next_seed: [0x7b6a_e1f4, 0x413c_e90f, 0x6781_6799, 0x770a_6bda]
162         }
163     }
164
165     fn add_victory(&mut self, next_seed: [u32; 4]) {
166         self.victories += 1;
167         self.attempts += 1;
168         self.next_seed = next_seed;
169     }
170
171     fn add_defeat(&mut self, next_seed: [u32; 4]) {
172         self.defeats += 1;
173         self.attempts += 1;
174         self.next_seed = next_seed;
175     }
176
177     fn add_draw(&mut self, next_seed: [u32; 4]) {
178         self.draws += 1;
179         self.attempts += 1;
180         self.next_seed = next_seed;
181     }
182
183     fn add_stalemate(&mut self, next_seed: [u32; 4]) {
184         self.stalemates += 1;
185         self.attempts += 1;
186         self.next_seed = next_seed;
187     }
188
189     fn win_ratio(&self) -> i32 {
190         (self.victories as i32 - self.defeats as i32) * 10000 / (self.attempts as i32)
191     }
192
193     //TODO: Devalue nothing so that it doesn't stand and do nothing when it can do things
194     fn init_command_scores(state: &BitwiseGameState) -> Vec<CommandScore> {
195         let all_buildings = sensible_buildings(&state.player);
196
197         let unoccupied_cells = (0..state.player.unoccupied_cell_count()).map(|i| state.player.location_of_unoccupied_cell(i));
198
199         let building_command_count = unoccupied_cells.len()*all_buildings.len();
200         
201         let mut commands = Vec::with_capacity(building_command_count + 2);
202         commands.push(CommandScore::new(Command::Nothing));
203         if state.player.can_build_iron_curtain() {
204             commands.push(CommandScore::new(Command::IronCurtain));
205         }
206
207         for position in unoccupied_cells {
208             for &building in &all_buildings {
209                 commands.push(CommandScore::new(Command::Build(position, building)));
210             }
211         }
212
213         commands
214     }
215 }
216
217 #[cfg(not(feature = "energy-cutoff"))]
218 fn sensible_buildings(player: &Player) -> Vec<BuildingType> {
219     let mut result = Vec::with_capacity(4);
220
221     if DEFENCE_PRICE <= player.energy {
222         result.push(BuildingType::Defence);
223     }
224     if MISSILE_PRICE <= player.energy {
225         result.push(BuildingType::Attack);
226     }
227     if ENERGY_PRICE <= player.energy {
228         result.push(BuildingType::Energy);
229     }
230     if TESLA_PRICE <= player.energy && !player.has_max_teslas() {
231         result.push(BuildingType::Tesla);
232     }
233
234     result
235 }
236
237
238 //TODO: Heuristic that avoids building the initial energy towers all in the same row? Max energy in a row?
239 //TODO: Update cutoff to maybe build iron curtains
240 #[cfg(feature = "energy-cutoff")]
241 fn sensible_buildings(player: &Player) -> Vec<BuildingType> {
242     let mut result = Vec::with_capacity(4);
243     let needs_energy = player.energy_generated() <= ENERGY_PRODUCTION_CUTOFF ||
244         player.energy <= ENERGY_STORAGE_CUTOFF;
245
246     if DEFENCE_PRICE <= player.energy {
247         result.push(BuildingType::Defence);
248     }
249     if MISSILE_PRICE <= player.energy {
250         result.push(BuildingType::Attack);
251     }
252     if ENERGY_PRICE <= player.energy && needs_energy {
253         result.push(BuildingType::Energy);
254     }
255     if TESLA_PRICE <= player.energy && !player.has_max_teslas() {
256         result.push(BuildingType::Tesla);
257     }
258     
259     result
260 }
261