1from logging import warning, debug
2
3import freeOrionAIInterface as fo  # pylint: disable=import-error
4from aistate_interface import get_aistate
5import AIstate
6import fleet_orders
7import PlanetUtilsAI
8import pathfinding
9from AIDependencies import INVALID_ID, DRYDOCK_HAPPINESS_THRESHOLD
10from target import TargetSystem
11from turn_state import state
12
13
14def create_move_orders_to_system(fleet, target):
15    """
16    Create a list of move orders from the fleet's current system to the target system.
17
18    :param fleet: Fleet to be moved
19    :type fleet: target.TargetFleet
20    :param target: target system
21    :type target: target.TargetSystem
22    :return: list of move orders
23    :rtype: list[fleet_orders.OrdersMove]
24    """
25    # TODO: use Graph Theory to construct move orders
26    # TODO: add priority
27    starting_system = fleet.get_system()  # current fleet location or current target system if on starlane
28    if starting_system == target:
29        # nothing to do here
30        return []
31    # if the mission does not end at the targeted system, make sure we can actually return to supply after moving.
32    ensure_return = target.id not in set(AIstate.colonyTargetedSystemIDs + AIstate.outpostTargetedSystemIDs
33                                         + AIstate.invasionTargetedSystemIDs)
34    system_targets = can_travel_to_system(fleet.id, starting_system, target, ensure_return=ensure_return)
35    result = [fleet_orders.OrderMove(fleet, system) for system in system_targets]
36    if not result and starting_system.id != target.id:
37        warning("fleet %s can't travel to system %s" % (fleet.id, target))
38    return result
39
40
41def can_travel_to_system(fleet_id, start, target, ensure_return=False):
42    """
43    Return list systems to be visited.
44
45    :param fleet_id:
46    :type fleet_id: int
47    :param start:
48    :type start: target.TargetSystem
49    :param target:
50    :type target:  target.TargetSystem
51    :param ensure_return:
52    :type ensure_return: bool
53    :return:
54    :rtype: list
55    """
56    if start == target:
57        return [TargetSystem(start.id)]
58
59    debug("Requesting path for fleet %s from %s to %s" % (fo.getUniverse().getFleet(fleet_id), start, target))
60    target_distance_from_supply = -min(state.get_system_supply(target.id), 0)
61
62    # low-aggression AIs may not travel far from supply
63    if not get_aistate().character.may_travel_beyond_supply(target_distance_from_supply):
64        debug("May not move %d out of supply" % target_distance_from_supply)
65        return []
66
67    min_fuel_at_target = target_distance_from_supply if ensure_return else 0
68    path_info = pathfinding.find_path_with_resupply(start.id, target.id, fleet_id,
69                                                    minimum_fuel_at_target=min_fuel_at_target)
70    if path_info is None:
71        debug("Found no valid path.")
72        return []
73
74    debug("Found valid path: %s" % str(path_info))
75    return [TargetSystem(sys_id) for sys_id in path_info.path]
76
77
78def get_nearest_supplied_system(start_system_id):
79    """ Return systemAITarget of nearest supplied system from starting system startSystemID."""
80    empire = fo.getEmpire()
81    fleet_supplyable_system_ids = empire.fleetSupplyableSystemIDs
82    universe = fo.getUniverse()
83
84    if start_system_id in fleet_supplyable_system_ids:
85        return TargetSystem(start_system_id)
86    else:
87        min_jumps = 9999  # infinity
88        supply_system_id = INVALID_ID
89        for system_id in fleet_supplyable_system_ids:
90            if start_system_id != INVALID_ID and system_id != INVALID_ID:
91                least_jumps_len = universe.jumpDistance(start_system_id, system_id)
92                if least_jumps_len < min_jumps:
93                    min_jumps = least_jumps_len
94                    supply_system_id = system_id
95        return TargetSystem(supply_system_id)
96
97
98def get_best_drydock_system_id(start_system_id, fleet_id):
99    """
100
101    Get system_id of best drydock capable of repair, where best is nearest drydock
102    that has a current and target happiness greater than the HAPPINESS_THRESHOLD
103    with a path that is not blockaded or that the fleet can fight through to with
104    acceptable losses.
105
106    :param start_system_id: current location of fleet - used to find closest target
107    :type start_system_id: int
108    :param fleet_id: fleet that needs path to drydock
109    :type: int
110    :return: most suitable system id where the fleet should be repaired.
111    :rtype: int
112    """
113    if start_system_id == INVALID_ID:
114        warning("get_best_drydock_system_id passed bad system id.")
115        return None
116
117    if fleet_id == INVALID_ID:
118        warning("get_best_drydock_system_id passed bad fleet id.")
119        return None
120
121    universe = fo.getUniverse()
122    start_system = TargetSystem(start_system_id)
123    drydock_system_ids = set()
124    for sys_id, pids in state.get_empire_drydocks().items():
125        if sys_id == INVALID_ID:
126            warning("get_best_drydock_system_id passed bad drydock sys_id.")
127            continue
128        for pid in pids:
129            planet = universe.getPlanet(pid)
130            if (planet and
131                    planet.currentMeterValue(fo.meterType.happiness) >= DRYDOCK_HAPPINESS_THRESHOLD and
132                    planet.currentMeterValue(fo.meterType.targetHappiness) >= DRYDOCK_HAPPINESS_THRESHOLD):
133                drydock_system_ids.add(sys_id)
134                break
135
136    sys_distances = sorted([(universe.jumpDistance(start_system_id, sys_id), sys_id)
137                            for sys_id in drydock_system_ids])
138
139    aistate = get_aistate()
140    fleet_rating = aistate.get_rating(fleet_id)
141    for _, dock_sys_id in sys_distances:
142        dock_system = TargetSystem(dock_sys_id)
143        path = can_travel_to_system(fleet_id, start_system, dock_system)
144
145        path_rating = sum([aistate.systemStatus[path_sys.id]['totalThreat']
146                           for path_sys in path])
147
148        SAFETY_MARGIN = 10
149        if SAFETY_MARGIN * path_rating <= fleet_rating:
150            debug("Drydock recommendation %s from %s for fleet %s with fleet rating %.1f and path rating %.1f."
151                  % (dock_system, start_system, universe.getFleet(fleet_id), fleet_rating, path_rating))
152            return dock_system.id
153
154    debug("No safe drydock recommendation from %s for fleet %s with fleet rating %.1f."
155          % (start_system, universe.getFleet(fleet_id), fleet_rating))
156    return None
157
158
159def get_safe_path_leg_to_dest(fleet_id, start_id, dest_id):
160    start_targ = TargetSystem(start_id)
161    dest_targ = TargetSystem(dest_id)
162    # TODO actually get a safe path
163    this_path = can_travel_to_system(fleet_id, start_targ, dest_targ, ensure_return=False)
164    path_ids = [targ.id for targ in this_path if targ.id != start_id] + [start_id]
165    universe = fo.getUniverse()
166    debug("Fleet %d requested safe path leg from %s to %s, found path %s" % (
167        fleet_id, universe.getSystem(start_id), universe.getSystem(dest_id), PlanetUtilsAI.sys_name_ids(path_ids)))
168    return path_ids[0]
169
170
171def get_resupply_fleet_order(fleet_target, current_system_target):
172    """Return fleet_orders.OrderResupply to nearest supplied system.
173
174    :param fleet_target: fleet that needs to be resupplied
175    :type fleet_target: target.TargetFleet
176    # TODO check if we can remove this id, because fleet already have it.
177    :param current_system_target: current system of fleet
178    :type current_system_target: target.TargetSystem
179    :return: order to resupply
180    :rtype fleet_orders.OrderResupply
181    """
182    # find nearest supplied system
183    supplied_system_target = get_nearest_supplied_system(current_system_target.id)
184    # create resupply AIFleetOrder
185    return fleet_orders.OrderResupply(fleet_target, supplied_system_target)
186
187
188def get_repair_fleet_order(fleet, current_system_id):
189    """Return fleet_orders.OrderRepair for fleet to proceed to system with drydock.
190
191    :param fleet: fleet that need to be repaired
192    :type fleet: target.TargetFleet
193    # TODO check if we can remove this id, because fleet already have it.
194    :param current_system_id: current location of the fleet, next system if currently on starlane.
195    :type current_system_id: int
196    :return: order to repair
197    :rtype fleet_orders.OrderRepair
198    """
199    # TODO Cover new mechanics where happiness increases repair rate - don't always use nearest system!
200    # find nearest drydock system
201    drydock_sys_id = get_best_drydock_system_id(current_system_id, fleet.id)
202    if drydock_sys_id is None:
203        return None
204
205    debug("Ordering fleet %s to %s for repair" % (fleet, fo.getUniverse().getSystem(drydock_sys_id)))
206    return fleet_orders.OrderRepair(fleet, TargetSystem(drydock_sys_id))
207