def get_decision(self) -> List['Action']: """Top level interface for the heuristic to generate a decision """ if self.game.points == 0: return [actions.EndRound()] # Handle BioTerrorism Events with higher priority if len(self.game.has_new_bioTerrorism) > 0: city = self.game.has_new_bioTerrorism[0] if actions.PutUnderQuarantine.is_possible(self.game, 2, city): return [actions.PutUnderQuarantine(city, 2)] if self.game.round > 1 and self.game.points <= 20: return [actions.EndRound()] # Add an EndRound action, so action_list will never be empty # TODO: Rethink rank self.action_list.append((0, actions.EndRound())) # Focus on pathogens first # This will populate the action list self.evaluate_pathogens() # Order action list descending by rank self.action_list = sorted( self.action_list, key=lambda action: action[0], reverse=True) # Return just the action without rank return [ranked_action[1] for ranked_action in self.action_list]
def get_best_city_action(self, ava_points, points_needed=0): """ Get most important action of a given sorted action list and optionally consider how much point shall be available in the future :param points_needed: points needed for an action that is desired to be executed next :param ava_points: point allowed to use :return: the best possible city action ready to be build """ if ava_points == 0: # Impossible to do anything if current amount of points is 0 # However, currently, this should never become relevant since this case is considered earlier return [(actions.EndRound(), 1)] # Check if desired action for next round exists if points_needed == 0: return self.select_action_for_points(ava_points) # Logic for wait "heuristic" points_per_round = self.game.points_per_round if points_per_round < points_needed: # If pointer_per_round < needed points if ava_points > points_needed - points_per_round: # 42 > 40 - 20 # if pointer_per_round + ava__points > needed_points, spend: ava - (needed - per_round) points_to_spend = ava_points - (points_needed - points_per_round) return self.select_action_for_points(points_to_spend) elif points_per_round + ava_points == points_needed: # if pointer per round + ava = needed points, end round return [(actions.EndRound(), 1)] elif points_per_round + ava_points < points_needed: # if pointer per round + ava < needed_points, check additional_num rounds # and if you can execute action right now additional_missing_points = abs(ava_points + points_per_round - points_needed) additional_num_of_rounds_to_wait = math.ceil(additional_missing_points / points_per_round) # Amount of points that are not used while waiting for the points to do the desired action too_much_points = additional_num_of_rounds_to_wait * points_per_round - additional_missing_points if ava_points >= too_much_points: # If the points, that are going to be too much later, are already available, use them # Simply search for best city action whereby needed points is adapted # As "Next round, only the points that are not too much + the default points per round are needed" tmp_points_needed = points_per_round + (ava_points - too_much_points) return self.get_best_city_action(ava_points, points_needed=tmp_points_needed) # If the overflow of points does not happen in this round, # the following executions of the code in the next round will catch the points and spend them # Hence we can just end the round here # same case as if we have to wait the num of round and are not allowed to spend any of it return [(actions.EndRound(), 1)] # if pointer_per_round >= needed_points, spend all ava_points return self.select_action_for_points(ava_points)
def round_1_gameplan(self, ava_points): """ Get best action in round 1 :param ava_points: points to spend in this round :return: Action object or list of action objects depending on the gameplan situation """ if ava_points < 40: return actions.EndRound() # Else First action in first round # Get all cities in which the mobility must be reduced candidate_cities = self.get_duration_candidate_cities() if candidate_cities: # Features pats_in_candidate_cities = [city.outbreak.pathogen for city in candidate_cities] not_candidate = [pat for pat in self.game.pathogens_in_cities if pat not in pats_in_candidate_cities] most_important_pat_candidate = sorted(candidate_cities, key=lambda x: compute_pathogen_importance( self.weighted_pathogens[x.outbreak.pathogen.name], x.outbreak.pathogen), reverse=True)[0] pat_cand_rank = compute_pathogen_importance( self.weighted_pathogens[most_important_pat_candidate.outbreak.pathogen.name], most_important_pat_candidate.outbreak.pathogen) more_important_ava = [pat for pat in not_candidate if compute_pathogen_importance(self.weighted_pathogens[pat.name], pat) > pat_cand_rank] # Use features to test if a harmless mobile exits # (e.g. a pathogen which would spread to every city and infect them before any other could # and if a more important existing pathogen is not a candidate, # if both are True skip this step and go to global actions if not more_important_ava: return self.get_action_for_candidate_cities(candidate_cities, ava_points) return self.sorted_global_actions[:self.max_list_len]
def get_action_for_global_actions(self, ava_points): """ Get the best global action or the best action to perform this global action in the future :param ava_points: points to spend :return: Action object """ tmp_var_developing, tmp_var_developed, tmp_var_developing_or_developed, \ tmp_developed_state_dict, tmp_overall_state_dict, \ tmp_every_pat_has_dev, tmp_any_pat_has_ava = self.get_pat_state() # If for every pathogen something is developing and nothing is already developed if tmp_every_pat_has_dev and not tmp_any_pat_has_ava: # just save the points to deploy once everything is developed return actions.EndRound() # If at least one medication or vaccine is still developing if tmp_var_developing: if tmp_var_developing_or_developed: # and each pathogen has a medication or a vaccine in development or already developed # Do best city action return self.get_best_city_action(ava_points) # or and not each pathogen has a medication or a vaccine in development or already developed # Do best global action (i.e. develop a medication or vaccine for a so far not considered pathogen # Filter out actions for pathogens which are already developing or developed filtered_global_actions = [(tmp_action, rank) for (tmp_action, rank) in self.sorted_global_actions if not tmp_overall_state_dict[tmp_action.parameters['pathogen']]] # Resulting list can not be empty since this would result in the previous if being true, # further order is preserved # Thus simply get new most important by taking the first one return [(self.plan_global_action(action, ava_points), rank) for (action, rank) in filtered_global_actions] # If non is currently developing if not tmp_var_developed: # and not each pathogen has either medication or vaccine developed filtered_global_actions = [(tmp_action, rank) for (tmp_action, rank) in self.sorted_global_actions if not tmp_developed_state_dict[tmp_action.parameters['pathogen']]] return [(self.plan_global_action(action, ava_points), rank) for (action, rank) in filtered_global_actions] # Dynamic cap which is maxed at 3 rounds - defines when another global action should be performed # A pat is in need of a global action if medi/vaci is needed but not available pat_in_need_of_global_action = [pat.name for pat in self.game.pathogens_in_cities if self.dev_needed.get(pat.name, 0) >= int(0.5 * len(self.game.get_cities_with_pathogen(pat)))] filtered_global_actions = [global_action for global_action in self.sorted_global_actions if global_action[0].parameters['pathogen'] in pat_in_need_of_global_action] # Check whether a global action for a pathogen exists which is in need of action if pat_in_need_of_global_action and filtered_global_actions: # Special trigger return [(self.plan_global_action(action, ava_points), rank) for (action, rank) in filtered_global_actions] if self.game.last_development_finished_since >= 3: # TODO: Possible improvement: extend features to use for dynamic max and create exceptions return [(self.plan_global_action(action, ava_points), rank) for (action, rank) in self.sorted_global_actions] return self.get_best_city_action(ava_points)
def get_action_for_isolated_cities(self, isolated_cities, ava_points, saved_points): ava_points = saved_points + ava_points still_isolated = [city for city in isolated_cities if city.under_quarantine or city.airport_closed] no_isolation_present = [city for city in isolated_cities if city not in still_isolated] if still_isolated: # all isolated cities are still isolated - hence get points to keep them isolated next round points_needed_in_future = actions.PutUnderQuarantine.get_costs(2) if ava_points + self.game.points_per_round >= points_needed_in_future: if no_isolation_present: return self.get_action_for_no_isolation_present(no_isolation_present, ava_points, saved_points) # # Additional puffer points_needed_in_future *= 2 pats_isolated = [city.outbreak.pathogen.name for city in isolated_cities] # more points than needed in future, do something with them rest_points = ava_points - points_needed_in_future + self.game.points_per_round # Get global actions which are cheap enough and of a different pathogen global_actions = [(self.plan_global_action(action, rest_points), rank) for (action, rank) in self.sorted_global_actions if action.costs <= rest_points and action.parameters['pathogen'] not in pats_isolated] # Get action list for city action_list = self.get_best_city_action(ava_points, points_needed=points_needed_in_future) # Needed for later med_or_vac_in_list = [bool(action.type in ['deployVaccine', 'deployMedication']) for (action, _) in action_list] # Check if global action might be a better idea if global_actions and (True not in med_or_vac_in_list): # if 20 or more points try a global action # (global action list would be empty if less than 20 points and thus also considered like this) action_list = global_actions return action_list # need more points, hence wait for them return actions.EndRound() return self.get_action_for_no_isolation_present(no_isolation_present, ava_points, saved_points)
def select_action_for_points(self, points): """ Logic for spending the specified amount of points :param points: points to spend :return: list of best possible actions for the specified number of points """ result_list = [] for _ in range(self.max_list_len): most_important_action = self.sorted_city_action[0][0] most_important_action_rank = self.sorted_city_action[0][1] # Basic idea if most_important_action.costs <= points or most_important_action.recalculate_costs_for_points( points).costs <= points: # Do most important if possible result_list.append((most_important_action, most_important_action_rank)) else: # Recalculate for actions (only changes the value if it would become due to the adjustment) recalc_actions = [(action.recalculate_costs_for_points(points), importance) for (action, importance) in self.sorted_city_action] # Find best possible action in list for points whereby reduce mobility action costs are fitted possible_actions_for_points = [(action, importance) for (action, importance) in recalc_actions if action.costs <= points] if possible_actions_for_points: # Return most important of these actions result_list.append((possible_actions_for_points[0][0], possible_actions_for_points[0][1])) else: # Not enough points for any action rank = 1 if result_list: rank = result_list[-1][1] * 0.8 result_list.append((actions.EndRound(), rank)) # Remove element from list selected_action = result_list[-1][0] if not selected_action.type == 'endRound': self.remove_element_from_list(selected_action) return result_list
def get_decision(game: Game): """ Get decision for the human heuristic - main entry point :param game: Game Object :return: List[Action] """ # Init game = copy.deepcopy(game) stateheuristic = Stateheuristic(game) # Simple shortcut to avoid recomputing everything in this case # because action list would consist only of this element in the end if game.points == 0: return [actions.EndRound() ] # if decide to add value: (actions.EndRound(), 1.0) # Rank of each global action (Scaled to 0-1 whereby most important is 1) ranked_global_actions = stateheuristic.rank_global_actions() # Rank of each city (Scaled to 0-1 whereby most important is 1) city_ranks = stateheuristic.rank_cities() # Rank of each action per city (Scaled to 0-1 for each city whereby most important = 1) ranked_city_actions_per_city = stateheuristic.rank_actions_for_cities() # Combine ranks of city and actions for cities to have one flat list combined_ranks = h_utils.compute_combined_importance( city_ranks, ranked_city_actions_per_city) # Init Gameplan gameplan = Gameplan(game, combined_ranks, ranked_global_actions, stateheuristic.weighted_pathogens, stateheuristic.dev_needed) # Build a list of actions whereby the first one is most important action_list = gameplan.build_action_list() return action_list
def round_x_gameplan(self, ava_points): """ Get best action any round greater 1 :param ava_points: points to spend in this round :return: Action object or list of action objects depending on the gameplan situation """ # Build up fall back points for bio terrorism and isolation in round X gameplan ava_points -= 20 saved_points = 20 isolated_cities = self.get_isolated_cities() # Isolated Gameplan (Make pathogens die out) if isolated_cities: # Get real number of points to avoid having not isolated cities # Also counters bio terrorism since bio terrorism only needs to be specially treated if it is also isolated # It only does no catch if we have an isolated bio terrorism with mobility equal 0 # - this case is handled by default by the global action gamneplan and heuristic return self.get_action_for_isolated_cities(isolated_cities, ava_points, saved_points) cities_with_bio_terrorism = self.game.has_new_bioTerrorism # Bio Terrorism game plan (counter bio terrorism of completely new pathogens) if cities_with_bio_terrorism: # we have a isolated bio terrorism with mobility equal 0, hence add saved points to ava points ava_points += saved_points # Save points if ava_points <= 0: return actions.EndRound() if not self.sorted_global_actions: # If no global actions anymore, do most important city action return self.get_best_city_action(ava_points) return self.get_action_for_global_actions(ava_points)