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 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
|
import copy
import freeOrionAIInterface as fo
from logging import debug, error, info, warning
from AIDependencies import INVALID_ID
from freeorion_tools import (
assertion_fails,
get_ship_part,
)
from freeorion_tools.design_compare import recursive_dict_diff
from turn_state import get_inhabited_planets
def build_cache_key(hullname: str, partlist: list[str]) -> str:
"""
This reference name is used to identify existing designs and is mapped
by Cache.map_reference_design_name to the ingame design name. Order of components are ignored.
:param hullname: hull name
:param partlist: list of part names
:return: reference name
"""
return "{}-{}".format(hullname, "-".join(sorted(partlist))) # "Hull-Part1-Part2-Part3-Part4"
class ShipDesignCache:
"""This class handles the caching of information used to assess and build shipdesigns in this module.
Important methods:
update_for_new_turn(self): Updates the cache for the current turn, to be called once at the beginning of each turn.
Important members:
design_id_by_name # {"designname": designid}
part_by_partname # {"partname": part object}
map_reference_design_name # {"reference_designname": "ingame_designname"}, cf. _build_reference_name()
hulls_for_planets # buildable hulls per planet {planetID: ["buildableHull1", "buildableHull2", ...]}
parts_for_planets # buildable parts per planet and slot: {planetID: {slottype1: ["part1", "part2"]}}
best_designs # {shipclass: {reqTup: {species: {available_parts: {hull: (rating, best_parts)}}}}}
production_cost # {planetID: {"partname1": local_production_cost, "hullname1": local_production_cost}}
production_time # {planetID: {"partname1": local_production_time, "hullname1": local_production_time}}
Debug methods:
print_CACHENAME(self), e.g. print_hulls_for_planets: prints content of the cache in some nicer format
print_all(self): calls all the printing functions
"""
def __init__(self):
"""Cache is empty on creation"""
self.design_id_by_name = {}
self.part_by_partname = {}
self.map_reference_design_name = {}
self.hulls_for_planets = {}
self.parts_for_planets = {}
self.best_designs = {}
self.production_cost = {}
self.production_time = {}
self.last_printed = {}
def update_for_new_turn(self):
"""Update the cache for the current turn.
Make sure this function is called once at the beginning of the turn,
i.e. before any other function of this module is used.
"""
info(10 * "=" + "Updating ShipDesignCache for new turn" + 10 * "=")
if not self.map_reference_design_name:
self._build_cache_after_load()
self._check_cache_for_consistency()
self.update_cost_cache()
self._update_buildable_items_this_turn()
def print_design_id_by_name(self):
"""Print the design_id_by_name cache."""
debug("DesignID cache: %s" % self.design_id_by_name)
def print_part_by_partname(self):
"""Print the part_by_partname cache."""
debug("Parts cached by name: %s" % self.part_by_partname)
def print_map_reference_design_name(self):
"""Print the ingame, reference name map of shipdesigns."""
debug("Design name map: %s" % self.map_reference_design_name)
def print_hulls_for_planets(self, pid=None):
"""Print the hulls buildable on each planet.
:param pid: None, int or list of ints
"""
if pid is None:
planets = list(self.hulls_for_planets)
elif isinstance(pid, int):
planets = [pid]
elif isinstance(pid, list):
planets = pid
else:
error("Invalid parameter 'pid' for 'print_hulls_for_planets'. Expected int, list or None.")
return
debug("Hull-cache:")
get_planet = fo.getUniverse().getPlanet
for pid in planets:
debug(f"{get_planet(pid).name}: {self.hulls_for_planets[pid]}")
def print_parts_for_planets(self, pid=None):
"""Print the parts buildable on each planet.
:param pid: int or list of ints
"""
if pid is None:
planets = list(self.parts_for_planets)
elif isinstance(pid, int):
planets = [pid]
elif isinstance(pid, list):
planets = pid
else:
error("Invalid parameter 'pid' for 'print_parts_for_planets'. Expected int, list or None.")
return
debug("Available parts per planet:")
get_planet = fo.getUniverse().getPlanet
for pid in planets:
debug(" %s:" % get_planet(pid).name)
for slot in self.parts_for_planets[pid]:
debug(f" {slot}: {self.parts_for_planets[pid][slot]}")
def print_best_designs(self, print_diff_only: bool = True):
"""Print the best designs that were previously found.
:param print_diff_only: Print only changes to cache since last print
"""
debug("Currently cached best designs:")
if print_diff_only:
print_dict = recursive_dict_diff(self.best_designs, self.last_printed, diff_level_threshold=1)
else:
print_dict = self.best_designs
for classname in print_dict:
debug(classname)
cache_name = print_dict[classname]
for consider_fleet in cache_name:
debug(4 * " " + str(consider_fleet))
cache_upkeep = cache_name[consider_fleet]
for req_tuple in cache_upkeep:
debug(8 * " " + str(req_tuple))
cache_reqs = cache_upkeep[req_tuple]
for tech_tuple in cache_reqs:
debug(12 * " " + str(tech_tuple) + " # relevant tech upgrades")
cache_techs = cache_reqs[tech_tuple]
for species_tuple in cache_techs:
debug(16 * " " + str(species_tuple) + " # relevant species stats")
cache_species = cache_techs[species_tuple]
for av_parts in cache_species:
debug(20 * " " + str(av_parts))
cache_parts = cache_species[av_parts]
for hullname in sorted(cache_parts, reverse=True, key=lambda x: cache_parts[x][0]):
debug(24 * " " + hullname + ":" + str(cache_parts[hullname]))
self.last_printed = copy.deepcopy(self.best_designs)
def print_production_cost(self):
"""Print production_cost cache."""
universe = fo.getUniverse()
debug("Cached production cost per planet:")
for pid in self.production_cost:
debug(f" {universe.getPlanet(pid).name}: {self.production_cost[pid]}")
def print_production_time(self):
"""Print production_time cache."""
universe = fo.getUniverse()
debug("Cached production cost per planet:")
for pid in self.production_time:
debug(f" {universe.getPlanet(pid).name}: {self.production_time[pid]}")
def print_all(self):
"""Print the entire ship design cache."""
debug("Printing the ShipDesignAI cache...")
self.print_design_id_by_name()
self.print_part_by_partname()
self.print_map_reference_design_name()
self.print_hulls_for_planets()
self.print_parts_for_planets()
self.print_best_designs()
self.print_production_cost()
self.print_production_time()
def update_cost_cache(self, partnames=None, hullnames=None):
"""Cache the production cost and time for each part and hull for each inhabited planet for this turn.
If partnames and hullnames are both None, rebuild Cache with available parts.
Otherwise, update cache for the specified items.
:param partnames: iterable
:param hullnames: iterable
"""
empire = fo.getEmpire()
empire_id = empire.empireID
parts_to_update = set()
hulls_to_update = set()
if partnames is None and hullnames is None:
# clear cache and rebuild with available parts, called at the beginning of the turn
self.production_cost.clear()
self.production_time.clear()
parts_to_update.update(list(empire.availableShipParts))
hulls_to_update.update(list(empire.availableShipHulls))
if partnames:
parts_to_update.update(partnames)
if hullnames:
hulls_to_update.update(hullnames)
# no need to update items we already cached in this turn
pids = list(get_inhabited_planets())
if self.production_cost and pids:
cached_items = set(self.production_cost[pids[0]].keys())
parts_to_update -= cached_items
hulls_to_update -= cached_items
for partname in parts_to_update:
part = get_ship_part(partname)
for pid in pids:
self.production_cost.setdefault(pid, {})[partname] = part.productionCost(empire_id, pid, INVALID_ID)
self.production_time.setdefault(pid, {})[partname] = part.productionTime(empire_id, pid, INVALID_ID)
for hullname in hulls_to_update:
hull = fo.getShipHull(hullname)
for pid in pids:
self.production_cost.setdefault(pid, {})[hullname] = hull.productionCost(empire_id, pid, INVALID_ID)
self.production_time.setdefault(pid, {})[hullname] = hull.productionTime(empire_id, pid, INVALID_ID)
def _build_cache_after_load(self):
"""Build cache after loading or starting a game.
This function is supposed to be called after a reload or at the first turn.
It reads out all the existing ship designs and then updates the following cache:
- map_reference_design_name
- design_id_by_name
"""
if self.map_reference_design_name or self.design_id_by_name:
warning("ShipDesignAI.Cache._build_cache_after_load() called but cache is not empty.")
for design_id in fo.getEmpire().allShipDesigns:
design = fo.getShipDesign(design_id)
reference_name = build_cache_key(design.hull, design.parts)
self.map_reference_design_name[reference_name] = design.name
self.design_id_by_name[design.name] = design_id
def _check_cache_for_consistency(self): # noqa: C901
"""Check if the persistent cache is consistent with the gamestate and fix it if not.
This function should be called once at the beginning of the turn (before update_shipdesign_cache()).
Especially (only?) in multiplayer games, the shipDesignIDs may sometimes change across turns.
"""
debug("Checking persistent cache for consistency...")
try:
for partname in self.part_by_partname:
cached_name = self.part_by_partname[partname].name
if cached_name != partname:
self.part_by_partname[partname] = fo.getShipPart(partname)
error(f"Part cache corrupted. Expected: {partname}, got: {cached_name}. Cache was repaired.")
except Exception as e:
self.part_by_partname.clear()
error(e, exc_info=True)
corrupted = []
# create a copy of the dict-keys so we can alter the dict
for designname in list(self.design_id_by_name):
# dropping invalid designs from cache
if self.design_id_by_name[designname] == INVALID_ID:
del self.design_id_by_name[designname]
continue
try:
cached_name = fo.getShipDesign(self.design_id_by_name[designname]).name
if cached_name != designname:
warning(f"ShipID cache corrupted. Expected: {designname}, got: {cached_name}.")
design_id = next(
iter(
[
shipDesignID
for shipDesignID in fo.getEmpire().allShipDesigns
if designname == fo.getShipDesign(shipDesignID).name
]
),
None,
)
if design_id is not None:
self.design_id_by_name[designname] = design_id
else:
corrupted.append(designname)
except AttributeError:
warning("ShipID cache corrupted. Could not get cached shipdesign. Repairing Cache.", exc_info=True)
design_id = next(
iter(
[
shipDesignID
for shipDesignID in fo.getEmpire().allShipDesigns
if designname == fo.getShipDesign(shipDesignID).name
]
),
None,
)
if design_id is not None:
self.design_id_by_name[designname] = design_id
else:
corrupted.append(designname)
for corrupted_entry in corrupted:
del self.design_id_by_name[corrupted_entry]
bad_ref = next(
iter([_key for _key, _val in self.map_reference_design_name.items() if _val == corrupted_entry]), None
)
if bad_ref is not None:
del self.map_reference_design_name[bad_ref]
def _update_buildable_items_this_turn(self):
"""Calculate which parts and hulls can be built on each planet this turn."""
self.hulls_for_planets.clear()
self.parts_for_planets.clear()
empire = fo.getEmpire()
all_hulls = list(empire.availableShipHulls)
all_parts = list(empire.availableShipParts)
for pid in get_inhabited_planets():
for hull_name in all_hulls:
hull = fo.getShipHull(hull_name)
if assertion_fails(hull is not None):
continue
if hull.productionLocation(pid):
self.hulls_for_planets.setdefault(pid, []).append(hull_name)
for part_name in all_parts:
ship_part = get_ship_part(part_name)
if assertion_fails(ship_part is not None):
continue
slot_types = ship_part.mountableSlotTypes
if ship_part.productionLocation(pid):
for slot_type in slot_types:
self.parts_for_planets.setdefault(pid, {}).setdefault(slot_type, []).append(part_name)
|