Allowed monte carlo search to use iron curtains
[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, PlayerBuildings, 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 = 30;
17 #[cfg(feature = "energy-cutoff")] pub const ENERGY_STORAGE_CUTOFF: u16 = 45;
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_opponent_move(&state_mut, 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_player_move(&state_mut, rng);
104         let opponent_command = random_opponent_move(&state_mut, 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 fn random_player_move<R: Rng>(state: &BitwiseGameState, rng: &mut R) -> Command {
118     let all_buildings = sensible_buildings(&state.player, &state.player_buildings, state.player_has_max_teslas());
119     random_move(&all_buildings, state.player_can_build_iron_curtain(), rng, state.unoccupied_player_cell_count(), |i| state.location_of_unoccupied_player_cell(i))
120 }
121
122 fn random_opponent_move<R: Rng>(state: &BitwiseGameState, rng: &mut R) -> Command {
123     let all_buildings = sensible_buildings(&state.opponent, &state.opponent_buildings, state.opponent_has_max_teslas());
124     random_move(&all_buildings, state.opponent_can_build_iron_curtain(), rng, state.unoccupied_opponent_cell_count(), |i| state.location_of_unoccupied_opponent_cell(i))
125 }
126
127 // TODO: Given enough energy, most opponents won't do nothing
128 fn random_move<R: Rng, F:Fn(usize)->Point>(all_buildings: &[BuildingType], iron_curtain_available: bool, rng: &mut R, free_positions_count: usize, get_point: F) -> Command {
129     let nothing_count = 1;
130     let iron_curtain_count = if iron_curtain_available { 1 } else { 0 };
131     let building_choice_index = rng.gen_range(0, all_buildings.len() + nothing_count + iron_curtain_count);
132     
133     if building_choice_index == all_buildings.len() {
134         Command::Nothing
135     } else if iron_curtain_available && building_choice_index == all_buildings.len() + 1 {
136         Command::IronCurtain
137     } else {
138         let position_choice = rng.gen_range(0, free_positions_count);
139         Command::Build(
140             get_point(position_choice),
141             all_buildings[building_choice_index]
142         )
143     }
144 }
145
146 #[derive(Debug)]
147 struct CommandScore {
148     command: Command,
149     victories: u32,
150     defeats: u32,
151     draws: u32,
152     stalemates: u32,
153     attempts: u32,
154     next_seed: [u32; 4]
155 }
156
157 impl CommandScore {
158     fn new(command: Command) -> CommandScore {
159         CommandScore {
160             command,
161             victories: 0,
162             defeats: 0,
163             draws: 0,
164             stalemates: 0,
165             attempts: 0,
166             next_seed: [0x7b6a_e1f4, 0x413c_e90f, 0x6781_6799, 0x770a_6bda]
167         }
168     }
169
170     fn add_victory(&mut self, next_seed: [u32; 4]) {
171         self.victories += 1;
172         self.attempts += 1;
173         self.next_seed = next_seed;
174     }
175
176     fn add_defeat(&mut self, next_seed: [u32; 4]) {
177         self.defeats += 1;
178         self.attempts += 1;
179         self.next_seed = next_seed;
180     }
181
182     fn add_draw(&mut self, next_seed: [u32; 4]) {
183         self.draws += 1;
184         self.attempts += 1;
185         self.next_seed = next_seed;
186     }
187
188     fn add_stalemate(&mut self, next_seed: [u32; 4]) {
189         self.stalemates += 1;
190         self.attempts += 1;
191         self.next_seed = next_seed;
192     }
193
194     fn win_ratio(&self) -> i32 {
195         (self.victories as i32 - self.defeats as i32) * 10000 / (self.attempts as i32)
196     }
197
198     //TODO: Devalue nothing so that it doesn't stand and do nothing when it can do things
199     fn init_command_scores(state: &BitwiseGameState) -> Vec<CommandScore> {
200         let all_buildings = sensible_buildings(&state.player, &state.player_buildings, state.player_has_max_teslas());
201
202         let unoccupied_cells = (0..state.unoccupied_player_cell_count()).map(|i| state.location_of_unoccupied_player_cell(i));
203
204         let building_command_count = unoccupied_cells.len()*all_buildings.len();
205         let nothing_count = 1;
206         
207         let mut commands = Vec::with_capacity(building_command_count + nothing_count);
208         commands.push(CommandScore::new(Command::Nothing));
209
210         for position in unoccupied_cells {
211             for &building in &all_buildings {
212                 commands.push(CommandScore::new(Command::Build(position, building)));
213             }
214         }
215
216         commands
217     }
218 }
219
220 #[cfg(not(feature = "energy-cutoff"))]
221 fn sensible_buildings(player: &Player, _player_buildings: &PlayerBuildings, has_max_teslas: bool) -> Vec<BuildingType> {
222     let mut result = Vec::with_capacity(4);
223
224     if DEFENCE_PRICE <= player.energy {
225         result.push(BuildingType::Defence);
226     }
227     if MISSILE_PRICE <= player.energy {
228         result.push(BuildingType::Attack);
229     }
230     if ENERGY_PRICE <= player.energy {
231         result.push(BuildingType::Energy);
232     }
233     if TESLA_PRICE <= player.energy && !has_max_teslas {
234         result.push(BuildingType::Tesla);
235     }
236
237     result
238 }
239
240
241 //TODO: Heuristic that avoids building the initial energy towers all in the same row?
242 //TODO: Update cutoff to maybe build iron curtains
243 #[cfg(feature = "energy-cutoff")]
244 fn sensible_buildings(player: &Player, player_buildings: &PlayerBuildings, has_max_teslas: bool) -> Vec<BuildingType> {
245     let mut result = Vec::with_capacity(4);
246     let needs_energy = player_buildings.energy_generated() <= ENERGY_PRODUCTION_CUTOFF ||
247         player.energy <= ENERGY_STORAGE_CUTOFF;
248
249     if DEFENCE_PRICE <= player.energy {
250         result.push(BuildingType::Defence);
251     }
252     if MISSILE_PRICE <= player.energy {
253         result.push(BuildingType::Attack);
254     }
255     if ENERGY_PRICE <= player.energy && needs_energy {
256         result.push(BuildingType::Energy);
257     }
258     if TESLA_PRICE <= player.energy && !has_max_teslas {
259         result.push(BuildingType::Tesla);
260     }
261     
262     result
263 }
264