Package horizons :: Package ai :: Package aiplayer :: Package combat :: Module combatmanager
[hide private]
[frames] | no frames]

Source Code for Module horizons.ai.aiplayer.combat.combatmanager

  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 logging 
 23   
 24  from fife import fife 
 25   
 26  import horizons.globals 
 27  from horizons.ai.aiplayer.behavior import BehaviorManager 
 28  from horizons.ai.aiplayer.behavior.movecallbacks import BehaviorMoveCallback 
 29  from horizons.ai.aiplayer.combat.unitmanager import UnitManager 
 30  from horizons.component.namedcomponent import NamedComponent 
 31  from horizons.constants import AI, LAYERS 
 32  from horizons.ext.enum import Enum 
 33  from horizons.util.python.callback import Callback 
 34  from horizons.util.python.defaultweakkeydictionary import DefaultWeakKeyDictionary 
 35  from horizons.util.worldobject import WorldObject 
36 37 38 -class CombatManager:
39 """ 40 CombatManager object is responsible for handling close combat in game. 41 It scans the environment (lookout) and requests certain actions from behavior 42 """ 43 log = logging.getLogger("ai.aiplayer.combat.combatmanager") 44 45 # states to keep track of combat movement of each ship. 46 47 shipStates = Enum('idle', 'moving') 48 49 combat_range = 18 50
51 - def __init__(self, owner):
52 super().__init__() # TODO: check if this call is needed 53 self.__init(owner)
54
55 - def __init(self, owner):
56 self.owner = owner 57 self.unit_manager = owner.unit_manager 58 self.world = owner.world 59 self.session = owner.session 60 61 # Dictionary of ship => shipState 62 self.ships = DefaultWeakKeyDictionary(lambda ship: self.shipStates.idle)
63 64 @classmethod
65 - def close_range(cls, ship):
66 """ 67 Range used when wanting to get close to ships. 68 """ 69 return (2 * ship._max_range + ship._min_range) / 3 + 1
70 71 @classmethod
72 - def fallback_range(cls, ship):
73 """ 74 Range used when wanting to get away from ships. 75 """ 76 return cls.combat_range - 1
77
78 - def save(self, db):
79 for ship, state in self.ships.items(): 80 db("INSERT INTO ai_combat_ship (owner_id, ship_id, state_id) VALUES (?, ?, ?)", self.owner.worldid, ship.worldid, state.index)
81
82 - def set_ship_state(self, ship, state):
83 self.ships[ship] = state
84
85 - def get_ship_state(self, ship):
86 if ship not in self.ships: 87 self.ships[ship] = self.shipStates.idle 88 return self.ships[ship]
89
90 - def add_new_unit(self, ship, state=None):
91 if not state: 92 state = self.shipStates.idle 93 94 self.set_ship_state(ship, state)
95
96 - def remove_unit(self, ship):
97 if ship in self.ships: 98 del self.ships[ship]
99 100 @classmethod
101 - def load(cls, db, owner):
102 self = cls.__new__(cls) 103 self._load(db, owner) 104 return self
105
106 - def _load(self, db, owner):
107 self.__init(owner) 108 109 db_result = db("SELECT ship_id, state_id FROM ai_combat_ship WHERE owner_id = ?", self.owner.worldid) 110 for (ship_id, state_id,) in db_result: 111 ship = WorldObject.get_object_by_id(ship_id) 112 state = self.shipStates[state_id] 113 114 # add move callbacks corresponding to given state 115 if state == self.shipStates.moving: 116 ship.add_move_callback(Callback(BehaviorMoveCallback._arrived, ship)) 117 118 self.add_new_unit(ship, state)
119 120 # DISPLAY-RELATED FUNCTIONS
121 - def _init_fake_tile(self):
122 """Sets the _fake_tile_obj class variable with a ready to use fife object. To create a new fake tile, use _add_fake_tile()""" 123 # use fixed SelectableBuildingComponent here, to make sure subclasses also read the same variable 124 if not hasattr(CombatManager, "_fake_range_tile_obj"): 125 # create object to create instances from 126 CombatManager._fake_range_tile_obj = horizons.globals.fife.engine.getModel().createObject('_fake_range_tile_obj', 'ground') 127 fife.ObjectVisual.create(CombatManager._fake_range_tile_obj) 128 129 img_path = 'content/gfx/fake_water.png' 130 img = horizons.globals.fife.imagemanager.load(img_path) 131 for rotation in [45, 135, 225, 315]: 132 CombatManager._fake_range_tile_obj.get2dGfxVisual().addStaticImage(rotation, img.getHandle()) 133 if not hasattr(self, '_selected_fake_tiles'): 134 self._selected_fake_tiles = [] 135 if not hasattr(self, '_selected_tiles'): 136 self._selected_tiles = []
137
138 - def _add_fake_tile(self, x, y, layer, renderer, color):
139 """Adds a fake tile to the position. Requires 'self._fake_tile_obj' to be set.""" 140 inst = layer.createInstance(CombatManager._fake_range_tile_obj, fife.ModelCoordinate(x, y, 0), "") 141 fife.InstanceVisual.create(inst) 142 self._selected_fake_tiles.append(inst) 143 renderer.addColored(inst, *color)
144
145 - def _add_tile(self, tile, renderer, color):
146 self._selected_tiles.append(tile) 147 renderer.addColored(tile._instance, *color)
148
149 - def _clear_fake_tiles(self):
150 if not hasattr(self, '_selected_fake_tiles'): 151 return 152 renderer = self.session.view.renderer['InstanceRenderer'] 153 for tile in self._selected_fake_tiles: 154 renderer.removeColored(tile) 155 self.session.view.layers[LAYERS.FIELDS].deleteInstance(tile) 156 self._selected_fake_tiles = [] 157 158 for tile in self._selected_tiles: 159 renderer.removeColored(tile._instance) 160 self._selected_tiles = []
161
162 - def _highlight_points(self, points, color):
163 renderer = self.session.view.renderer['InstanceRenderer'] 164 layer = self.session.world.session.view.layers[LAYERS.FIELDS] 165 for point in points: 166 tup = (point.x, point.y) 167 island_tile = [island for island in self.session.world.islands if island.get_tile_tuple(tup)] 168 if island_tile: 169 tile = island_tile[0].get_tile_tuple(tup) 170 self._add_tile(tile, renderer, color) 171 else: 172 self._add_fake_tile(tup[0], tup[1], layer, renderer, color)
173
174 - def _highlight_circle(self, position, radius, color):
175 points = set(self.session.world.get_points_in_radius(position, radius)) 176 points2 = set(self.session.world.get_points_in_radius(position, radius - 1)) 177 self._highlight_points(list(points - points2), color)
178
179 - def display(self):
180 """ 181 Display combat ranges. 182 """ 183 if not AI.HIGHLIGHT_COMBAT: 184 return 185 186 combat_range_color = (80, 0, 250) 187 attack_range_color = (255, 0, 0) 188 close_range_color = (0, 0, 100) 189 fallback_range_color = (0, 180, 100) 190 center_point_color = (0, 200, 0) 191 192 self._clear_fake_tiles() 193 self._init_fake_tile() 194 195 for ship, state in self.ships.items(): 196 range = self.combat_range 197 self._highlight_circle(ship.position, range, combat_range_color) 198 self._highlight_circle(ship.position, self.close_range(ship), close_range_color) 199 self._highlight_circle(ship.position, self.fallback_range(ship), fallback_range_color) 200 self._highlight_circle(ship.position, ship._max_range, attack_range_color) 201 self._highlight_circle(ship.position, ship._min_range, attack_range_color) 202 self._highlight_points([ship.position], center_point_color)
203
204 - def handle_mission_combat(self, mission):
205 """ 206 Routine for handling combat in mission that requests for it. 207 """ 208 filters = self.unit_manager.filtering_rules 209 fleet = mission.fleet 210 211 ship_group = fleet.get_ships() 212 ship_group = self.unit_manager.filter_ships(ship_group, (filters.ship_state(self.ships, self.shipStates.idle))) 213 214 if not ship_group: 215 mission.abort_mission() 216 217 ships_around = self.unit_manager.find_ships_near_group(ship_group, self.combat_range) 218 ships_around = self.unit_manager.filter_ships(ships_around, (filters.hostile(), )) 219 pirate_ships = self.unit_manager.filter_ships(ships_around, (filters.pirate, )) 220 fighting_ships = self.unit_manager.filter_ships(ships_around, (filters.fighting(), )) 221 working_ships = self.unit_manager.filter_ships(ships_around, (filters.working(), )) 222 223 environment = {'ship_group': ship_group} 224 225 # begin combat if it's still unresolved 226 if fighting_ships: 227 environment['enemies'] = fighting_ships 228 environment['power_balance'] = UnitManager.calculate_power_balance(ship_group, fighting_ships) 229 self.log.debug("Player: %s vs Player: %s -> power_balance:%s", self.owner.name, fighting_ships[0].owner.name, environment['power_balance']) 230 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 231 'fighting_ships_in_sight', **environment) 232 elif pirate_ships: 233 environment['enemies'] = pirate_ships 234 environment['power_balance'] = UnitManager.calculate_power_balance(ship_group, pirate_ships) 235 self.log.debug("Player: %s vs Player: %s -> power_balance:%s", self.owner.name, pirate_ships[0].owner.name, environment['power_balance']) 236 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 237 'pirate_ships_in_sight', **environment) 238 elif working_ships: 239 environment['enemies'] = working_ships 240 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 241 'working_ships_in_sight', **environment) 242 else: 243 # no one else is around to fight -> continue mission 244 mission.continue_mission()
245
246 - def handle_uncertain_combat(self, mission):
247 """ 248 Handles fleets that may way to be in combat. 249 """ 250 filters = self.unit_manager.filtering_rules 251 252 # test first whether requesting for combat is of any use (any ships nearby) 253 ship_group = mission.fleet.get_ships() 254 255 # filter out ships that are already doing a combat move 256 ship_group = self.unit_manager.filter_ships(ship_group, (filters.ship_state(self.ships, self.shipStates.idle))) 257 ships_around = self.unit_manager.find_ships_near_group(ship_group, self.combat_range) 258 ships_around = self.unit_manager.filter_ships(ships_around, (filters.hostile())) 259 pirate_ships = self.unit_manager.filter_ships(ships_around, (filters.pirate, )) 260 fighting_ships = self.unit_manager.filter_ships(ships_around, (filters.fighting(), )) 261 working_ships = self.unit_manager.filter_ships(ships_around, (filters.working(), )) 262 263 if fighting_ships: 264 environment = {'enemies': fighting_ships} 265 if self.owner.strategy_manager.request_to_pause_mission(mission, **environment): 266 self.handle_mission_combat(mission) 267 elif pirate_ships: 268 environment = {'enemies': pirate_ships} 269 if self.owner.strategy_manager.request_to_pause_mission(mission, **environment): 270 self.handle_mission_combat(mission) 271 elif working_ships: 272 environment = {'enemies': working_ships} 273 if self.owner.strategy_manager.request_to_pause_mission(mission, **environment): 274 self.handle_mission_combat(mission)
275
276 - def handle_casual_combat(self):
277 """ 278 Handles combat for ships wandering around the map (not assigned to any fleet/mission). 279 """ 280 filters = self.unit_manager.filtering_rules 281 282 rules = (filters.not_in_fleet, filters.fighting(), filters.ship_state(self.ships, self.shipStates.idle)) 283 for ship in self.unit_manager.get_ships(rules): 284 # Turn into one-ship group, since reasoning is based around groups of ships 285 ship_group = [ship, ] 286 # TODO: create artificial groups by dividing ships that are nearby into groups based on their distance. 287 # This may end up being costly, so postpone until we have more cpu resources to spare. 288 289 ships_around = self.unit_manager.find_ships_near_group(ship_group, self.combat_range) 290 pirate_ships = self.unit_manager.filter_ships(ships_around, (filters.pirate, )) 291 fighting_ships = self.unit_manager.filter_ships(ships_around, (filters.fighting(), )) 292 working_ships = self.unit_manager.filter_ships(ships_around, (filters.working(), )) 293 environment = {'ship_group': ship_group} 294 if fighting_ships: 295 environment['enemies'] = fighting_ships 296 environment['power_balance'] = UnitManager.calculate_power_balance(ship_group, fighting_ships) 297 self.log.debug("Player: %s vs Player: %s -> power_balance:%s", self.owner.name, fighting_ships[0].owner.name, environment['power_balance']) 298 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 299 'fighting_ships_in_sight', **environment) 300 elif pirate_ships: 301 environment['enemies'] = pirate_ships 302 environment['power_balance'] = UnitManager.calculate_power_balance(ship_group, pirate_ships) 303 self.log.debug("Player: %s vs Player: %s -> power_balance:%s", self.owner.name, pirate_ships[0].owner.name, environment['power_balance']) 304 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 305 'pirate_ships_in_sight', **environment) 306 elif working_ships: 307 environment['enemies'] = working_ships 308 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 309 'working_ships_in_sight', **environment) 310 else: 311 # execute idle action only if whole fleet is idle 312 # we check for AIPlayer state here 313 if all((self.owner.ships[ship] == self.owner.shipStates.idle for ship in ship_group)): 314 self.owner.behavior_manager.request_action(BehaviorManager.action_types.idle, 315 'no_one_in_sight', **environment)
316
317 - def lookout(self):
318 """ 319 Basically do 3 things: 320 1. Handle combat for missions that explicitly request for it. 321 2. Check whether any of current missions may want to be interrupted to resolve potential 322 combat that was not planned (e.g. hostile ships nearby fleet on a mission) 323 3. Handle combat for ships currently not used in any mission. 324 """ 325 # handle fleets that explicitly request to be in combat 326 for mission in self.owner.strategy_manager.get_missions(condition=lambda mission: mission.combat_phase): 327 self.handle_mission_combat(mission) 328 329 # handle fleets that may way to be in combat, but request for it first 330 for mission in self.owner.strategy_manager.get_missions(condition=lambda mission: not mission.combat_phase): 331 self.handle_uncertain_combat(mission) 332 333 # handle idle ships that are wandering around the map 334 self.handle_casual_combat() 335 336 # Log ship states every tick 337 if self.log.isEnabledFor(logging.DEBUG): 338 self.log.debug("Player:%s Ships combat states:", self.owner.name) 339 for ship, state in self.ships.items(): 340 self.log.debug(" %s: %s", ship.get_component(NamedComponent).name, state)
341
342 - def tick(self):
343 self.lookout() 344 self.display()
345
346 347 -class PirateCombatManager(CombatManager):
348 """ 349 Pirate player requires slightly different handling of combat, thus it gets his own CombatManager. 350 Pirate player is able to use standard BehaviorComponents in it's BehaviorManager. 351 """ 352 log = logging.getLogger("ai.aiplayer.piratecombatmanager") 353 354 shipStates = Enum.get_extended(CombatManager.shipStates, 'chasing_ship', 'going_home') 355
356 - def __init__(self, owner):
357 super().__init__(owner)
358
359 - def handle_mission_combat(self, mission):
360 """ 361 Routine for handling combat in mission that requests for it. 362 """ 363 filters = self.unit_manager.filtering_rules 364 fleet = mission.fleet 365 366 ship_group = fleet.get_ships() 367 ship_group = self.unit_manager.filter_ships(ship_group, (filters.ship_state(self.ships, self.shipStates.idle))) 368 369 if not ship_group: 370 mission.abort_mission() 371 372 ships_around = self.unit_manager.find_ships_near_group(ship_group, self.combat_range) 373 ships_around = self.unit_manager.filter_ships(ships_around, (filters.hostile(), )) 374 fighting_ships = self.unit_manager.filter_ships(ships_around, (filters.fighting(), )) 375 working_ships = self.unit_manager.filter_ships(ships_around, (filters.working(), )) 376 377 environment = {'ship_group': ship_group} 378 379 # begin combat if it's still unresolved 380 if fighting_ships: 381 environment['enemies'] = fighting_ships 382 environment['power_balance'] = UnitManager.calculate_power_balance(ship_group, fighting_ships) 383 self.log.debug("Player: %s vs Player: %s -> power_balance:%s", self.owner.name, fighting_ships[0].owner.name, environment['power_balance']) 384 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 385 'fighting_ships_in_sight', **environment) 386 elif working_ships: 387 environment['enemies'] = working_ships 388 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 389 'working_ships_in_sight', **environment) 390 else: 391 # no one else is around to fight -> continue mission 392 mission.continue_mission()
393
394 - def handle_uncertain_combat(self, mission):
395 """ 396 Handles fleets that may way to be in combat. 397 """ 398 filters = self.unit_manager.filtering_rules 399 400 # test first whether requesting for combat is of any use (any ships nearby) 401 ship_group = mission.fleet.get_ships() 402 ship_group = self.unit_manager.filter_ships(ship_group, (filters.ship_state(self.ships, self.shipStates.idle))) 403 ships_around = self.unit_manager.find_ships_near_group(ship_group, self.combat_range) 404 ships_around = self.unit_manager.filter_ships(ships_around, (filters.hostile())) 405 fighting_ships = self.unit_manager.filter_ships(ships_around, (filters.fighting(), )) 406 working_ships = self.unit_manager.filter_ships(ships_around, (filters.working(), )) 407 408 if fighting_ships: 409 environment = {'enemies': fighting_ships} 410 if self.owner.strategy_manager.request_to_pause_mission(mission, **environment): 411 self.handle_mission_combat(mission) 412 elif working_ships: 413 environment = {'enemies': working_ships} 414 if self.owner.strategy_manager.request_to_pause_mission(mission, **environment): 415 self.handle_mission_combat(mission)
416
417 - def handle_casual_combat(self):
418 """ 419 Combat with idle ships (not assigned to a mission) 420 """ 421 filters = self.unit_manager.filtering_rules 422 423 rules = (filters.not_in_fleet, filters.pirate, filters.ship_state(self.ships, self.shipStates.idle)) 424 for ship in self.unit_manager.get_ships(rules): 425 # Turn into one-ship group, since reasoning is based around groups of ships 426 ship_group = [ship, ] 427 428 ships_around = self.unit_manager.find_ships_near_group(ship_group, self.combat_range) 429 fighting_ships = self.unit_manager.filter_ships(ships_around, (filters.fighting(), )) 430 working_ships = self.unit_manager.filter_ships(ships_around, (filters.working(), )) 431 environment = {'ship_group': ship_group} 432 if fighting_ships: 433 environment['enemies'] = fighting_ships 434 environment['power_balance'] = UnitManager.calculate_power_balance(ship_group, fighting_ships) 435 self.log.debug("Player: %s vs Player: %s -> power_balance:%s", self.owner.name, fighting_ships[0].owner.name, environment['power_balance']) 436 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 437 'fighting_ships_in_sight', **environment) 438 elif working_ships: 439 environment['enemies'] = working_ships 440 self.owner.behavior_manager.request_action(BehaviorManager.action_types.offensive, 441 'working_ships_in_sight', **environment) 442 else: 443 self.owner.behavior_manager.request_action(BehaviorManager.action_types.idle, 444 'no_one_in_sight', **environment)
445