2b98eed56b8a68437ecb8bc54b130239026e9750
[entelect-challenge-tower-defence.git] / src / strategy / monte_carlo.rs
1 use engine::command::*;
2 use engine::status::GameStatus;
3 use engine::bitwise_engine::{Player, BitwiseGameState};
4 use engine::constants::*;
5
6 use rand::{Rng, XorShiftRng, SeedableRng};
7
8 const MAX_MOVES: u16 = 400;
9
10 use time::{Duration, PreciseTime};
11
12 #[cfg(not(feature = "single-threaded"))]
13 use rayon::prelude::*;
14
15 //TODO Rethink / adjust these?
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 mut next_seed: [u8;16] = [0; 16];
109     rng.fill_bytes(&mut next_seed);
110     match status {
111         GameStatus::PlayerWon => command_score.add_victory(next_seed),
112         GameStatus::OpponentWon => command_score.add_defeat(next_seed),
113         GameStatus::Continue => command_score.add_stalemate(next_seed),
114         GameStatus::Draw => command_score.add_draw(next_seed)
115     }
116 }
117
118 fn random_move<R: Rng>(player: &Player, rng: &mut R) -> Command {
119     let all_buildings = sensible_buildings(player);
120     let free_positions_count = player.unoccupied_cell_count();
121     
122     let nothing_count = if all_buildings.len() > 2 && free_positions_count > 0 { 0 } else { 1 };
123     let iron_curtain_count = if player.can_build_iron_curtain() { 1 } else { 0 };
124         
125     let building_choice_index = rng.gen_range(0, all_buildings.len() + nothing_count + iron_curtain_count);
126
127     if building_choice_index < all_buildings.len() && free_positions_count > 0 {
128         let position_choice = rng.gen_range(0, free_positions_count);
129         Command::Build(
130             player.location_of_unoccupied_cell(position_choice),
131             all_buildings[building_choice_index]
132         )
133     }
134     else if iron_curtain_count > 0 && building_choice_index == all_buildings.len() {
135         Command::IronCurtain
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: [u8; 16]
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: [0x7b, 0x6a, 0xe1, 0xf4, 0x41, 0x3c, 0xe9, 0x0f, 0x67, 0x81, 0x67, 0x99, 0x77, 0x0a, 0x6b, 0xda]
162         }
163     }
164
165     fn add_victory(&mut self, next_seed: [u8; 16]) {
166         self.victories += 1;
167         self.attempts += 1;
168         self.next_seed = next_seed;
169     }
170
171     fn add_defeat(&mut self, next_seed: [u8; 16]) {
172         self.defeats += 1;
173         self.attempts += 1;
174         self.next_seed = next_seed;
175     }
176
177     fn add_draw(&mut self, next_seed: [u8; 16]) {
178         self.draws += 1;
179         self.attempts += 1;
180         self.next_seed = next_seed;
181     }
182
183     fn add_stalemate(&mut self, next_seed: [u8; 16]) {
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 #[cfg(feature = "energy-cutoff")]
240 fn sensible_buildings(player: &Player) -> Vec<BuildingType> {
241     let mut result = Vec::with_capacity(4);
242     let needs_energy = player.energy_generated() <= ENERGY_PRODUCTION_CUTOFF ||
243         player.energy <= ENERGY_STORAGE_CUTOFF;
244
245     if DEFENCE_PRICE <= player.energy {
246         result.push(BuildingType::Defence);
247     }
248     if MISSILE_PRICE <= player.energy {
249         result.push(BuildingType::Attack);
250     }
251     if ENERGY_PRICE <= player.energy && needs_energy {
252         result.push(BuildingType::Energy);
253     }
254     if TESLA_PRICE <= player.energy && !player.has_max_teslas() {
255         result.push(BuildingType::Tesla);
256     }
257     
258     result
259 }
260