Package horizons :: Package ai :: Package aiplayer :: Package strategy :: Module strategymanager
[hide private]
[frames] | no frames]

Source Code for Module horizons.ai.aiplayer.strategy.strategymanager

  1  # ################################################### 
  2  # Copyright (C) 2008-2017 The Unknown Horizons Team 
  3  # team@unknown-horizons.org 
  4  # This file is part of Unknown Horizons. 
  5  # 
  6  # Unknown Horizons is free software; you can redistribute it and/or modify 
  7  # it under the terms of the GNU General Public License as published by 
  8  # the Free Software Foundation; either version 2 of the License, or 
  9  # (at your option) any later version. 
 10  # 
 11  # This program is distributed in the hope that it will be useful, 
 12  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 13  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 14  # GNU General Public License for more details. 
 15  # 
 16  # You should have received a copy of the GNU General Public License 
 17  # along with this program; if not, write to the 
 18  # Free Software Foundation, Inc., 
 19  # 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA 
 20  # ################################################### 
 21   
 22  import collections 
 23  import logging 
 24   
 25  from horizons.ai.aiplayer.combat.unitmanager import UnitManager 
 26  from horizons.ai.aiplayer.strategy.mission.chaseshipsandattack import ChaseShipsAndAttack 
 27  from horizons.ai.aiplayer.strategy.mission.pirateroutine import PirateRoutine 
 28  from horizons.ai.aiplayer.strategy.mission.scouting import ScoutingMission 
 29  from horizons.ai.aiplayer.strategy.mission.surpriseattack import SurpriseAttack 
 30  from horizons.component.storagecomponent import StorageComponent 
 31  from horizons.constants import RES 
 32  from horizons.util.python import map_balance, trim_value 
 33  from horizons.util.worldobject import WorldObject 
