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 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
|
import freeOrionAIInterface as fo
from logging import debug, error, info
import FleetUtilsAI
import MoveUtilsAI
import PlanetUtilsAI
from AIDependencies import INVALID_ID, MINIMUM_GUARD_DISTANCE_TO_HOME_SYSTEM
from aistate_interface import get_aistate
from common.fo_typing import SystemId
from EnumsAI import MissionType
from freeorion_tools import get_fleet_position, get_partial_visibility_turn
from target import TargetSystem
from universe.system_network import get_neighbors, systems_connected
graph_flags = set()
border_unexplored_system_ids = set()
def get_current_exploration_info():
"""Returns ([current target list], [available scout list])."""
fleet_ids = FleetUtilsAI.get_empire_fleet_ids_by_role(MissionType.EXPLORATION)
available_scouts = []
already_covered = set()
aistate = get_aistate()
for fleet_id in fleet_ids:
fleet_mission = aistate.get_fleet_mission(fleet_id)
if not fleet_mission.type:
available_scouts.append(fleet_id)
else:
if fleet_mission.type == MissionType.EXPLORATION:
already_covered.add(fleet_mission.target.id)
if not fleet_mission.target:
debug("problem determining existing exploration target systems")
else:
debug("found existing exploration target: %s" % fleet_mission.target)
debug("Current exploration targets: %s" % already_covered)
debug("Available scout fleets: %s" % available_scouts)
return list(already_covered), available_scouts
def assign_scouts_to_explore_systems(): # noqa: C901
# TODO: use Graph Theory to explore closest systems
universe = fo.getUniverse()
capital_sys_id = PlanetUtilsAI.get_capital_sys_id()
# order fleets to explore
if not border_unexplored_system_ids or (capital_sys_id == INVALID_ID):
return
exp_systems_by_dist = sorted((universe.linearDistance(capital_sys_id, x), x) for x in border_unexplored_system_ids)
debug(
"Exploration system considering following system-distance pairs:\n %s"
% ("\n ".join("%3d: %5.1f" % (sys_id, dist) for (dist, sys_id) in exp_systems_by_dist))
)
explore_list = [sys_id for dist, sys_id in exp_systems_by_dist]
already_covered, available_scouts = get_current_exploration_info()
debug("Explorable system IDs: %s" % explore_list)
debug("Already targeted: %s" % already_covered)
aistate = get_aistate()
needs_vis = aistate.misc.setdefault("needs_vis", [])
check_list = aistate.needsEmergencyExploration + needs_vis + explore_list
if INVALID_ID in check_list: # shouldn't normally happen, unless due to bug elsewhere
for sys_list, name in [
(aistate.needsEmergencyExploration, "aistate.needsEmergencyExploration"),
(needs_vis, "needs_vis"),
(explore_list, "explore_list"),
]:
if INVALID_ID in sys_list:
error("INVALID_ID found in " + name, exc_info=True)
# emergency coverage can be due to invasion detection trouble, etc.
debug("Check list: %s" % check_list)
needs_coverage = [sys_id for sys_id in check_list if sys_id not in already_covered and sys_id != INVALID_ID]
debug("Needs coverage: %s" % needs_coverage)
debug(
"Available scouts & AIstate locs: %s"
% [(x, aistate.fleetStatus.get(x, {}).get("sysID", INVALID_ID)) for x in available_scouts]
)
debug("Available scouts & universe locs: %s" % [(x, universe.getFleet(x).systemID) for x in available_scouts])
if not needs_coverage or not available_scouts:
return
# clean up targets which can not or don't need to be scouted
for sys_id in list(needs_coverage):
if sys_id not in explore_list: # doesn't necessarily need direct visit
if universe.getVisibility(sys_id, fo.empireID()) >= fo.visibility.partial:
# already got visibility; remove from visit lists and skip
if sys_id in needs_vis:
del needs_vis[needs_vis.index(sys_id)]
if sys_id in aistate.needsEmergencyExploration:
del aistate.needsEmergencyExploration[aistate.needsEmergencyExploration.index(sys_id)]
debug("system id %d already currently visible; skipping exploration" % sys_id)
needs_coverage.remove(sys_id)
continue
# skip systems threatened by monsters
sys_status = aistate.systemStatus.setdefault(sys_id, {})
if not aistate.character.may_explore_system(sys_status.setdefault("monsterThreat", 0)) or (
fo.currentTurn() < 20 and aistate.systemStatus[sys_id]["monsterThreat"] > 0
):
debug(
"Skipping exploration of system %d due to Big Monster, threat %d"
% (sys_id, aistate.systemStatus[sys_id]["monsterThreat"])
)
needs_coverage.remove(sys_id)
continue
# find the jump distance for all possible scout-system pairings
options = []
available_scouts = set(available_scouts)
for fleet_id in available_scouts:
start = TargetSystem(get_fleet_position(fleet_id))
for sys_id in needs_coverage:
target = TargetSystem(sys_id)
path = MoveUtilsAI.can_travel_to_system(fleet_id, start, target, ensure_return=True)
if not path:
continue
num_jumps = len(path) - 1 # -1 as path contains the original system
options.append((num_jumps, fleet_id, sys_id))
# Apply a simple, greedy heuristic to match scouts to nearby systems:
# Always choose the shortest possible path from the remaining scout-system pairing.
# This is clearly not optimal in the general case but it works well enough for now.
# TODO: Consider using a more sophisticated assignment algorithm
options.sort()
while options:
debug("Remaining options: %s" % options)
_, fleet_id, sys_id = options[0]
fleet_mission = aistate.get_fleet_mission(fleet_id)
target = TargetSystem(sys_id)
info("Sending fleet %d to explore %s" % (fleet_id, target))
fleet_mission.set_target(MissionType.EXPLORATION, target)
options = [option for option in options if option[1] != fleet_id and option[2] != sys_id]
available_scouts.remove(fleet_id)
needs_coverage.remove(sys_id)
debug("Exploration assignment finished.")
debug("Unassigned scouts: %s" % available_scouts)
debug("Unassigned exploration targets: %s" % needs_coverage)
def follow_vis_system_connections(start_system_id, home_system_id): # noqa: C901
universe = fo.getUniverse()
empire_id = fo.empireID()
exploration_list = [start_system_id]
aistate = get_aistate()
while exploration_list:
cur_system_id = exploration_list.pop()
if cur_system_id in graph_flags:
continue
graph_flags.add(cur_system_id)
system = universe.getSystem(cur_system_id)
if cur_system_id in aistate.visBorderSystemIDs:
pre_vis = "a border system"
elif cur_system_id in aistate.visInteriorSystemIDs:
pre_vis = "an interior system"
else:
pre_vis = "an unknown system"
system_header = "*** system %s;" % system
if fo.currentTurn() < 50:
visibility_turn_list = sorted(
universe.getVisibilityTurnsMap(cur_system_id, empire_id).items(), key=lambda x: x[0].numerator
)
visibility_info = ", ".join(f"{vis.name}: {turn}" for vis, turn in visibility_turn_list)
debug(f"{system_header} previously {pre_vis}. Visibility per turn: {visibility_info} ")
status_info = []
else:
status_info = [system_header]
has_been_visible = get_partial_visibility_turn(cur_system_id) > 0
is_connected = systems_connected(cur_system_id, home_system_id)
status_info.append(" -- is%s partially visible" % ("" if has_been_visible else " not"))
status_info.append(" -- is%s visibly connected to homesystem" % ("" if is_connected else " not"))
if has_been_visible:
sys_status = aistate.systemStatus.setdefault(cur_system_id, {})
aistate.visInteriorSystemIDs.add(cur_system_id)
aistate.visBorderSystemIDs.discard(cur_system_id)
neighbors = get_neighbors(cur_system_id)
sys_status.setdefault("neighbors", set()).update(neighbors)
if neighbors:
status_info.append(" -- has neighbors %s" % sorted(neighbors))
for sys_id in neighbors:
if sys_id not in aistate.exploredSystemIDs:
aistate.unexploredSystemIDs.add(sys_id)
if (sys_id not in graph_flags) and (sys_id not in aistate.visInteriorSystemIDs):
aistate.visBorderSystemIDs.add(sys_id)
exploration_list.append(sys_id)
if fo.currentTurn() < 50:
debug("\n".join(status_info))
debug("----------------------------------------------------------")
def update_explored_systems():
universe = fo.getUniverse()
empire = fo.getEmpire()
obs_lanes = empire.obstructedStarlanes()
obs_lanes_list = [el for el in obs_lanes] # should result in list of tuples (sys_id1, sys_id2)
if obs_lanes_list:
debug("Obstructed starlanes are: %s" % ", ".join("{}-{}".format(*item) for item in obs_lanes_list))
else:
debug("No obstructed Starlanes")
newly_explored = []
still_unexplored = []
aistate = get_aistate()
for sys_id in list(aistate.unexploredSystemIDs):
# consider making determination according to visibility rather than actual visit,
# which I think is what empire.hasExploredSystem covers (Dilvish-fo)
if empire.hasExploredSystem(sys_id):
aistate.unexploredSystemIDs.discard(sys_id)
aistate.exploredSystemIDs.add(sys_id)
system = universe.getSystem(sys_id)
debug("Moved system %s from unexplored list to explored list" % system)
border_unexplored_system_ids.discard(sys_id)
newly_explored.append(sys_id)
else:
still_unexplored.append(sys_id)
neighbor_list = []
dummy = []
for id_list, next_list in [(newly_explored, neighbor_list), (neighbor_list, dummy)]:
for sys_id in id_list:
neighbors = get_neighbors(sys_id)
for neighbor_id in neighbors:
# when it matters, unexplored will be smaller than explored
if neighbor_id not in aistate.unexploredSystemIDs:
next_list.append(neighbor_id)
for sys_id in still_unexplored:
neighbors = get_neighbors(sys_id)
if any(nid in aistate.exploredSystemIDs for nid in neighbors):
border_unexplored_system_ids.add(sys_id)
return newly_explored
def request_emergency_exploration(system_id: SystemId):
aistate = get_aistate()
if system_id not in aistate.needsEmergencyExploration:
aistate.needsEmergencyExploration.append(system_id)
def system_could_have_unknown_stationary_guard(system_id: SystemId) -> bool:
"""Return True if the system may have spawned stationary guards.
A stationary guard is defined as immobile monster fleets spawned at game start.
If this function indicates that there is no such guard, there still may be other threats.
"""
# We do not play around invisible guards, so if system was visible at some point,
# there should not be a stationary guard there
system_was_visible = get_partial_visibility_turn(system_id) > 0
if system_was_visible:
return False
# Universe setup settings may forbid guards
if fo.getGalaxySetupData().monsterFrequency == fo.galaxySetupOptionMonsterFreq.none:
return False
# Stationary guards require some distance to the home system to be spawned
home_system = PlanetUtilsAI.get_capital_sys_id()
jump_distance_to_home_system = fo.getUniverse().jumpDistance(system_id, home_system)
if jump_distance_to_home_system < MINIMUM_GUARD_DISTANCE_TO_HOME_SYSTEM:
return False
# No indicator that there isn't a stationary guard
return True
|