File: MoveUtilsAI.py

package info (click to toggle)
freeorion 0.5.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 194,940 kB
  • sloc: cpp: 186,508; python: 40,969; ansic: 1,164; xml: 719; makefile: 32; sh: 7
file content (186 lines) | stat: -rw-r--r-- 7,812 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import freeOrionAIInterface as fo
from logging import debug, warning
from typing import Optional

import AIstate
import fleet_orders
import pathfinding
import PlanetUtilsAI
from AIDependencies import DRYDOCK_HAPPINESS_THRESHOLD, INVALID_ID
from aistate_interface import get_aistate
from buildings import get_empire_drydocks
from common.fo_typing import SystemId
from freeorion_tools import get_fleet_position
from target import TargetFleet, TargetSystem
from turn_state import get_system_supply


def create_move_orders_to_system(fleet: TargetFleet, target: TargetSystem) -> list["fleet_orders.OrderMove"]:
    """
    Create a list of move orders from the fleet's current system to the target system.

    :param fleet: Fleet to be moved
    :param target: target system
    :return: list of move orders
    """
    # TODO: use Graph Theory to construct move orders
    # TODO: add priority
    starting_system = fleet.get_system()  # current fleet location or current target system if on starlane
    if starting_system == target:
        # nothing to do here
        return []
    # if the mission does not end at the targeted system, make sure we can actually return to supply after moving.
    ensure_return = target.id not in set(
        AIstate.colonyTargetedSystemIDs + AIstate.outpostTargetedSystemIDs + AIstate.invasionTargetedSystemIDs
    )
    system_targets = can_travel_to_system(fleet.id, starting_system, target, ensure_return=ensure_return)
    result = [fleet_orders.OrderMove(fleet, system) for system in system_targets]
    if not result and starting_system.id != target.id:
        warning(f"fleet {fleet.id} can't travel to system {target}")
    return result


def can_travel_to_system(
    fleet_id: int, start: TargetSystem, target: TargetSystem, ensure_return: bool = False
) -> list[TargetSystem]:
    """
    Return list systems to be visited.
    """
    if start == target:
        return [TargetSystem(start.id)]

    debug(f"Requesting path for fleet {fo.getUniverse().getFleet(fleet_id)} from {start} to {target}")
    target_distance_from_supply = -min(get_system_supply(target.id), 0)

    # low-aggression AIs may not travel far from supply
    if not get_aistate().character.may_travel_beyond_supply(target_distance_from_supply):
        debug("May not move %d out of supply" % target_distance_from_supply)
        return []

    min_fuel_at_target = target_distance_from_supply if ensure_return else 0
    path_info = pathfinding.find_path_with_resupply(
        start.id, target.id, fleet_id, minimum_fuel_at_target=min_fuel_at_target
    )
    if path_info is None:
        debug("Found no valid path.")
        return []

    debug("Found valid path: %s" % str(path_info))
    return [TargetSystem(sys_id) for sys_id in path_info.path]


def get_nearest_supplied_system(start_system_id: SystemId):
    """Return systemAITarget of nearest supplied system from starting system startSystemID."""
    empire = fo.getEmpire()
    fleet_supplyable_system_ids = empire.fleetSupplyableSystemIDs
    universe = fo.getUniverse()

    if start_system_id in fleet_supplyable_system_ids:
        return TargetSystem(start_system_id)
    else:
        min_jumps = 9999  # infinity
        supply_system_id = INVALID_ID
        for system_id in fleet_supplyable_system_ids:
            if start_system_id != INVALID_ID and system_id != INVALID_ID:
                least_jumps_len = universe.jumpDistance(start_system_id, system_id)
                if least_jumps_len < min_jumps:
                    min_jumps = least_jumps_len
                    supply_system_id = system_id
        return TargetSystem(supply_system_id)


