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