34 35 36 -class StrategyManager:
37 """ 38 StrategyManager object is responsible for handling major decisions in game such as 39 sending fleets to battle, keeping track of diplomacy between players, declare wars. 40 """ 41 log = logging.getLogger("ai.aiplayer.fleetmission") 42 43 # Redundant use of super()?
44 - def __init__(self, owner):
45 super().__init__() # TODO: figure out whether this is needed 46 self.__init(owner)
47
48 - def __init(self, owner):
49 self.owner = owner 50 self.world = owner.world 51 self.session = owner.session 52 self.unit_manager = owner.unit_manager 53 self.missions = set() 54 55 # Dictionary of Condition_hash => FleetMission. Condition_hash is a key since it's searched for more often. Values are 56 # unique because of WorldObject's inheritance, but it makes removing items from it in O(n). 57 self.conditions_being_resolved = {} 58 59 self.missions_to_load = { 60 ScoutingMission: "ai_scouting_mission", 61 SurpriseAttack: "ai_mission_surprise_attack", 62 ChaseShipsAndAttack: "ai_mission_chase_ships_and_attack", 63 }
64 65 @property
66 - def conditions(self):
67 # conditions are held in behavior manager since they are a part of behavior profile (just like actions and strategies) 68 return self.owner.behavior_manager.get_conditions()
69
70 - def calculate_player_wealth_balance(self, other_player):
71 """ 72 Calculates wealth balance between two players. 73 Wealth balance of 1.2 means that self.owner is 1.2 times wealthier than other_player. 74 @param other_player: other player matched against self.owner 75 @type other_player: Player 76 """ 77 78 gold_weight = 0.25 # we don't value gold that much 79 resources_weight = 0.75 80 81 resource_values = [] 82 for player in (self.owner, other_player): 83 resources_value = 0.0 84 for settlement in player.settlements: 85 resources_value += sum((self.session.db.get_res_value(resource) * amount for resource, amount 86 in settlement.get_component(StorageComponent).inventory.itercontents() if self.session.db.get_res_value(resource))) 87 resource_values.append(resources_value) 88 ai_resources, enemy_resources = resource_values 89 90 ai_gold = self.owner.get_component(StorageComponent).inventory[RES.GOLD] 91 enemy_gold = other_player.get_component(StorageComponent).inventory[RES.GOLD] 92 return (ai_resources * resources_weight + ai_gold * gold_weight) / (enemy_resources * resources_weight + enemy_gold * gold_weight)
93
94 - def calculate_player_power_balance(self, other_player):
95 """ 96 Calculates power balance between two players. 97 Power balance of 1.2 means that self.owner is 1.2 times stronger than other_player 98 99 @param other_player: other player who is matched against self.owner 100 @type other_player: Player 101 @return: power balance between self.owner and other_player 102 @rtype: float 103 """ 104 105 min_balance = 10e-7 106 max_balance = 1000.0 107 108 ships = list(self.owner.ships.keys()) 109 ships = self.unit_manager.filter_ships(ships, (self.unit_manager.filtering_rules.fighting(),)) 110 enemy_ships = self.unit_manager.get_player_ships(other_player) 111 enemy_ships = self.unit_manager.filter_ships(enemy_ships, (self.unit_manager.filtering_rules.fighting(),)) 112 113 # infinitely more powerful (is either or both expected to return None?) 114 if ships and not enemy_ships: 115 return max_balance 116 117 # infinitely less powerful (is either or both expected to return None?) 118 elif not ships and enemy_ships: 119 return min_balance 120 elif not ships and not enemy_ships: 121 return 1.0 122 123 return UnitManager.calculate_power_balance(ships, enemy_ships)
124
125 - def calculate_player_terrain_balance(self, other_player):
126 """ 127 Calculates balance between sizes of terrain, i.e. size on map. 128 Terrain balance of 1.2 means that self.owner has 1.2 times larger terrain than other_player 129 """ 130 131 min_balance = 10e-7 132 max_balance = 1000.0 133 134 terrains = [] 135 island_counts = [] 136 for player in (self.owner, other_player): 137 terrain_total = 0 138 islands = set() 139 for settlement in player.settlements: 140 terrain_total += len(settlement.ground_map) 141 islands.add(settlement.island) 142 terrains.append(terrain_total) 143 island_counts.append(len(islands)) 144 145 ai_terrain, enemy_terrain = terrains 146 ai_islands, enemy_islands = island_counts 147 148 # if not (is either or both expected to return None?) 149 if ai_islands and not enemy_islands: 150 return max_balance 151 if not ai_islands and enemy_islands: 152 return min_balance 153 if not ai_islands and not enemy_islands: 154 return 1.0 155 156 island_count_balance = float(ai_islands) / float(enemy_islands) 157 158 # it favors having 3 islands of total size X, than 2 of total size X (or bigger) 159 return (float(ai_terrain) / float(enemy_terrain)) * island_count_balance
160
161 - def calculate_player_balance(self, player, trimming_factor=10.0, linear_boundary=10.0):
162 """ 163 Calculate power balance between self.owner and other player. 164 165 trimming_factor: Since any balance returns values of (0, inf) we agree to assume if x < 0.1 -> x = 0.1 and if x > 10.0 -> x=10.0 166 linear_boundary: boundary of [-10.0, 10.0] for new balance scale 167 168 @param player: player to calculate balance against 169 @type player: Player 170 @param trimming_factor: trim actual balance values to range [1./trimming_factor, trimming_factor] e.g. [0.1, 10.0] 171 @type trimming_factor: float 172 @param linear_boundary: boundaries of new balance scale [-linear_boundary, linear_boundary], e.g. [-10.0, 10.0] 173 @type linear_boundary: float 174 @return: unified balance for various variables 175 @rtype: collections.namedtuple 176 """ 177 wealth_balance = self.owner.strategy_manager.calculate_player_wealth_balance(player) 178 power_balance = self.owner.strategy_manager.calculate_player_power_balance(player) 179 terrain_balance = self.owner.strategy_manager.calculate_player_terrain_balance(player) 180 balance = { 181 'wealth': wealth_balance, 182 'power': power_balance, 183 'terrain': terrain_balance, 184 } 185 balance = {key: trim_value(value, 1. / trimming_factor, trimming_factor) for key, value in balance.items()} 186 balance = {key: map_balance(value, trimming_factor, linear_boundary) for key, value in balance.items()} 187 188 return collections.namedtuple('Balance', 'wealth, power, terrain')(**balance)
189
190 - def save(self, db):
191 for mission in list(self.missions): 192 mission.save(db) 193 194 for condition, mission in self.conditions_being_resolved.items(): 195 db("INSERT INTO ai_condition_lock (owner_id, condition, mission_id) VALUES(?, ?, ?)", self.owner.worldid, condition, mission.worldid)
196 197 @classmethod
198 - def load(cls, db, owner):
199 # TODO: clean up below super() call; it is a hack 200 self = cls.__new__(cls) 201 super(StrategyManager, self).__init__() 202 self.__init(owner) 203 self._load(db) 204 return self
205
206 - def _load(self, db):
207 for class_name, db_table in self.missions_to_load.items(): 208 db_result = db("SELECT m.rowid FROM {} m, ai_fleet_mission f WHERE f.owner_id = ? and m.rowid = f.rowid".format(db_table), self.owner.worldid) 209 for (mission_id,) in db_result: 210 self.missions.add(class_name.load(mission_id, self.owner, db, self.report_success, self.report_failure)) 211 212 # load condition locks 213 db_result = db("SELECT condition, mission_id FROM ai_condition_lock WHERE owner_id = ?", self.owner.worldid) 214 for (condition, mission_id) in db_result: 215 self.conditions_being_resolved[condition] = WorldObject.get_object_by_id(mission_id)
216
217 - def report_success(self, mission, msg):
218 self.log.info("Player: %s|StrategyManager|Mission %s was a success: %s", self.owner.worldid, mission, msg) 219 self.end_mission(mission)
220
221 - def report_failure(self, mission, msg):
222 self.log.info("Player: %s|StrategyManager|Mission %s was a failure: %s", self.owner.worldid, mission, msg) 223 self.end_mission(mission)
224
225 - def end_mission(self, mission):
226 self.log.info("Player: %s|StrategyManager|Mission %s ended", self.owner.worldid, mission) 227 if mission in self.missions: 228 self.missions.remove(mission) 229 230 # remove condition lock (if condition was lockable) after mission ends 231 self.unlock_condition(mission) 232 mission.end()
233
234 - def start_mission(self, mission):
235 self.log.info("Player: %s|StrategyManager|Mission %s started", self.owner.worldid, mission) 236 self.missions.add(mission) 237 mission.start()
238
239 - def lock_condition(self, condition, mission):
240 self.conditions_being_resolved[condition] = mission
241
242 - def unlock_condition(self, mission):
243 # values (FleetMission) are unique so it's possible to remove them this way: 244 for condition, value in self.conditions_being_resolved.items(): 245 if mission == value: 246 del self.conditions_being_resolved[condition] 247 return
248
249 - def get_missions(self, condition=None):
250 """ 251 Get missions filtered by certain condition (by default return all missions) 252 """ 253 if condition: 254 return [mission for mission in self.missions if condition(mission)] 255 else: 256 return self.missions
257
258 - def request_to_pause_mission(self, mission, **environment):
259 """ 260 @return: returns True if mission is allowed to pause, False otherwise 261 @rtype: bool 262 """ 263 # TODO: make that decision based on environment (**environment as argument) 264 mission.pause_mission() 265 return True
266
267 - def get_ships_for_mission(self):
268 filters = self.unit_manager.filtering_rules 269 rules = (filters.ship_state(self.owner.ships, (self.owner.shipStates.idle,)), filters.fighting(), filters.not_in_fleet) 270 idle_ships = self.unit_manager.get_ships(rules) 271 272 return idle_ships
273
274 - def handle_strategy(self):
275 276 # Get all available ships that can take part in a mission 277 idle_ships = self.get_ships_for_mission() 278 279 # Get all other players 280 other_players = [player for player in self.session.world.players if player != self.owner] 281 282 # Check which conditions occur 283 occuring_conditions = [] 284 285 environment = {'idle_ships': idle_ships} 286 287 for player in other_players: 288 # Prepare environment 289 self.log.debug("Conditions occurring against player %s", player.name) 290 environment['player'] = player 291 292 for condition in list(self.conditions.keys()): 293 294 # Check whether given condition is already being resolved 295 if condition.get_identifier(**environment) in self.conditions_being_resolved: 296 self.log.debug(" %s: Locked", condition.__class__.__name__) 297 continue 298 299 condition_outcome = condition.check(**environment) 300 self.log.debug(" %s: %s", condition.__class__.__name__, ("Yes" if condition_outcome else "No")) 301 if condition_outcome: 302 occuring_conditions.append((condition, condition_outcome)) 303 304 # Revert environment to previous state 305 del environment['player'] 306 307 # Nothing to do when none of the conditions occur 308 if occuring_conditions: 309 # Choose the most important one 310 311 selected_condition, selected_outcome = max(occuring_conditions, 312 key=lambda condition_outcome1: self.conditions[condition_outcome1[0]] * condition_outcome1[1]['certainty']) 313 314 self.log.debug("Selected condition: %s", selected_condition.__class__.__name__) 315 for key, value in selected_outcome.items(): 316 # Insert condition-gathered info into environment 317 environment[key] = value 318 self.log.debug(" %s: %s", key, value) 319 320 # Try to execute a mission that resolves given condition the best 321 mission = self.owner.behavior_manager.request_strategy(**environment) 322 if mission: 323 self.start_mission(mission) 324 if selected_condition.lockable: 325 self.lock_condition(selected_condition.get_identifier(**environment), mission) 326 327 self.log.debug("Missions:") 328 for mission in list(self.missions): 329 self.log.debug("%s", mission) 330 331 self.log.debug("Fleets:") 332 for fleet in list(self.unit_manager.fleets): 333 self.log.debug("%s", fleet)
334
335 - def tick(self):
336 self.handle_strategy()
337
338 - def end(self):
339 for mission in self.missions: 340 mission.end()
341
342 343 -class PirateStrategyManager(StrategyManager):
344
345 - def __init__(self, owner):
346 super().__init__(owner) 347 self.__init(owner)
348
349 - def get_ships_for_mission(self):
350 filters = self.unit_manager.filtering_rules 351 rules = (filters.ship_state(self.owner.ships, (self.owner.shipStates.idle,)), filters.pirate, filters.not_in_fleet) 352 idle_ships = self.unit_manager.get_ships(rules) 353 return idle_ships
354 355 @classmethod
356 - def load(cls, db, owner):
357 # TODO: clean below code; it is a _very_ dirty hack 358 self = cls.__new__(cls) 359 super(PirateStrategyManager, self).__init__(owner) 360 self.__init(owner) 361 self._load(db) 362 return self
363
364 - def __init(self, owner):
365 self.missions_to_load = { 366 PirateRoutine: "ai_mission_pirate_routine", 367 }
368