1import math 2from logging import error, warning, debug 3 4import freeOrionAIInterface as fo # pylint: disable=import-error 5 6import AIDependencies 7import CombatRatingsAI 8import MoveUtilsAI 9from AIDependencies import INVALID_ID 10from aistate_interface import get_aistate 11from freeorion_tools import assertion_fails 12from EnumsAI import MissionType, ShipRoleType 13from ShipDesignAI import get_ship_part 14from target import TargetPlanet, TargetFleet, TargetSystem 15 16 17def stats_meet_reqs(stats, requirements): 18 """Check if (fleet) stats meet requirements. 19 20 :param stats: Stats (of fleet) 21 :type stats: dict 22 :param requirements: Requirements 23 :type requirements: dict 24 :return: True if requirements are met. 25 :rtype: bool 26 """ 27 for key in requirements: 28 if key not in stats: # skip requirements not related to stats 29 if key != "target_system": # expected not to be in stats 30 warning("Requirement %s not in stats", key) 31 continue 32 if stats.get(key, 0) < requirements[key]: 33 return False 34 return True 35 36 37def count_troops_in_fleet(fleet_id): 38 """Get the number of troops in the fleet. 39 40 :param fleet_id: fleet to be queried 41 :type fleet_id: int 42 :return: total troopCapacity of the fleet 43 """ 44 universe = fo.getUniverse() 45 fleet = universe.getFleet(fleet_id) 46 if not fleet: 47 return 0 48 fleet_troop_capacity = 0 49 for ship_id in fleet.shipIDs: 50 ship = universe.getShip(ship_id) 51 if ship: 52 fleet_troop_capacity += ship.troopCapacity 53 return fleet_troop_capacity 54 55 56def get_targeted_planet_ids(planet_ids, mission_type): 57 """Find the planets that are targets of the specified mission type. 58 59 :param planet_ids: planets to be queried 60 :type planet_ids: list[int] 61 :param mission_type: 62 :type mission_type: MissionType 63 :return: Subset of *planet_ids* targeted by *mission_type* 64 :rtype: list[int] 65 """ 66 selected_fleet_missions = get_aistate().get_fleet_missions_with_any_mission_types([mission_type]) 67 targeted_planets = [] 68 for planet_id in planet_ids: 69 # add planets that are target of a mission 70 for fleet_mission in selected_fleet_missions: 71 ai_target = TargetPlanet(planet_id) 72 if fleet_mission.has_target(mission_type, ai_target): 73 targeted_planets.append(planet_id) 74 return targeted_planets 75 76 77# TODO: Avoid mutable arguments and use return values instead 78# TODO: Use Dijkstra's algorithm instead of BFS to consider starlane length 79def get_fleets_for_mission(target_stats, min_stats, cur_stats, starting_system, 80 fleet_pool_set, fleet_list, species="", ensure_return=False): 81 """Get fleets for a mission. 82 83 Implements breadth-first search through systems starting at the **starting_sytem**. 84 In each system, local fleets are checked if they are in the allowed **fleet_pool_set** and suitable for the mission. 85 If so, they are added to the **fleet_list** and **cur_stats** is updated with the currently selected fleet summary. 86 The search continues until the requirements defined in **target_stats** are met or there are no more systems/fleets. 87 In that case, if the **min_stats** are covered, the **fleet_list** is returned anyway. 88 Otherwise, an empty list is returned by the function, in which case the caller can make an evaluation of 89 an emergency use of the found fleets in fleet_list; if not to be used they should be added back to the main pool. 90 91 :param target_stats: stats the fleet should ideally meet 92 :type target_stats: dict 93 :param min_stats: minimum stats the final fleet must meet to be accepted 94 :type min_stats: dict 95 :param cur_stats: (**mutated**) stat summary of selected fleets 96 :type cur_stats: dict 97 :param starting_system: system_id where breadth-first-search is centered 98 :type starting_system: int 99 :param fleet_pool_set: (**mutated**) fleets allowed to be selected. Split fleed_ids are added, used ones removed. 100 :type: fleet_pool_set: set[int] 101 :param fleet_list: (**mutated**) fleets that are selected for the mission. Gets filled during the call. 102 :type fleet_list: list[int] 103 :param species: species for colonization mission 104 :type species: str 105 :param bool ensure_return: If true, fleet must have sufficient fuel to return into supply after mission 106 :return: List of selected fleet_ids or empty list if couldn't meet minimum requirements. 107 :rtype: list[int] 108 """ 109 universe = fo.getUniverse() 110 colonization_roles = (ShipRoleType.CIVILIAN_COLONISATION, ShipRoleType.BASE_COLONISATION) 111 systems_enqueued = [starting_system] 112 systems_visited = [] 113 # loop over systems in a breadth-first-search trying to find nearby suitable ships in fleet_pool_set 114 aistate = get_aistate() 115 while systems_enqueued and fleet_pool_set: 116 this_system_id = systems_enqueued.pop(0) 117 this_system_obj = TargetSystem(this_system_id) 118 systems_visited.append(this_system_id) 119 accessible_fleets = aistate.systemStatus.get(this_system_id, {}).get('myFleetsAccessible', []) 120 fleets_here = [fid for fid in accessible_fleets if fid in fleet_pool_set] 121 # loop over all fleets in the system, split them if possible and select suitable ships 122 while fleets_here: 123 fleet_id = fleets_here.pop(0) 124 fleet = universe.getFleet(fleet_id) 125 if not fleet: # TODO should be checked before passed to the function 126 fleet_pool_set.remove(fleet_id) 127 continue 128 # try splitting fleet 129 if fleet.numShips > 1: 130 debug("Splitting candidate fleet to get ships for mission.") 131 new_fleets = split_fleet(fleet_id) 132 fleet_pool_set.update(new_fleets) 133 fleets_here.extend(new_fleets) 134 135 if ('target_system' in target_stats and 136 not MoveUtilsAI.can_travel_to_system(fleet_id, this_system_obj, 137 target_stats['target_system'], 138 ensure_return=ensure_return)): 139 continue 140 141 # check species for colonization missions 142 if species: 143 for ship_id in fleet.shipIDs: 144 ship = universe.getShip(ship_id) 145 if (ship and aistate.get_ship_role(ship.design.id) in colonization_roles and 146 species == ship.speciesName): 147 break 148 else: # no suitable species found 149 continue 150 # check troop capacity for invasion missions 151 troop_capacity = 0 152 if 'troopCapacity' in target_stats: 153 troop_capacity = count_troops_in_fleet(fleet_id) 154 if troop_capacity <= 0: 155 continue 156 157 # check if we need additional rating vs planets 158 this_rating_vs_planets = 0 159 if 'ratingVsPlanets' in target_stats: 160 this_rating_vs_planets = aistate.get_rating(fleet_id, against_planets=True) 161 if this_rating_vs_planets <= 0 and cur_stats.get('rating', 0) >= target_stats.get('rating', 0): 162 # we already have enough general rating, so do not add any more warships useless against planets 163 continue 164 165 # all checks passed, add ship to selected fleets and update the stats 166 try: 167 fleet_pool_set.remove(fleet_id) 168 except KeyError: 169 error("After having split a fleet, the original fleet apparently no longer exists.", exc_info=True) 170 continue 171 fleet_list.append(fleet_id) 172 173 this_rating = aistate.get_rating(fleet_id) 174 cur_stats['rating'] = CombatRatingsAI.combine_ratings(cur_stats.get('rating', 0), this_rating) 175 if 'ratingVsPlanets' in target_stats: 176 cur_stats['ratingVsPlanets'] = CombatRatingsAI.combine_ratings(cur_stats.get('ratingVsPlanets', 0), 177 this_rating_vs_planets) 178 if 'troopCapacity' in target_stats: 179 cur_stats['troopCapacity'] = cur_stats.get('troopCapacity', 0) + troop_capacity 180 # if we already meet the requirements, we can stop looking for more ships 181 if (sum(len(universe.getFleet(fid).shipIDs) for fid in fleet_list) >= 1) \ 182 and stats_meet_reqs(cur_stats, target_stats): 183 return fleet_list 184 185 # finished system without meeting requirements. Add neighboring systems to search queue. 186 for neighbor_id in universe.getImmediateNeighbors(this_system_id, fo.empireID()): 187 if all(( 188 neighbor_id not in systems_visited, 189 neighbor_id not in systems_enqueued, 190 neighbor_id in aistate.exploredSystemIDs 191 )): 192 systems_enqueued.append(neighbor_id) 193 # we ran out of systems or fleets to check but did not meet requirements yet. 194 if stats_meet_reqs(cur_stats, min_stats) and any(universe.getFleet(fid).shipIDs for fid in fleet_list): 195 return fleet_list 196 else: 197 return [] 198 199 200def split_fleet(fleet_id): 201 """Split a fleet into its ships. 202 203 :param fleet_id: fleet to be split. 204 :type fleet_id: int 205 :return: New fleets. Empty if couldn't split. 206 :rtype: list[int] 207 """ 208 universe = fo.getUniverse() 209 empire_id = fo.empireID() 210 fleet = universe.getFleet(fleet_id) 211 new_fleets = [] 212 213 if fleet is None: 214 return [] 215 if not fleet.ownedBy(empire_id): 216 return [] 217 218 if len(list(fleet.shipIDs)) <= 1: # fleet with only one ship cannot be split 219 return [] 220 ship_ids = list(fleet.shipIDs) 221 aistate = get_aistate() 222 for ship_id in ship_ids[1:]: 223 new_fleet_id = split_ship_from_fleet(fleet_id, ship_id) 224 new_fleets.append(new_fleet_id) 225 226 aistate.get_fleet_role(fleet_id, force_new=True) 227 aistate.update_fleet_rating(fleet_id) 228 if new_fleets: 229 aistate.ensure_have_fleet_missions(new_fleets) 230 231 return new_fleets 232 233 234def split_ship_from_fleet(fleet_id, ship_id): 235 universe = fo.getUniverse() 236 fleet = universe.getFleet(fleet_id) 237 if assertion_fails(fleet is not None): 238 return 239 240 if assertion_fails(ship_id in fleet.shipIDs): 241 return 242 243 if assertion_fails(fleet.numShips > 1, "Can't split last ship from fleet"): 244 return 245 246 new_fleet_id = fo.issueNewFleetOrder("Fleet %4d" % ship_id, ship_id) 247 if new_fleet_id: 248 aistate = get_aistate() 249 new_fleet = universe.getFleet(new_fleet_id) 250 if not new_fleet: 251 warning("Newly split fleet %d not available from universe" % new_fleet_id) 252 debug("Successfully split ship %d from fleet %d into new fleet %d", 253 ship_id, fleet_id, new_fleet_id) 254 fo.issueRenameOrder(new_fleet_id, "Fleet %4d" % new_fleet_id) # to ease review of debugging logs 255 fo.issueAggressionOrder(new_fleet_id, True) 256 aistate.update_fleet_rating(new_fleet_id) 257 aistate.newlySplitFleets[new_fleet_id] = True 258 # register the new fleets so AI logic is aware of them 259 sys_status = aistate.systemStatus.setdefault(fleet.systemID, {}) 260 sys_status['myfleets'].append(new_fleet_id) 261 sys_status['myFleetsAccessible'].append(new_fleet_id) 262 else: 263 if fleet.systemID == INVALID_ID: 264 warning("Tried to split ship id (%d) from fleet %d when fleet is in starlane" % ( 265 ship_id, fleet_id)) 266 else: 267 warning("Got no fleet ID back after trying to split ship id (%d) from fleet %d" % ( 268 ship_id, fleet_id)) 269 return new_fleet_id 270 271 272def merge_fleet_a_into_b(fleet_a_id, fleet_b_id, leave_rating=0, need_rating=0, context=""): 273 debug("Merging fleet %s into %s", TargetFleet(fleet_a_id), TargetFleet(fleet_b_id)) 274 universe = fo.getUniverse() 275 fleet_a = universe.getFleet(fleet_a_id) 276 fleet_b = universe.getFleet(fleet_b_id) 277 if not fleet_a or not fleet_b: 278 return 0 279 system_id = fleet_a.systemID 280 if fleet_b.systemID != system_id: 281 return 0 282 283 # TODO: Should this rate against specific enemy? 284 remaining_rating = CombatRatingsAI.get_fleet_rating(fleet_a_id) 285 transferred_rating = 0 286 for ship_id in fleet_a.shipIDs: 287 this_ship = universe.getShip(ship_id) 288 if not this_ship: 289 continue 290 this_rating = CombatRatingsAI.ShipCombatStats(ship_id).get_rating() 291 remaining_rating = CombatRatingsAI.rating_needed(remaining_rating, this_rating) 292 if remaining_rating < leave_rating: # merging this would leave old fleet under minimum rating, try other ships. 293 continue 294 transferred = fo.issueFleetTransferOrder(ship_id, fleet_b_id) 295 if transferred: 296 transferred_rating = CombatRatingsAI.combine_ratings(transferred_rating, this_rating) 297 else: 298 debug(" *** transfer of ship %4d, formerly of fleet %4d, into fleet %4d failed; %s" % ( 299 ship_id, fleet_a_id, fleet_b_id, (" context is %s" % context) if context else "")) 300 if need_rating != 0 and need_rating <= transferred_rating: 301 break 302 fleet_a = universe.getFleet(fleet_a_id) 303 aistate = get_aistate() 304 if not fleet_a or fleet_a.empty or fleet_a_id in universe.destroyedObjectIDs(fo.empireID()): 305 aistate.delete_fleet_info(fleet_a_id) 306 aistate.update_fleet_rating(fleet_b_id) 307 308 309def fleet_has_ship_with_role(fleet_id, ship_role): 310 """Returns True if a ship with shipRole is in the fleet.""" 311 universe = fo.getUniverse() 312 fleet = universe.getFleet(fleet_id) 313 314 if fleet is None: 315 return False 316 aistate = get_aistate() 317 for ship_id in fleet.shipIDs: 318 ship = universe.getShip(ship_id) 319 if aistate.get_ship_role(ship.design.id) == ship_role: 320 return True 321 return False 322 323 324def get_ship_id_with_role(fleet_id, ship_role, verbose=True): 325 """Returns a ship with the specified role in the fleet.""" 326 327 if not fleet_has_ship_with_role(fleet_id, ship_role): 328 if verbose: 329 debug("No ship with role %s found." % ship_role) 330 return None 331 332 universe = fo.getUniverse() 333 fleet = universe.getFleet(fleet_id) 334 aistate = get_aistate() 335 336 for ship_id in fleet.shipIDs: 337 ship = universe.getShip(ship_id) 338 if aistate.get_ship_role(ship.design.id) == ship_role: 339 return ship_id 340 341 342def get_empire_fleet_ids(): 343 """Returns all fleetIDs for current empire.""" 344 empire_id = fo.empireID() 345 universe = fo.getUniverse() 346 empire_fleet_ids = [] 347 destroyed_object_ids = universe.destroyedObjectIDs(empire_id) 348 for fleet_id in set(list(universe.fleetIDs) + list(get_aistate().newlySplitFleets)): 349 fleet = universe.getFleet(fleet_id) 350 if fleet is None: 351 continue 352 if fleet.ownedBy(empire_id) and fleet_id not in destroyed_object_ids and not fleet.empty and fleet.shipIDs: 353 empire_fleet_ids.append(fleet_id) 354 return empire_fleet_ids 355 356 357def get_empire_fleet_ids_by_role(fleet_role): 358 """Returns a list with fleet_ids that have the specified role.""" 359 fleet_ids = get_empire_fleet_ids() 360 fleet_ids_with_role = [] 361 aistate = get_aistate() 362 for fleet_id in fleet_ids: 363 if aistate.get_fleet_role(fleet_id) != fleet_role: 364 continue 365 fleet_ids_with_role.append(fleet_id) 366 return fleet_ids_with_role 367 368 369def extract_fleet_ids_without_mission_types(fleets_ids): 370 """Extracts a list with fleetIDs that have no mission.""" 371 aistate = get_aistate() 372 return [fleet_id for fleet_id in fleets_ids if not aistate.get_fleet_mission(fleet_id).type] 373 374 375def assess_fleet_role(fleet_id): 376 """ 377 Assesses ShipRoles represented in a fleet and 378 returns a corresponding overall fleetRole (of type MissionType). 379 """ 380 universe = fo.getUniverse() 381 ship_roles = {} 382 fleet = universe.getFleet(fleet_id) 383 if not fleet: 384 debug("couldn't get fleet with id " + str(fleet_id)) 385 return ShipRoleType.INVALID 386 387 # count ship_roles 388 aistate = get_aistate() 389 for ship_id in fleet.shipIDs: 390 ship = universe.getShip(ship_id) 391 if ship.design: 392 role = aistate.get_ship_role(ship.design.id) 393 else: 394 role = ShipRoleType.INVALID 395 396 if role != ShipRoleType.INVALID: 397 ship_roles[role] = ship_roles.get(role, 0) + 1 398 # determine most common ship_role 399 favourite_role = ShipRoleType.INVALID 400 for ship_role in ship_roles: 401 if ship_roles[ship_role] == max(ship_roles.values()): 402 favourite_role = ship_role 403 404 # assign fleet role 405 if ShipRoleType.CIVILIAN_COLONISATION in ship_roles: 406 selected_role = MissionType.COLONISATION 407 elif ShipRoleType.BASE_COLONISATION in ship_roles: 408 selected_role = MissionType.COLONISATION 409 elif ShipRoleType.CIVILIAN_OUTPOST in ship_roles: 410 selected_role = MissionType.OUTPOST 411 elif ShipRoleType.BASE_OUTPOST in ship_roles: 412 selected_role = MissionType.ORBITAL_OUTPOST 413 elif ShipRoleType.BASE_INVASION in ship_roles: 414 selected_role = MissionType.ORBITAL_INVASION 415 elif ShipRoleType.BASE_DEFENSE in ship_roles: 416 selected_role = MissionType.ORBITAL_DEFENSE 417 elif ShipRoleType.MILITARY_INVASION in ship_roles: 418 selected_role = MissionType.INVASION 419 #### 420 elif favourite_role == ShipRoleType.CIVILIAN_EXPLORATION: 421 selected_role = MissionType.EXPLORATION 422 elif favourite_role == ShipRoleType.MILITARY_ATTACK: 423 selected_role = MissionType.MILITARY 424 elif favourite_role == ShipRoleType.MILITARY: 425 selected_role = MissionType.MILITARY 426 else: 427 selected_role = ShipRoleType.INVALID 428 return selected_role 429 430 431def assess_ship_design_role(design): 432 parts = [get_ship_part(partname) for partname in design.parts if partname and get_ship_part(partname)] 433 434 if any(p.partClass == fo.shipPartClass.colony and p.capacity == 0 for p in parts): 435 if design.speed > 0: 436 return ShipRoleType.CIVILIAN_OUTPOST 437 else: 438 return ShipRoleType.BASE_OUTPOST 439 440 if any(p.partClass == fo.shipPartClass.colony and p.capacity > 0 for p in parts): 441 if design.speed > 0: 442 return ShipRoleType.CIVILIAN_COLONISATION 443 else: 444 return ShipRoleType.BASE_COLONISATION 445 446 if any(p.partClass == fo.shipPartClass.troops for p in parts): 447 if design.speed > 0: 448 return ShipRoleType.MILITARY_INVASION 449 else: 450 return ShipRoleType.BASE_INVASION 451 452 if design.speed == 0: 453 if not parts or parts[0].partClass == fo.shipPartClass.shields: # ToDo: Update logic for new ship designs 454 return ShipRoleType.BASE_DEFENSE 455 else: 456 return ShipRoleType.INVALID 457 458 if design.isArmed or design.hasFighters: 459 return ShipRoleType.MILITARY 460 if any(p.partClass == fo.shipPartClass.detection for p in parts): 461 return ShipRoleType.CIVILIAN_EXPLORATION 462 else: # if no suitable role found, use as (bad) scout as it still has inherent detection 463 warning("Defaulting ship role to 'exploration' for ship with parts: %s", design.parts) 464 return ShipRoleType.CIVILIAN_EXPLORATION 465 466 467def generate_fleet_orders_for_fleet_missions(): 468 """Generates fleet orders from targets.""" 469 debug("Generating fleet orders") 470 471 # The following fleet lists are based on *Roles* -- Secure type missions are done by fleets with Military Roles 472 debug("Fleets by Role\n") 473 debug("Exploration Fleets: %s" % get_empire_fleet_ids_by_role(MissionType.EXPLORATION)) 474 debug("Colonization Fleets: %s" % get_empire_fleet_ids_by_role(MissionType.COLONISATION)) 475 debug("Outpost Fleets: %s" % get_empire_fleet_ids_by_role(MissionType.OUTPOST)) 476 debug("Invasion Fleets: %s" % get_empire_fleet_ids_by_role(MissionType.INVASION)) 477 debug("Military Fleets: %s" % get_empire_fleet_ids_by_role(MissionType.MILITARY)) 478 debug("Orbital Defense Fleets: %s" % get_empire_fleet_ids_by_role(MissionType.ORBITAL_DEFENSE)) 479 debug("Outpost Base Fleets: %s" % get_empire_fleet_ids_by_role(MissionType.ORBITAL_OUTPOST)) 480 debug("Invasion Base Fleets: %s" % get_empire_fleet_ids_by_role(MissionType.ORBITAL_INVASION)) 481 debug("Securing Fleets: %s (currently FLEET_MISSION_MILITARY should be used instead of this Role)" % ( 482 get_empire_fleet_ids_by_role(MissionType.SECURE))) 483 484 aistate = get_aistate() 485 if fo.currentTurn() < 50: 486 debug('') 487 debug("Explored systems:") 488 _print_systems_and_supply(aistate.get_explored_system_ids()) 489 debug("Unexplored systems:") 490 _print_systems_and_supply(aistate.get_unexplored_system_ids()) 491 debug('') 492 493 exploration_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.EXPLORATION]) 494 if exploration_fleet_missions: 495 debug("Exploration targets:") 496 for explorationAIFleetMission in exploration_fleet_missions: 497 debug(" - %s" % explorationAIFleetMission) 498 else: 499 debug("Exploration targets: None") 500 501 colonisation_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.COLONISATION]) 502 if colonisation_fleet_missions: 503 debug("Colonization targets: ") 504 else: 505 debug("Colonization targets: None") 506 for colonisation_fleet_mission in colonisation_fleet_missions: 507 debug(" %s" % colonisation_fleet_mission) 508 509 outpost_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.OUTPOST]) 510 if outpost_fleet_missions: 511 debug("Outpost targets: ") 512 else: 513 debug("Outpost targets: None") 514 for outpost_fleet_mission in outpost_fleet_missions: 515 debug(" %s" % outpost_fleet_mission) 516 517 outpost_base_fleet_missions = aistate.get_fleet_missions_with_any_mission_types( 518 [MissionType.ORBITAL_OUTPOST]) 519 if outpost_base_fleet_missions: 520 debug("Outpost Base targets (must have been interrupted by combat): ") 521 else: 522 debug("Outpost Base targets: None (as expected, due to expected timing of order submission and execution)") 523 for outpost_fleet_mission in outpost_base_fleet_missions: 524 debug(" %s" % outpost_fleet_mission) 525 526 invasion_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.INVASION]) 527 if invasion_fleet_missions: 528 debug("Invasion targets: ") 529 else: 530 debug("Invasion targets: None") 531 for invasion_fleet_mission in invasion_fleet_missions: 532 debug(" %s" % invasion_fleet_mission) 533 534 troop_base_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.ORBITAL_INVASION]) 535 if troop_base_fleet_missions: 536 debug("Invasion Base targets (must have been interrupted by combat): ") 537 else: 538 debug("Invasion Base targets: None (as expected, due to expected timing of order submission and execution)") 539 for invasion_fleet_mission in troop_base_fleet_missions: 540 debug(" %s" % invasion_fleet_mission) 541 542 military_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.MILITARY]) 543 if military_fleet_missions: 544 debug("General Military targets: ") 545 else: 546 debug("General Military targets: None") 547 for military_fleet_mission in military_fleet_missions: 548 debug(" %s" % military_fleet_mission) 549 550 secure_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.SECURE]) 551 if secure_fleet_missions: 552 debug("Secure targets: ") 553 else: 554 debug("Secure targets: None") 555 for secure_fleet_mission in secure_fleet_missions: 556 debug(" %s" % secure_fleet_mission) 557 558 orb_defense_fleet_missions = aistate.get_fleet_missions_with_any_mission_types([MissionType.ORBITAL_DEFENSE]) 559 if orb_defense_fleet_missions: 560 debug("Orbital Defense targets: ") 561 else: 562 debug("Orbital Defense targets: None") 563 for orb_defence_fleet_mission in orb_defense_fleet_missions: 564 debug(" %s" % orb_defence_fleet_mission) 565 566 fleet_missions = list(aistate.get_all_fleet_missions()) 567 destroyed_objects = fo.getUniverse().destroyedObjectIDs(fo.empireID()) 568 569 # merge fleets where appropriate before generating fleet orders. 570 # This allows us to consider the full fleet strength when determining 571 # e.g. whether to engage or avoid an enemy. 572 for mission in fleet_missions: 573 fleet_id = mission.fleet.id 574 fleet = mission.fleet.get_object() 575 if not fleet or not fleet.shipIDs or fleet_id in destroyed_objects: 576 continue 577 mission.check_mergers() 578 # get new set of fleet missions without fleets that are empty after merge 579 fleet_missions = aistate.get_all_fleet_missions() 580 for mission in fleet_missions: 581 mission.generate_fleet_orders() 582 583 584def issue_fleet_orders_for_fleet_missions(): 585 """Issues fleet orders.""" 586 debug('') 587 universe = fo.getUniverse() 588 aistate = get_aistate() 589 fleet_missions = list(aistate.get_all_fleet_missions()) 590 thisround = 0 591 while thisround < 3: 592 thisround += 1 593 debug("Issuing fleet orders round %d:" % thisround) 594 for mission in fleet_missions: 595 fleet_id = mission.fleet.id 596 fleet = mission.fleet.get_object() 597 # check that fleet was merged into another previously during this turn 598 if not fleet or not fleet.shipIDs or fleet_id in universe.destroyedObjectIDs(fo.empireID()): 599 continue 600 mission.issue_fleet_orders() 601 fleet_missions = aistate.misc.get('ReassignedFleetMissions', []) 602 aistate.misc['ReassignedFleetMissions'] = [] 603 debug('') 604 605 606def _print_systems_and_supply(system_ids): 607 universe = fo.getUniverse() 608 empire = fo.getEmpire() 609 fleet_supplyable_system_ids = empire.fleetSupplyableSystemIDs 610 for system_id in system_ids: 611 system = universe.getSystem(system_id) 612 debug(' %s%s' % ( 613 system if system else " S_%s<>" % system_id, 614 'supplied' if system_id in fleet_supplyable_system_ids else '')) 615 616 617def get_fighter_capacity_of_fleet(fleet_id): 618 """Return current and max fighter capacity 619 620 :param fleet_id: 621 :type fleet_id: int 622 :return: current and max fighter capacity 623 """ 624 universe = fo.getUniverse() 625 fleet = universe.getFleet(fleet_id) 626 cur_capacity = 0 627 max_capacity = 0 628 ships = (universe.getShip(ship_id) for ship_id in (fleet.shipIDs if fleet else [])) 629 for ship in ships: 630 design = ship and ship.design 631 design_parts = design.parts if design and design.hasFighters else [] 632 for partname in design_parts: 633 part = get_ship_part(partname) 634 if part and part.partClass == fo.shipPartClass.fighterHangar: 635 cur_capacity += ship.currentPartMeterValue(fo.meterType.capacity, partname) 636 max_capacity += ship.currentPartMeterValue(fo.meterType.maxCapacity, partname) 637 return cur_capacity, max_capacity 638 639 640def get_fuel(fleet_id): 641 """Get fuel of fleet. 642 643 :param fleet_id: Queried fleet 644 :type fleet_id: int 645 :return: fuel of fleet 646 :rtype: float 647 """ 648 fleet = fo.getUniverse().getFleet(fleet_id) 649 return fleet and fleet.fuel or 0.0 650 651 652def get_max_fuel(fleet_id): 653 """Get maximum fuel capacity of fleet. 654 655 :param fleet_id: Queried fleet 656 :type fleet_id: int 657 :return: max fuel of fleet 658 :rtype: float 659 """ 660 fleet = fo.getUniverse().getFleet(fleet_id) 661 return fleet and fleet.maxFuel or 0.0 662 663 664def get_fleet_upkeep(): 665 # TODO: Use new upkeep calculation 666 return 1 + AIDependencies.SHIP_UPKEEP * get_aistate().shipCount 667 668 669def calculate_estimated_time_of_arrival(fleet_id, target_system_id): 670 universe = fo.getUniverse() 671 fleet = universe.getFleet(fleet_id) 672 if not fleet or not fleet.speed: 673 return 99999 674 distance = universe.shortestPathDistance(fleet_id, target_system_id) 675 return math.ceil(float(distance) / fleet.speed) 676 677 678def get_fleet_system(fleet): 679 """Return the current fleet location or the target system if currently on starlane. 680 681 :param fleet: 682 :type fleet: UniverseObject.TargetFleet | int 683 :return: current system_id or target system_id if currently on starlane 684 :rtype: int 685 """ 686 if isinstance(fleet, int): 687 fleet = fo.getUniverse().getFleet(fleet) 688 return fleet.systemID if fleet.systemID != INVALID_ID else fleet.nextSystemID 689 690 691def get_current_and_max_structure(fleet): 692 """Return a 2-tuple of the sums of structure and maxStructure meters of all ships in the fleet 693 694 :param fleet: 695 :type fleet: int | target.TargetFleet | fo.Fleet 696 :return: tuple of sums of structure and maxStructure meters of all ships in the fleet 697 :rtype: (float, float) 698 """ 699 700 universe = fo.getUniverse() 701 destroyed_ids = universe.destroyedObjectIDs(fo.empireID()) 702 if isinstance(fleet, int): 703 fleet = universe.getFleet(fleet) 704 elif isinstance(fleet, TargetFleet): 705 fleet = fleet.get_object() 706 if not fleet: 707 return (0.0, 0.0) 708 ships_cur_health = 0 709 ships_max_health = 0 710 for ship_id in fleet.shipIDs: 711 # Check if we have see this ship get destroyed in a different fleet since the last time we saw the subject fleet 712 # this may be redundant with the new fleet assignment check made below, but for its limited scope it may be more 713 # reliable, in that it does not rely on any particular handling of post-destruction stats 714 if ship_id in destroyed_ids: 715 continue 716 this_ship = universe.getShip(ship_id) 717 # check that the ship has not been seen in a new fleet since this current fleet was last observed 718 if not (this_ship and this_ship.fleetID == fleet.id): 719 continue 720 ships_cur_health += this_ship.initialMeterValue(fo.meterType.structure) 721 ships_max_health += this_ship.initialMeterValue(fo.meterType.maxStructure) 722 723 return ships_cur_health, ships_max_health 724