1from logging import debug, warning 2 3import freeOrionAIInterface as fo # pylint: disable=import-error 4 5# the following import is used for type hinting, which pylint seems not to recognize 6from fleet_orders import AIFleetOrder # pylint: disable=unused-import # noqa: F401 7from fleet_orders import OrderMove, OrderPause, OrderOutpost, OrderColonize, OrderMilitary, OrderInvade, OrderDefend 8import AIstate 9import EspionageAI 10import FleetUtilsAI 11import MoveUtilsAI 12import MilitaryAI 13import InvasionAI 14import CombatRatingsAI 15from aistate_interface import get_aistate 16from target import TargetSystem, TargetFleet, TargetPlanet 17from EnumsAI import MissionType 18from AIDependencies import INVALID_ID 19from freeorion_tools import get_partial_visibility_turn, assertion_fails 20 21ORDERS_FOR_MISSION = { 22 MissionType.EXPLORATION: OrderPause, 23 MissionType.OUTPOST: OrderOutpost, 24 MissionType.COLONISATION: OrderColonize, 25 MissionType.INVASION: OrderInvade, 26 MissionType.MILITARY: OrderMilitary, 27 # SECURE is mostly same as MILITARY, but waits for system removal from all targeted system lists 28 # (invasion, colonization, outpost, blockade) before clearing 29 MissionType.SECURE: OrderMilitary, 30 MissionType.PROTECT_REGION: OrderPause, 31 MissionType.ORBITAL_INVASION: OrderInvade, 32 MissionType.ORBITAL_OUTPOST: OrderOutpost, 33 MissionType.ORBITAL_DEFENSE: OrderDefend, 34} 35 36COMBAT_MISSION_TYPES = ( 37 MissionType.MILITARY, 38 MissionType.SECURE, 39 MissionType.PROTECT_REGION, 40) 41 42MERGEABLE_MISSION_TYPES = ( 43 MissionType.MILITARY, 44 MissionType.INVASION, 45 MissionType.PROTECT_REGION, 46 MissionType.ORBITAL_INVASION, 47 MissionType.SECURE, 48 MissionType.ORBITAL_DEFENSE, 49) 50 51 52class AIFleetMission: 53 """ 54 Stores information about AI mission. Every mission has fleetID and AI targets depending upon AI fleet mission type. 55 :type orders: list[AIFleetOrder] 56 :type target: target.Target | None 57 """ 58 59 def __init__(self, fleet_id): 60 self.orders = [] 61 self.fleet = TargetFleet(fleet_id) 62 self.type = None 63 self.target = None 64 65 def __setstate__(self, state): 66 target_type = state.pop("target_type") 67 if state["target"] is not None: 68 object_map = {TargetPlanet.object_name: TargetPlanet, 69 TargetSystem.object_name: TargetSystem, 70 TargetFleet.object_name: TargetFleet} 71 state["target"] = object_map[target_type](state["target"]) 72 73 state["fleet"] = TargetFleet(state["fleet"]) 74 self.__dict__ = state 75 76 def __getstate__(self): 77 retval = dict(self.__dict__) 78 # do only store the fleet id not the Fleet object 79 retval["fleet"] = self.fleet.id 80 81 # store target type and id rather than the object 82 if self.target is not None: 83 retval["target_type"] = self.target.object_name 84 retval["target"] = self.target.id 85 else: 86 retval["target_type"] = None 87 retval["target"] = None 88 89 return retval 90 91 def set_target(self, mission_type, target): 92 """ 93 Set mission and target for this fleet. 94 95 :type mission_type: MissionType 96 :type target: target.Target 97 """ 98 if self.type == mission_type and self.target == target: 99 return 100 if self.type or self.target: 101 debug("%s: change mission assignment from %s:%s to %s:%s" % ( 102 self.fleet, self.type, self.target, mission_type, target)) 103 self.type = mission_type 104 self.target = target 105 106 def clear_target(self): 107 """Clear target and mission for this fleet.""" 108 self.target = None 109 self.type = None 110 111 def has_target(self, mission_type, target): 112 """ 113 Check if fleet has specified mission_type and target. 114 115 :type mission_type: MissionType 116 :type target: target.Target 117 :rtype: bool 118 """ 119 return self.type == mission_type and self.target == target 120 121 def clear_fleet_orders(self): 122 """Clear this fleets orders but do not clear mission and target.""" 123 self.orders = [] 124 125 def _get_fleet_order_from_target(self, mission_type, target): 126 """ 127 Get a fleet order according to mission type and target. 128 129 :type mission_type: MissionType 130 :type target: target.Target 131 :rtype: AIFleetOrder 132 """ 133 fleet_target = TargetFleet(self.fleet.id) 134 return ORDERS_FOR_MISSION[mission_type](fleet_target, target) 135 136 def check_mergers(self, context=""): 137 """ 138 Merge local fleets with same mission into this fleet. 139 140 :param context: Context of the function call for logging purposes 141 :type context: str 142 """ 143 debug("Considering to merge %s", self.__str__()) 144 if self.type not in MERGEABLE_MISSION_TYPES: 145 debug("Mission type does not allow merging.") 146 return 147 148 if not self.target: 149 debug("Mission has no valid target - do not merge.") 150 return 151 152 universe = fo.getUniverse() 153 empire_id = fo.empireID() 154 155 fleet_id = self.fleet.id 156 main_fleet = universe.getFleet(fleet_id) 157 main_fleet_system_id = main_fleet.systemID 158 if main_fleet_system_id == INVALID_ID: 159 debug("Can't merge: fleet in middle of starlane.") 160 return 161 162 # only merge PROTECT_REGION if there is any threat near target 163 if self.type == MissionType.PROTECT_REGION: 164 neighbor_systems = universe.getImmediateNeighbors(self.target.id, empire_id) 165 if not any(MilitaryAI.get_system_local_threat(sys_id) 166 for sys_id in neighbor_systems): 167 debug("Not merging PROTECT_REGION fleet - no threat nearby.") 168 return 169 170 destroyed_list = set(universe.destroyedObjectIDs(empire_id)) 171 aistate = get_aistate() 172 system_status = aistate.systemStatus[main_fleet_system_id] 173 other_fleets_here = [fid for fid in system_status.get('myFleetsAccessible', []) if fid != fleet_id and 174 fid not in destroyed_list and universe.getFleet(fid).ownedBy(empire_id)] 175 if not other_fleets_here: 176 debug("No other fleets here") 177 return 178 179 for fid in other_fleets_here: 180 fleet_mission = aistate.get_fleet_mission(fid) 181 if fleet_mission.type != self.type or fleet_mission.target != self.target: 182 debug("Local candidate %s does not have same mission." % fleet_mission) 183 continue 184 FleetUtilsAI.merge_fleet_a_into_b(fid, fleet_id, context="Order %s of mission %s" % (context, self)) 185 186 def _is_valid_fleet_mission_target(self, mission_type, target): 187 if not target: 188 return False 189 if mission_type == MissionType.EXPLORATION: 190 if isinstance(target, TargetSystem): 191 empire = fo.getEmpire() 192 if not empire.hasExploredSystem(target.id): 193 return True 194 elif mission_type in [MissionType.OUTPOST, MissionType.ORBITAL_OUTPOST]: 195 fleet = self.fleet.get_object() 196 if not fleet.hasOutpostShips: 197 return False 198 if isinstance(target, TargetPlanet): 199 planet = target.get_object() 200 if planet.unowned: 201 return True 202 elif mission_type == MissionType.COLONISATION: 203 fleet = self.fleet.get_object() 204 if not fleet.hasColonyShips: 205 return False 206 if isinstance(target, TargetPlanet): 207 planet = target.get_object() 208 population = planet.initialMeterValue(fo.meterType.population) 209 if planet.unowned or (planet.owner == fleet.owner and population == 0): 210 return True 211 elif mission_type in [MissionType.INVASION, MissionType.ORBITAL_INVASION]: 212 fleet = self.fleet.get_object() 213 if not fleet.hasTroopShips: 214 return False 215 if isinstance(target, TargetPlanet): 216 planet = target.get_object() 217 # TODO remove latter portion of next check in light of invasion retargeting, or else correct logic 218 if not planet.unowned or planet.owner != fleet.owner: 219 return True 220 elif mission_type in [MissionType.MILITARY, MissionType.SECURE, 221 MissionType.ORBITAL_DEFENSE, MissionType.PROTECT_REGION]: 222 if isinstance(target, TargetSystem): 223 return True 224 # TODO: implement other mission types 225 return False 226 227 def clean_invalid_targets(self): 228 """clean invalid AITargets""" 229 if not self._is_valid_fleet_mission_target(self.type, self.target): 230 self.target = None 231 self.type = None 232 233 def _check_abort_mission(self, fleet_order): 234 """ checks if current mission (targeting a planet) should be aborted""" 235 planet_stealthed = False 236 target_is_planet = fleet_order.target and isinstance(fleet_order.target, TargetPlanet) 237 planet = None 238 if target_is_planet: 239 planet = fleet_order.target.get_object() 240 # Check visibility prediction, but if somehow still have current visibility, don't 241 # abort the mission yet 242 if not EspionageAI.colony_detectable_by_empire(planet.id, empire=fo.empireID()): 243 if get_partial_visibility_turn(planet.id) == fo.currentTurn(): 244 debug("EspionageAI predicts planet id %d to be stealthed" % planet.id + 245 ", but somehow have current visibity anyway, so won't trigger mission abort") 246 else: 247 debug("EspionageAI predicts we can no longer detect %s, will abort mission" % fleet_order.target) 248 planet_stealthed = True 249 if target_is_planet and not planet_stealthed: 250 if isinstance(fleet_order, OrderColonize): 251 if (planet.initialMeterValue(fo.meterType.population) == 0 and 252 (planet.ownedBy(fo.empireID()) or planet.unowned)): 253 return False 254 elif isinstance(fleet_order, OrderOutpost): 255 if planet.unowned: 256 return False 257 elif isinstance(fleet_order, OrderInvade): # TODO add substantive abort check 258 return False 259 else: 260 return False 261 262 # canceling fleet orders 263 debug(" %s" % fleet_order) 264 debug("Fleet %d had a target planet that is no longer valid for this mission; aborting." % self.fleet.id) 265 self.clear_fleet_orders() 266 self.clear_target() 267 FleetUtilsAI.split_fleet(self.fleet.id) 268 return True 269 270 def _check_retarget_invasion(self): 271 """checks if an invasion mission should be retargeted""" 272 universe = fo.getUniverse() 273 empire_id = fo.empireID() 274 fleet_id = self.fleet.id 275 fleet = universe.getFleet(fleet_id) 276 if fleet.systemID == INVALID_ID: 277 # next_loc = fleet.nextSystemID 278 return # TODO: still check 279 system = universe.getSystem(fleet.systemID) 280 if not system: 281 return 282 orders = self.orders 283 last_sys_target = INVALID_ID 284 if orders: 285 last_sys_target = orders[-1].target.id 286 if last_sys_target == fleet.systemID: 287 return # TODO: check for best local target 288 open_targets = [] 289 already_targeted = InvasionAI.get_invasion_targeted_planet_ids(system.planetIDs, MissionType.INVASION) 290 aistate = get_aistate() 291 for pid in system.planetIDs: 292 if pid in already_targeted or (pid in aistate.qualifyingTroopBaseTargets): 293 continue 294 planet = universe.getPlanet(pid) 295 if planet.unowned or (planet.owner == empire_id): 296 continue 297 if (planet.initialMeterValue(fo.meterType.shield)) <= 0: 298 open_targets.append(pid) 299 if not open_targets: 300 return 301 troops_in_fleet = FleetUtilsAI.count_troops_in_fleet(fleet_id) 302 target_id = INVALID_ID 303 best_score = -1 304 target_troops = 0 305 # 306 for pid, rating in InvasionAI.assign_invasion_values(open_targets).items(): 307 p_score, p_troops = rating 308 if p_score > best_score: 309 if p_troops >= troops_in_fleet: 310 continue 311 best_score = p_score 312 target_id = pid 313 target_troops = p_troops 314 if target_id == INVALID_ID: 315 return 316 317 debug("\t Splitting and retargetting fleet %d" % fleet_id) 318 new_fleets = FleetUtilsAI.split_fleet(fleet_id) 319 self.clear_target() # TODO: clear from foAIstate 320 self.clear_fleet_orders() 321 troops_needed = max(0, target_troops - FleetUtilsAI.count_troops_in_fleet(fleet_id)) 322 min_stats = {'rating': 0, 'troopCapacity': troops_needed} 323 target_stats = {'rating': 10, 'troopCapacity': troops_needed} 324 found_fleets = [] 325 # TODO check if next statement does not mutate any global states and can be removed 326 327 _ = FleetUtilsAI.get_fleets_for_mission(target_stats, min_stats, {}, starting_system=fleet.systemID, # noqa: F841 328 fleet_pool_set=set(new_fleets), fleet_list=found_fleets) 329 for fid in found_fleets: 330 FleetUtilsAI.merge_fleet_a_into_b(fid, fleet_id) 331 target = TargetPlanet(target_id) 332 self.set_target(MissionType.INVASION, target) 333 self.generate_fleet_orders() 334 335 def need_to_pause_movement(self, last_move_target_id, new_move_order): 336 """ 337 When a fleet has consecutive move orders, assesses whether something about the interim destination warrants 338 forcing a stop (such as a military fleet choosing to engage with an enemy fleet about to enter the same system, 339 or it may provide a good vantage point to view current status of next system in path). Assessments about whether 340 the new destination is suitable to move to are (currently) separately made by OrderMove.can_issue_order() 341 :param last_move_target_id: 342 :type last_move_target_id: int 343 :param new_move_order: 344 :type new_move_order: OrderMove 345 :rtype: bool 346 """ 347 fleet = self.fleet.get_object() 348 # don't try skipping over more than one System 349 if fleet.nextSystemID != last_move_target_id: 350 return True 351 universe = fo.getUniverse() 352 current_dest_system = universe.getSystem(fleet.nextSystemID) 353 if not current_dest_system: 354 # shouldn't really happen, but just to be safe 355 return True 356 distance_to_next_system = ((fleet.x - current_dest_system.x)**2 + (fleet.y - current_dest_system.y)**2)**0.5 357 surplus_travel_distance = fleet.speed - distance_to_next_system 358 # if need more than one turn to reach current destination, then don't add another jump yet 359 if surplus_travel_distance < 0: 360 return True 361 # TODO: add assessments for other situations we'd prefer to pause, such as cited above re military fleets, and 362 # for situations where high value fleets like colony fleets might deem it safest to stop and look around 363 # before proceeding 364 return False 365 366 def issue_fleet_orders(self): 367 """issues AIFleetOrders which can be issued in system and moves to next one if is possible""" 368 # TODO: priority 369 order_completed = True 370 371 debug("\nChecking orders for fleet %s (on turn %d), with mission type %s and target %s", 372 self.fleet.get_object(), fo.currentTurn(), self.type or 'No mission', self.target or 'No Target') 373 if MissionType.INVASION == self.type: 374 self._check_retarget_invasion() 375 just_issued_move_order = False 376 last_move_target_id = INVALID_ID 377 # Note: the following abort check somewhat assumes only one major mission type 378 for fleet_order in self.orders: 379 if (isinstance(fleet_order, (OrderColonize, OrderOutpost, OrderInvade)) and 380 self._check_abort_mission(fleet_order)): 381 return 382 aistate = get_aistate() 383 for fleet_order in self.orders: 384 if just_issued_move_order and self.fleet.get_object().systemID != last_move_target_id: 385 # having just issued a move order, we will normally stop issuing orders this turn, except that if there 386 # are consecutive move orders we will consider moving through the first destination rather than stopping 387 # Without the below noinspection directive, PyCharm is concerned about the 2nd part of the test 388 # noinspection PyTypeChecker 389 if (not isinstance(fleet_order, OrderMove) or 390 self.need_to_pause_movement(last_move_target_id, fleet_order)): 391 break 392 debug("Checking order: %s" % fleet_order) 393 self.check_mergers(context=str(fleet_order)) 394 if fleet_order.can_issue_order(verbose=False): 395 # only move if all other orders completed 396 if isinstance(fleet_order, OrderMove) and order_completed: 397 debug("Issuing fleet order %s" % fleet_order) 398 fleet_order.issue_order() 399 just_issued_move_order = True 400 last_move_target_id = fleet_order.target.id 401 elif not isinstance(fleet_order, OrderMove): 402 debug("Issuing fleet order %s" % fleet_order) 403 fleet_order.issue_order() 404 else: 405 debug("NOT issuing (even though can_issue) fleet order %s" % fleet_order) 406 status_words = tuple(["not", ""][_s] for _s in [fleet_order.order_issued, fleet_order.executed]) 407 debug("Order %s issued and %s fully executed." % status_words) 408 if not fleet_order.executed: 409 order_completed = False 410 else: # check that we're not held up by a Big Monster 411 if fleet_order.order_issued: 412 # A previously issued order that wasn't instantly executed must have had cirumstances change so that 413 # the order can't currently be reissued (or perhaps simply a savegame has been reloaded on the same 414 # turn the order was issued). 415 if not fleet_order.executed: 416 order_completed = False 417 # Go on to the next order. 418 continue 419 debug("CAN'T issue fleet order %s because:" % fleet_order) 420 fleet_order.can_issue_order(verbose=True) 421 if isinstance(fleet_order, OrderMove): 422 this_system_id = fleet_order.target.id 423 this_status = aistate.systemStatus.setdefault(this_system_id, {}) 424 threat_threshold = fo.currentTurn() * MilitaryAI.cur_best_mil_ship_rating() / 4.0 425 if this_status.get('monsterThreat', 0) > threat_threshold: 426 # if this move order is not this mil fleet's final destination, and blocked by Big Monster, 427 # release and hope for more effective reassignment 428 if (self.type not in (MissionType.MILITARY, MissionType.SECURE) or 429 fleet_order != self.orders[-1]): 430 debug("Aborting mission due to being blocked by Big Monster at system %d, threat %d" % ( 431 this_system_id, aistate.systemStatus[this_system_id]['monsterThreat'])) 432 debug("Full set of orders were:") 433 for this_order in self.orders: 434 debug(" - %s" % this_order) 435 self.clear_fleet_orders() 436 self.clear_target() 437 return 438 break # do not order the next order until this one is finished. 439 else: # went through entire order list 440 if order_completed: 441 debug("Final order is completed") 442 orders = self.orders 443 last_order = orders[-1] if orders else None 444 universe = fo.getUniverse() 445 446 if last_order and isinstance(last_order, OrderColonize): 447 planet = universe.getPlanet(last_order.target.id) 448 sys_partial_vis_turn = get_partial_visibility_turn(planet.systemID) 449 planet_partial_vis_turn = get_partial_visibility_turn(planet.id) 450 if (planet_partial_vis_turn == sys_partial_vis_turn and 451 not planet.initialMeterValue(fo.meterType.population)): 452 warning("Fleet %s has tentatively completed its " 453 "colonize mission but will wait to confirm population.", self.fleet) 454 debug(" Order details are %s" % last_order) 455 debug(" Order is valid: %s; issued: %s; executed: %s" % ( 456 last_order.is_valid(), last_order.order_issued, last_order.executed)) 457 if not last_order.is_valid(): 458 source_target = last_order.fleet 459 target_target = last_order.target 460 debug(" source target validity: %s; target target validity: %s " % ( 461 bool(source_target), bool(target_target))) 462 return # colonize order must not have completed yet 463 clear_all = True 464 last_sys_target = INVALID_ID 465 if last_order and isinstance(last_order, OrderMilitary): 466 last_sys_target = last_order.target.id 467 # not doing this until decide a way to release from a SECURE mission 468 # if (MissionType.SECURE == self.type) or 469 secure_targets = set(AIstate.colonyTargetedSystemIDs + 470 AIstate.outpostTargetedSystemIDs + 471 AIstate.invasionTargetedSystemIDs) 472 if last_sys_target in secure_targets: # consider a secure mission 473 if last_sys_target in AIstate.colonyTargetedSystemIDs: 474 secure_type = "Colony" 475 elif last_sys_target in AIstate.outpostTargetedSystemIDs: 476 secure_type = "Outpost" 477 elif last_sys_target in AIstate.invasionTargetedSystemIDs: 478 secure_type = "Invasion" 479 else: 480 secure_type = "Unidentified" 481 debug("Fleet %d has completed initial stage of its mission " 482 "to secure system %d (targeted for %s), " 483 "may release a portion of ships" % (self.fleet.id, last_sys_target, secure_type)) 484 clear_all = False 485 486 # for PROTECT_REGION missions, only release fleet if no more threat 487 if self.type == MissionType.PROTECT_REGION: 488 # use military logic code below to determine if can release 489 # any or even all of the ships. 490 clear_all = False 491 last_sys_target = self.target.id 492 debug("Check if PROTECT_REGION mission with target %d is finished.", last_sys_target) 493 494 fleet_id = self.fleet.id 495 if clear_all: 496 if orders: 497 debug("Fleet %d has completed its mission; clearing all orders and targets." % self.fleet.id) 498 debug("Full set of orders were:") 499 for this_order in orders: 500 debug("\t\t %s" % this_order) 501 self.clear_fleet_orders() 502 self.clear_target() 503 if aistate.get_fleet_role(fleet_id) in (MissionType.MILITARY, MissionType.SECURE): 504 allocations = MilitaryAI.get_military_fleets(mil_fleets_ids=[fleet_id], 505 try_reset=False, 506 thisround="Fleet %d Reassignment" % fleet_id) 507 if allocations: 508 MilitaryAI.assign_military_fleets_to_systems(use_fleet_id_list=[fleet_id], 509 allocations=allocations) 510 else: # no orders 511 debug("No Current Orders") 512 else: 513 potential_threat = CombatRatingsAI.combine_ratings( 514 MilitaryAI.get_system_local_threat(last_sys_target), 515 MilitaryAI.get_system_neighbor_threat(last_sys_target) 516 ) 517 threat_present = potential_threat > 0 518 debug("Fleet threat present? %s", threat_present) 519 target_system = universe.getSystem(last_sys_target) 520 if not threat_present and target_system: 521 for pid in target_system.planetIDs: 522 planet = universe.getPlanet(pid) 523 if (planet and 524 planet.owner != fo.empireID() and 525 planet.currentMeterValue(fo.meterType.maxDefense) > 0): 526 debug("Found local planetary threat: %s", planet) 527 threat_present = True 528 break 529 if not threat_present: 530 debug("No current threat in target system; releasing a portion of ships.") 531 # at least first stage of current task is done; 532 # release extra ships for potential other deployments 533 new_fleets = FleetUtilsAI.split_fleet(self.fleet.id) 534 if self.type == MissionType.PROTECT_REGION: 535 self.clear_fleet_orders() 536 self.clear_target() 537 new_fleets.append(self.fleet.id) 538 else: 539 debug("Threat remains in target system; Considering to release some ships.") 540 new_fleets = [] 541 fleet_portion_to_remain = self._portion_of_fleet_needed_here() 542 if fleet_portion_to_remain >= 1: 543 debug("Can not release fleet yet due to large threat.") 544 elif fleet_portion_to_remain > 0: 545 debug("Not all ships are needed here - considering releasing a few") 546 # TODO: Rate against specific enemy threat cause 547 fleet_remaining_rating = CombatRatingsAI.get_fleet_rating(fleet_id) 548 fleet_min_rating = fleet_portion_to_remain * fleet_remaining_rating 549 debug("Starting rating: %.1f, Target rating: %.1f", 550 fleet_remaining_rating, fleet_min_rating) 551 allowance = CombatRatingsAI.rating_needed(fleet_remaining_rating, fleet_min_rating) 552 debug("May release ships with total rating of %.1f", allowance) 553 ship_ids = list(self.fleet.get_object().shipIDs) 554 for ship_id in ship_ids: 555 ship_rating = CombatRatingsAI.get_ship_rating(ship_id) 556 debug("Considering to release ship %d with rating %.1f", ship_id, ship_rating) 557 if ship_rating > allowance: 558 debug("Remaining rating insufficient. Not released.") 559 continue 560 debug("Splitting from fleet.") 561 new_fleet_id = FleetUtilsAI.split_ship_from_fleet(fleet_id, ship_id) 562 if assertion_fails(new_fleet_id and new_fleet_id != INVALID_ID): 563 break 564 new_fleets.append(new_fleet_id) 565 fleet_remaining_rating = CombatRatingsAI.rating_difference( 566 fleet_remaining_rating, ship_rating) 567 allowance = CombatRatingsAI.rating_difference( 568 fleet_remaining_rating, fleet_min_rating) 569 debug("Remaining fleet rating: %.1f - Allowance: %.1f", 570 fleet_remaining_rating, allowance) 571 if new_fleets: 572 aistate.get_fleet_role(fleet_id, force_new=True) 573 aistate.update_fleet_rating(fleet_id) 574 aistate.ensure_have_fleet_missions(new_fleets) 575 else: 576 debug("Planetary defenses are deemed sufficient. Release fleet.") 577 new_fleets = FleetUtilsAI.split_fleet(self.fleet.id) 578 579 new_military_fleets = [] 580 for fleet_id in new_fleets: 581 if aistate.get_fleet_role(fleet_id) in COMBAT_MISSION_TYPES: 582 new_military_fleets.append(fleet_id) 583 allocations = [] 584 if new_military_fleets: 585 allocations = MilitaryAI.get_military_fleets( 586 mil_fleets_ids=new_military_fleets, 587 try_reset=False, 588 thisround="Fleet Reassignment %s" % new_military_fleets 589 ) 590 if allocations: 591 MilitaryAI.assign_military_fleets_to_systems(use_fleet_id_list=new_military_fleets, 592 allocations=allocations) 593 594 def _portion_of_fleet_needed_here(self): 595 """Calculate the portion of the fleet needed in target system considering enemy forces.""" 596 # TODO check rating against planets 597 if assertion_fails(self.type in COMBAT_MISSION_TYPES, msg=str(self)): 598 return 0 599 if assertion_fails(self.target and self.target.id != INVALID_ID, msg=str(self)): 600 return 0 601 system_id = self.target.id 602 aistate = get_aistate() 603 local_defenses = MilitaryAI.get_my_defense_rating_in_system(system_id) 604 potential_threat = CombatRatingsAI.combine_ratings( 605 MilitaryAI.get_system_local_threat(system_id), 606 MilitaryAI.get_system_neighbor_threat(system_id) 607 ) 608 universe = fo.getUniverse() 609 system = universe.getSystem(system_id) 610 611 # tally planetary defenses 612 total_defense = total_shields = 0 613 for planet_id in system.planetIDs: 614 planet = universe.getPlanet(planet_id) 615 total_defense += planet.currentMeterValue(fo.meterType.defense) 616 total_shields += planet.currentMeterValue(fo.meterType.shield) 617 planetary_ratings = total_defense * (total_shields + total_defense) 618 potential_threat += planetary_ratings # TODO: rewrite to return min rating vs planets as well 619 620 # consider safety factor just once here rather than everywhere below 621 safety_factor = aistate.character.military_safety_factor() 622 potential_threat *= safety_factor 623 624 # TODO: Rate against specific threat here 625 fleet_rating = CombatRatingsAI.get_fleet_rating(self.fleet.id) 626 return CombatRatingsAI.rating_needed(potential_threat, local_defenses) / float(max(fleet_rating, 1.)) 627 628 def generate_fleet_orders(self): 629 """generates AIFleetOrders from fleets targets to accomplish""" 630 universe = fo.getUniverse() 631 fleet_id = self.fleet.id 632 fleet = universe.getFleet(fleet_id) 633 if (not fleet) or fleet.empty or (fleet_id in universe.destroyedObjectIDs(fo.empireID())): 634 # fleet was probably merged into another or was destroyed 635 get_aistate().delete_fleet_info(fleet_id) 636 return 637 638 # TODO: priority 639 self.clear_fleet_orders() 640 system_id = fleet.systemID 641 start_sys_id = [fleet.nextSystemID, system_id][system_id >= 0] 642 # if fleet doesn't have any mission, 643 # then repair if needed or resupply if is current location not in supplyable system 644 empire = fo.getEmpire() 645 fleet_supplyable_system_ids = empire.fleetSupplyableSystemIDs 646 # if (not self.hasAnyAIMissionTypes()): 647 if not self.target and (system_id not in set(AIstate.colonyTargetedSystemIDs + 648 AIstate.outpostTargetedSystemIDs + 649 AIstate.invasionTargetedSystemIDs)): 650 if self._need_repair(): 651 repair_fleet_order = MoveUtilsAI.get_repair_fleet_order(self.fleet, start_sys_id) 652 if repair_fleet_order and repair_fleet_order.is_valid(): 653 self.orders.append(repair_fleet_order) 654 cur_fighter_capacity, max_fighter_capacity = FleetUtilsAI.get_fighter_capacity_of_fleet(fleet_id) 655 if (fleet.fuel < fleet.maxFuel or cur_fighter_capacity < max_fighter_capacity 656 and self.get_location_target().id not in fleet_supplyable_system_ids): 657 resupply_fleet_order = MoveUtilsAI.get_resupply_fleet_order(self.fleet, self.get_location_target()) 658 if resupply_fleet_order.is_valid(): 659 self.orders.append(resupply_fleet_order) 660 return # no targets 661 662 if self.target: 663 # for some targets fleet has to visit systems and therefore fleet visit them 664 665 system_to_visit = (self.target.get_system() if not self.type == MissionType.PROTECT_REGION 666 else TargetSystem(self._get_target_for_protection_mission())) 667 if not system_to_visit: 668 return 669 orders_to_visit_systems = MoveUtilsAI.create_move_orders_to_system(self.fleet, system_to_visit) 670 # TODO: if fleet doesn't have enough fuel to get to final target, consider resetting Mission 671 for fleet_order in orders_to_visit_systems: 672 self.orders.append(fleet_order) 673 674 # also generate appropriate final orders 675 fleet_order = self._get_fleet_order_from_target(self.type, 676 self.target if not self.type == MissionType.PROTECT_REGION 677 else system_to_visit) 678 self.orders.append(fleet_order) 679 680 def _need_repair(self, repair_limit=0.70): 681 """Check if fleet needs to be repaired. 682 683 If the fleet is already at a system where it can be repaired, stay there until fully repaired. 684 Otherwise, repair if fleet health is below specified *repair_limit*. 685 For military fleets, there is a special evaluation called, cf. *MilitaryAI.avail_mil_needing_repair()* 686 687 :param repair_limit: percentage of health below which the fleet is sent to repair 688 :type repair_limit: float 689 :return: True if fleet needs repair 690 :rtype: bool 691 """ 692 # TODO: More complex evaluation if fleet needs repair (consider self-repair, distance, threat, mission...) 693 fleet_id = self.fleet.id 694 # if we are already at a system where we can repair, make sure we use it... 695 system = self.fleet.get_system() 696 # TODO starlane obstruction is not considered in the next call 697 nearest_dock = MoveUtilsAI.get_best_drydock_system_id(system.id, fleet_id) 698 if nearest_dock == system.id: 699 repair_limit = 0.99 700 # if combat fleet, use military repair check 701 if get_aistate().get_fleet_role(fleet_id) in COMBAT_MISSION_TYPES: 702 return fleet_id in MilitaryAI.avail_mil_needing_repair([fleet_id], on_mission=bool(self.orders), 703 repair_limit=repair_limit)[0] 704 # TODO: Allow to split fleet to send only damaged ships to repair 705 ships_cur_health, ships_max_health = FleetUtilsAI.get_current_and_max_structure(fleet_id) 706 return ships_cur_health < repair_limit * ships_max_health 707 708 def get_location_target(self): 709 """system AITarget where fleet is or will be""" 710 # TODO add parameter turn 711 fleet = fo.getUniverse().getFleet(self.fleet.id) 712 system_id = fleet.systemID 713 if system_id >= 0: 714 return TargetSystem(system_id) 715 else: # in starlane, so return next system 716 return TargetSystem(fleet.nextSystemID) 717 718 def __eq__(self, other): 719 return isinstance(other, self.__class__) and self.fleet == other.target 720 721 def __hash__(self): 722 return hash(self.fleet) 723 724 def __str__(self): 725 fleet = self.fleet.get_object() 726 727 fleet_id = self.fleet.id 728 return "%-25s [%-11s] ships: %2d; total rating: %4d; target: %s" % (fleet, 729 "NONE" if self.type is None else self.type, 730 (fleet and len(fleet.shipIDs)) or 0, 731 CombatRatingsAI.get_fleet_rating(fleet_id), 732 self.target or 'no target') 733 734 def _get_target_for_protection_mission(self): 735 """Get a target for a PROTECT_REGION mission. 736 737 1) If primary target (system target of this mission) is under attack, move to primary target. 738 2) If neighbors of primary target have local enemy forces weaker than this fleet, may move to attack 739 3) If no neighboring fleets or strongest enemy force is too strong, move to defend primary target 740 """ 741 # TODO: Also check fleet rating vs planets in decision making below not only vs fleets 742 universe = fo.getUniverse() 743 primary_objective = self.target.id 744 # TODO: Rate against specific threats 745 fleet_rating = CombatRatingsAI.get_fleet_rating(self.fleet.id) 746 debug("%s finding target for protection mission (primary target %s). Fleet Rating: %.1f", 747 self.fleet, self.target, fleet_rating) 748 immediate_threat = MilitaryAI.get_system_local_threat(primary_objective) 749 if immediate_threat: 750 debug(" Immediate threat! Moving to primary mission target") 751 return primary_objective 752 else: 753 debug(" No immediate threats.") 754 # Try to eliminate neighbouring fleets 755 neighbors = universe.getImmediateNeighbors(primary_objective, fo.empireID()) 756 threat_list = sorted(map( 757 lambda x: (MilitaryAI.get_system_local_threat(x), x), 758 neighbors 759 ), reverse=True) 760 761 if not threat_list: 762 debug(" No neighbors (?!). Moving to primary mission target") 763 return primary_objective 764 else: 765 debug(" Neighboring threats:") 766 for threat, sys_id in threat_list: 767 debug(" %s - %.1f", TargetSystem(sys_id), threat) 768 top_threat, candidate_system = threat_list[0] 769 if not top_threat: 770 # TODO: Move into second ring but needs more careful evaluation 771 # For now, consider staying at the current location if enemy 772 # owns a planet here which we can bombard. 773 current_system_id = self.fleet.get_current_system_id() 774 if current_system_id in neighbors: 775 system = universe.getSystem(current_system_id) 776 if assertion_fails(system is not None): 777 return primary_objective 778 empire_id = fo.empireID() 779 for planet_id in system.planetIDs: 780 planet = universe.getPlanet(planet_id) 781 if (planet and 782 not planet.ownedBy(empire_id) and 783 not planet.unowned): 784 debug("Currently no neighboring threats. " 785 "Staying for bombardment of planet %s", planet) 786 return current_system_id 787 788 # TODO consider attacking neighboring, non-military fleets 789 # - needs more careful evaluation against neighboring threats 790 # empire_id = fo.empireID() 791 # for sys_id in neighbors: 792 # system = universe.getSystem(sys_id) 793 # if assertion_fails(system is not None): 794 # continue 795 # local_fleets = system.fleetIDs 796 # for fleet_id in local_fleets: 797 # fleet = universe.getFleet(fleet_id) 798 # if not fleet or fleet.ownedBy(empire_id): 799 # continue 800 # return sys_id 801 802 debug("No neighboring threats. Moving to primary mission target") 803 return primary_objective 804 805 # TODO rate against threat in target system 806 # TODO only engage if can reach in 1 turn or leaves sufficient defense behind 807 safety_factor = get_aistate().character.military_safety_factor() 808 if fleet_rating < safety_factor*top_threat: 809 debug(" Neighboring threat is too powerful. Moving to primary mission target") 810 return primary_objective # do not engage! 811 812 debug(" Engaging neighboring threat: %s", TargetSystem(candidate_system)) 813 return candidate_system 814