def get_best_drydock_system_id(start_system_id: int, fleet_id: int) -> Optional[int]:
    """
    Get system_id of best drydock capable of repair, where best is nearest drydock
    that has a current and target happiness greater than the HAPPINESS_THRESHOLD
    with a path that is not blockaded or that the fleet can fight through to with
    acceptable losses.

    :param start_system_id: current location of fleet - used to find closest target
    :param fleet_id: fleet that needs path to drydock
    :return: most suitable system id where the fleet should be repaired.
    """
    if start_system_id == INVALID_ID:
        warning("get_best_drydock_system_id passed bad system id.")
        return None

    if fleet_id == INVALID_ID:
        warning("get_best_drydock_system_id passed bad fleet id.")
        return None

    universe = fo.getUniverse()
    start_system = TargetSystem(start_system_id)
    drydock_system_ids = set()
    for sys_id, pids in get_empire_drydocks().items():
        if sys_id == INVALID_ID:
            warning("get_best_drydock_system_id passed bad drydock sys_id.")
            continue
        for pid in pids:
            planet = universe.getPlanet(pid)
            if (
                planet
                and planet.currentMeterValue(fo.meterType.happiness) >= DRYDOCK_HAPPINESS_THRESHOLD
                and planet.currentMeterValue(fo.meterType.targetHappiness) >= DRYDOCK_HAPPINESS_THRESHOLD
            ):
                drydock_system_ids.add(sys_id)
                break

    sys_distances = sorted([(universe.jumpDistance(start_system_id, sys_id), sys_id) for sys_id in drydock_system_ids])

    aistate = get_aistate()
    fleet_rating = aistate.get_rating(fleet_id)
    for _, dock_sys_id in sys_distances:
        dock_system = TargetSystem(dock_sys_id)
        path = can_travel_to_system(fleet_id, start_system, dock_system)

        path_rating = sum([aistate.systemStatus[path_sys.id]["totalThreat"] for path_sys in path])

        SAFETY_MARGIN = 10
        if SAFETY_MARGIN * path_rating <= fleet_rating:
            debug(
                f"Drydock recommendation {dock_system} from {start_system} for fleet {universe.getFleet(fleet_id)} with fleet rating {fleet_rating:.1f} and path rating {path_rating:.1f}."
            )
            return dock_system.id

    debug(
        f"No safe drydock recommendation from {start_system} for fleet {universe.getFleet(fleet_id)} with fleet rating {fleet_rating:.1f}."
    )
    return None


def get_safe_path_leg_to_dest(fleet_id, start_id, dest_id):
    start_targ = TargetSystem(start_id)
    dest_targ = TargetSystem(dest_id)
    # TODO actually get a safe path
    this_path = can_travel_to_system(fleet_id, start_targ, dest_targ, ensure_return=False)
    path_ids = [targ.id for targ in this_path if targ.id != start_id] + [start_id]
    universe = fo.getUniverse()
    debug(
        "Fleet %d requested safe path leg from %s to %s, found path %s"
        % (fleet_id, universe.getSystem(start_id), universe.getSystem(dest_id), PlanetUtilsAI.sys_name_ids(path_ids))
    )
    return path_ids[0]


def get_resupply_fleet_order(fleet_target: TargetFleet) -> "fleet_orders.OrderResupply":
    """
    Return fleet_orders.OrderResupply to nearest supplied system.
    """
    # find nearest supplied system
    supplied_system_target = get_nearest_supplied_system(get_fleet_position(fleet_target.id))
    # create resupply AIFleetOrder
    return fleet_orders.OrderResupply(fleet_target, supplied_system_target)


def get_repair_fleet_order(fleet: TargetFleet) -> Optional["fleet_orders.OrderRepair"]:
    """
    Return fleet_orders.OrderRepair for fleet to proceed to system with drydock.
    """
    # TODO Cover new mechanics where happiness increases repair rate - don't always use nearest system!
    # find nearest drydock system
    drydock_sys_id = get_best_drydock_system_id(get_fleet_position(fleet.id), fleet.id)
    if drydock_sys_id is None:
        return None

    debug(f"Ordering fleet {fleet} to {fo.getUniverse().getSystem(drydock_sys_id)} for repair")
    return fleet_orders.OrderRepair(fleet, TargetSystem(drydock_sys_id))