0a51b392d97b418a96d9ed192d2a86e025829f09
[entelect-challenge-tower-defence.git] / src / strategy / monte_carlo.rs
1 use engine::settings::GameSettings;
2 use engine::command::*;
3 use engine::geometry::*;
4 use engine::{GameState, GameStatus, Player};
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 #[cfg(feature = "energy-cutoff")] pub const ENERGY_PRODUCTION_CUTOFF: u16 = 30;
16 #[cfg(feature = "energy-cutoff")] pub const ENERGY_STORAGE_CUTOFF: u16 = 45;
17
18 pub fn choose_move<GS: GameState>(settings: &GameSettings, state: &GS, start_time: &PreciseTime, max_time: Duration) -> Command {
19     let mut command_scores = CommandScore::init_command_scores(settings, state);
20     let command = simulate_options_to_timeout(&mut command_scores, settings, state, start_time, max_time);
21
22     #[cfg(feature = "benchmarking")]
23     {
24         let total_iterations: u32 = command_scores.iter().map(|c| c.attempts).sum();
25         println!("Iterations: {}", total_iterations);
26     }
27     
28     match command {
29         Some(command) => command.command,
30         _ => Command::Nothing
31     }
32 }
33
34 #[cfg(not(feature = "discard-poor-performers"))]
35 fn simulate_options_to_timeout<'a, GS: GameState>(command_scores: &'a mut Vec<CommandScore>, settings: &GameSettings, state: &GS, start_time: &PreciseTime, max_time: Duration) -> Option<&'a CommandScore> {
36     loop {
37         simulate_all_options_once(command_scores, settings, state);
38         if start_time.to(PreciseTime::now()) > max_time {
39             break;
40         }
41     }
42     command_scores.iter().max_by_key(|&c| c.win_ratio())
43 }
44
45 #[cfg(feature = "discard-poor-performers")]
46 fn simulate_options_to_timeout<'a, GS: GameState>(command_scores: &'a mut Vec<CommandScore>, settings: &GameSettings, state: &GS, start_time: &PreciseTime, max_time: Duration) -> Option<&'a CommandScore> {
47     use std::cmp;
48     
49     let maxes = [max_time / 4, max_time / 2, max_time * 3 / 4, max_time];
50     for (i, &max) in maxes.iter().enumerate() {
51         let new_length = cmp::max(20, command_scores.len() / (2usize.pow(i as u32)));
52         let active_scores = &mut command_scores[0..new_length];
53         loop {
54             simulate_all_options_once(active_scores, settings, state);
55             if start_time.to(PreciseTime::now()) > max {
56                 break;
57             }
58         }
59         active_scores.sort_unstable_by_key(|c| -c.win_ratio());
60     }
61     command_scores.first()
62 }
63
64 #[cfg(feature = "single-threaded")]
65 fn simulate_all_options_once<GS: GameState>(command_scores: &mut[CommandScore], settings: &GameSettings, state: &GS) {
66     command_scores.iter_mut()
67         .for_each(|score| {
68             let mut rng = XorShiftRng::from_seed(score.next_seed);
69             simulate_to_endstate(score, settings, state, &mut rng);
70         });
71 }
72
73 #[cfg(not(feature = "single-threaded"))]
74 fn simulate_all_options_once<GS: GameState>(command_scores: &mut[CommandScore], settings: &GameSettings, state: &GS) {
75     command_scores.par_iter_mut()
76         .for_each(|score| {
77             let mut rng = XorShiftRng::from_seed(score.next_seed);
78             simulate_to_endstate(score, settings, state, &mut rng);
79         });
80 }
81
82 fn simulate_to_endstate<R: Rng, GS: GameState>(command_score: &mut CommandScore, settings: &GameSettings, state: &GS, rng: &mut R) {
83     let mut state_mut = state.clone();
84     
85     let opponent_first = random_opponent_move(settings, &state_mut, rng);
86     let mut status = state_mut.simulate(settings, command_score.command, opponent_first);
87     
88     for _ in 0..MAX_MOVES {
89         if status != GameStatus::Continue {
90             break;
91         }
92
93         let player_command = random_player_move(settings, &state_mut, rng);
94         let opponent_command = random_opponent_move(settings, &state_mut, rng);
95         status = state_mut.simulate(settings, player_command, opponent_command);
96     }
97
98     let next_seed = [rng.next_u32(), rng.next_u32(), rng.next_u32(), rng.next_u32()];
99     match status {
100         GameStatus::PlayerWon => command_score.add_victory(next_seed),
101         GameStatus::OpponentWon => command_score.add_defeat(next_seed),
102         GameStatus::Continue => command_score.add_stalemate(next_seed),
103         GameStatus::Draw => command_score.add_draw(next_seed)
104     }
105 }
106
107 fn random_player_move<R: Rng, GS: GameState>(settings: &GameSettings, state: &GS, rng: &mut R) -> Command {
108     let all_buildings = sensible_buildings(settings, &state.player(), state.player_has_max_teslas());
109     random_move(&all_buildings, rng, state.unoccupied_player_cell_count(), |i| state.location_of_unoccupied_player_cell(i))
110 }
111
112 fn random_opponent_move<R: Rng, GS: GameState>(settings: &GameSettings, state: &GS, rng: &mut R) -> Command {
113     let all_buildings = sensible_buildings(settings, &state.opponent(), state.opponent_has_max_teslas());
114     random_move(&all_buildings, rng, state.unoccupied_opponent_cell_count(), |i| state.location_of_unoccupied_opponent_cell(i))
115 }
116
117 fn random_move<R: Rng, F:Fn(usize)->Point>(all_buildings: &[BuildingType], rng: &mut R, free_positions_count: usize, get_point: F) -> Command {
118     let building_command_count = free_positions_count*all_buildings.len();
119     let nothing_count = 1;
120
121     let number_of_commands = building_command_count + nothing_count;
122     
123     let choice_index = rng.gen_range(0, number_of_commands);
124
125     if choice_index == number_of_commands - 1 {
126         Command::Nothing
127     } else {
128         Command::Build(
129             get_point(choice_index/all_buildings.len()),
130             all_buildings[choice_index%all_buildings.len()]
131         )
132     }
133 }
134
135 #[derive(Debug)]
136 struct CommandScore {
137     command: Command,
138     victories: u32,
139     defeats: u32,
140     draws: u32,
141     stalemates: u32,
142     attempts: u32,
143     next_seed: [u32; 4]
144 }
145
146 impl CommandScore {
147     fn new(command: Command) -> CommandScore {
148         CommandScore {
149             command,
150             victories: 0,
151             defeats: 0,
152             draws: 0,
153             stalemates: 0,
154             attempts: 0,
155             next_seed: [0x7b6a_e1f4, 0x413c_e90f, 0x6781_6799, 0x770a_6bda]
156         }
157     }
158
159     fn add_victory(&mut self, next_seed: [u32; 4]) {
160         self.victories += 1;
161         self.attempts += 1;
162         self.next_seed = next_seed;
163     }
164
165     fn add_defeat(&mut self, next_seed: [u32; 4]) {
166         self.defeats += 1;
167         self.attempts += 1;
168         self.next_seed = next_seed;
169     }
170
171     fn add_draw(&mut self, next_seed: [u32; 4]) {
172         self.draws += 1;
173         self.attempts += 1;
174         self.next_seed = next_seed;
175     }
176
177     fn add_stalemate(&mut self, next_seed: [u32; 4]) {
178         self.stalemates += 1;
179         self.attempts += 1;
180         self.next_seed = next_seed;
181     }
182
183     fn win_ratio(&self) -> i32 {
184         (self.victories as i32 - self.defeats as i32) * 10000 / (self.attempts as i32)
185     }
186     
187     fn init_command_scores<GS: GameState>(settings: &GameSettings, state: &GS) -> Vec<CommandScore> {
188         let all_buildings = sensible_buildings(settings, &state.player(), state.player_has_max_teslas());
189
190         let unoccupied_cells = (0..state.unoccupied_player_cell_count()).map(|i| state.location_of_unoccupied_player_cell(i));
191
192         let building_command_count = unoccupied_cells.len()*all_buildings.len();
193         let nothing_count = 1;
194         
195         let mut commands = Vec::with_capacity(building_command_count + nothing_count);
196         commands.push(CommandScore::new(Command::Nothing));
197
198         for position in unoccupied_cells {
199             for &building in &all_buildings {
200                 commands.push(CommandScore::new(Command::Build(position, building)));
201             }
202         }
203
204         commands
205     }
206 }
207
208 #[cfg(not(feature = "energy-cutoff"))]
209 fn sensible_buildings(settings: &GameSettings, player: &Player, has_max_teslas: bool) -> Vec<BuildingType> {
210     let mut result = Vec::with_capacity(4);
211     for b in BuildingType::all().iter() {
212         let building_setting = settings.building_settings(*b);
213         let affordable = building_setting.price <= player.energy;
214         let is_tesla = *b == BuildingType::Tesla;
215         if affordable && (!is_tesla || !has_max_teslas) {
216             result.push(*b);
217         }
218     }
219     result
220 }
221
222
223 #[cfg(feature = "energy-cutoff")]
224 fn sensible_buildings(settings: &GameSettings, player: &Player, has_max_teslas: bool) -> Vec<BuildingType> {
225     let mut result = Vec::with_capacity(4);
226     let needs_energy = player.energy_generated <= ENERGY_PRODUCTION_CUTOFF ||
227         player.energy <= ENERGY_STORAGE_CUTOFF;
228     
229     for b in BuildingType::all().iter() {
230         let building_setting = settings.building_settings(*b);
231         let affordable = building_setting.price <= player.energy;
232         let energy_producing = building_setting.energy_generated_per_turn > 0;
233         let is_tesla = *b == BuildingType::Tesla;
234         if affordable && (!energy_producing || needs_energy) && (!is_tesla || !has_max_teslas) {
235             result.push(*b);
236         }
237     }
238     result
239 }
240