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
|
"""
This module encapsulate some basic buildings manipulation.
It's organized as a set of enums with various methods.
Each enum represent a set of building types with similar usage.
Keep in mind when implementing enum:
- It encapsulate FOCS names and fo API usage exposing convenient API for AI.
- It should not contain any decision logic, , only provide information that is easy to handle.
For example direct manipulation like enqueue or information like if this building could be constructed.
- It should not accept or expose any fo.* object.
- We don't need to keep enums names the same as FOCS ids, so keep them short but recognizable.
TODO: Use building ids only inside that file, change other code to accept enums.
"""
from __future__ import annotations
import freeOrionAIInterface as fo
from collections import defaultdict
from collections.abc import Iterator, Mapping
from copy import copy
from enum import Enum
from itertools import chain
from typing import NamedTuple
from aistate_interface import get_aistate
from common.fo_typing import BuildingId, BuildingName, PlanetId, SystemId
from freeorion_tools import ReadOnlyDict
from freeorion_tools.caching import cache_for_current_turn
from PlanetUtilsAI import Opinion, get_planet_opinion
from turn_state import get_all_empire_planets
@cache_for_current_turn
def get_empire_drydocks() -> Mapping[SystemId, tuple[PlanetId]]:
"""
Return a map from system ids to planet ids where empire drydocks are located.
"""
universe = fo.getUniverse()
drydocks = {}
for pid in Shipyard.ORBITAL_DRYDOCK.built_at():
planet = universe.getPlanet(pid)
drydocks.setdefault(planet.systemID, []).append(pid)
return ReadOnlyDict({k: tuple(v) for k, v in drydocks.items()})
class BuildingTypeBase:
"""
Mixin class for building enums.
This class contains operations that applicable for all building enums.
"""
def is_this_type(self, building_id: BuildingId):
"""Return whether the building with the given identifier is of this type."""
return fo.getUniverse().getBuilding(building_id) == self.value
def enqueue(self, pid: PlanetId) -> bool:
"""
Add building to production queue and return True if succeeded.
"""
return bool(fo.issueEnqueueBuildingProductionOrder(self.value, pid))
def available(self) -> bool:
"""
Return true if this building is available for empire and may be built by this AI.
"""
character = get_aistate().character
return character.may_build_building(self.value) and fo.getEmpire().buildingTypeAvailable(self.value)
def get_opinions(self) -> Opinion:
"""
Returns opinions about the building at the empire's planets.
"""
return get_planet_opinion(self.value)
def queued_at(self) -> list[PlanetId]:
"""
Return list of planet ids where this building is queued.
"""
return [element.locationID for element in fo.getEmpire().productionQueue if (element.name == self.value)]
def queued_at_sys(self) -> list[PlanetId]:
"""
Return list of system ids where this building is queued.
"""
return [fo.getUniverse().getPlanet(pid).systemID for pid in self.queued_at()]
def built_at(self) -> set[PlanetId]:
"""
Return List of planet ids where the building exists.
"""
return _get_building_locations()[self.value].planets
def built_at_sys(self) -> set[SystemId]:
"""
Return List of system ids where the building exists.
"""
return _get_building_locations()[self.value].systems
def built_or_queued_at(self) -> set[PlanetId]:
"""
Return List of planet ids where the building either exists or is queued.
"""
built_at_planets = _get_building_locations()[self.value].planets
ret = copy(built_at_planets)
ret.update(set(self.queued_at()))
return ret
def built_or_queued_at_sys(self) -> set[SystemId]:
"""
Return List of system ids where the building either exists or is queued.
"""
built_at_systems = _get_building_locations()[self.value].systems
ret = copy(built_at_systems)
ret.update(set(self.queued_at_sys()))
return ret
def can_be_enqueued(self, planet: PlanetId) -> bool:
"""
Return whether the building can be enqueued at the given planet.
"""
return fo.getBuildingType(self.value).canBeEnqueued(fo.empireID(), planet)
def can_be_produced(self, planet: PlanetId) -> bool:
"""
Return whether the building can be produced at the given planet.
"""
return fo.getBuildingType(self.value).canBeProduced(fo.empireID(), planet)
def production_cost(self, planet: PlanetId) -> float:
"""
Returns overall production cost of the building at the given planet.
"""
return fo.getBuildingType(self.value).productionCost(fo.empireID(), planet)
def production_time(self, planet: PlanetId) -> int:
"""
Returns minimum number of turns the building takes to finish at the given planet.
"""
return fo.getBuildingType(self.value).productionTime(fo.empireID(), planet)
def turn_cost(self, planet: PlanetId) -> float:
"""
Returns production points then can be spent per turn for the building at the given planet.
"""
return fo.getBuildingType(self.value).perTurnCost(fo.empireID(), planet)
def prerequisite(self): # not yet. -> Optional[BuildingType]:
"""Return another building type, that this is based on or None."""
return _prerequisites.get(self, None)
class BuildingType(BuildingTypeBase, Enum):
"""
Represent basic buildings.
Start adding new buildings here.
There are 2 things why you want to move anything to separate enum.
1) names became shorter, for example BuildingType.SHIPYARD_BASE -> Shipyard.BASE
2) you need new method exclusively to that building/buildings.
"""
AUTO_HISTORY_ANALYSER = "BLD_AUTO_HISTORY_ANALYSER"
BLACK_HOLE_POW_GEN = "BLD_BLACK_HOLE_POW_GEN"
COLLECTIVE_NET = "BLD_COLLECTIVE_NET"
CULTURE_ARCHIVES = "BLD_CULTURE_ARCHIVES"
CULTURE_LIBRARY = "BLD_CULTURE_LIBRARY"
ENCLAVE_VOID = "BLD_ENCLAVE_VOID"
GAS_GIANT_GEN = "BLD_GAS_GIANT_GEN"
GENOME_BANK = "BLD_GENOME_BANK"
INDUSTRY_CENTER = "BLD_INDUSTRY_CENTER"
INTERSPECIES_ACADEMY = "BLD_INTERSPECIES_ACADEMY"
LIGHTHOUSE = "BLD_LIGHTHOUSE"
MEGALITH = "BLD_MEGALITH"
MILITARY_COMMAND = "BLD_MILITARY_COMMAND"
NEUTRONIUM_SYNTH = "BLD_NEUTRONIUM_SYNTH"
NEUTRONIUM_EXTRACTOR = "BLD_NEUTRONIUM_EXTRACTOR"
PALACE = "BLD_IMPERIAL_PALACE"
REGIONAL_ADMIN = "BLD_REGIONAL_ADMIN"
SCANNING_FACILITY = "BLD_SCANNING_FACILITY"
SCRYING_SPHERE = "BLD_SCRYING_SPHERE"
SOL_ORB_GEN = "BLD_SOL_ORB_GEN"
SPACE_ELEVATOR = "BLD_SPACE_ELEVATOR"
SPATIAL_DISTORT_GEN = "BLD_SPATIAL_DISTORT_GEN"
STARGATE = "BLD_STARGATE"
STOCKPILING_CENTER = "BLD_STOCKPILING_CENTER"
TRANSLATOR = "BLD_TRANSLATOR"
XENORESURRECTION_LAB = "BLD_XENORESURRECTION_LAB"
class Shipyard(BuildingTypeBase, Enum):
"""
Represent buildings required to build ships.
"""
BASE = "BLD_SHIPYARD_BASE"
ORBITAL_DRYDOCK = "BLD_SHIPYARD_ORBITAL_DRYDOCK"
NANOROBO = "BLD_SHIPYARD_CON_NANOROBO"
GEO = "BLD_SHIPYARD_CON_GEOINT"
ADV_ENGINE = "BLD_SHIPYARD_CON_ADV_ENGINE"
ASTEROID = "BLD_SHIPYARD_AST"
ASTEROID_REF = "BLD_SHIPYARD_AST_REF"
ORG_ORB_INC = "BLD_SHIPYARD_ORG_ORB_INC"
ORG_CELL_GRO_CHAMB = "BLD_SHIPYARD_ORG_CELL_GRO_CHAMB"
XENO_FACILITY = "BLD_SHIPYARD_ORG_XENO_FAC"
ENRG_COMP = "BLD_SHIPYARD_ENRG_COMP"
ENRG_SOLAR = "BLD_SHIPYARD_ENRG_SOLAR"
NEUTRONIUM_FORGE = "BLD_NEUTRONIUM_FORGE" # not a shipyard by name, but only used for building ships
@staticmethod
def get_system_ship_facilities():
return frozenset({Shipyard.ASTEROID, Shipyard.ASTEROID_REF})
def iterate_buildings_types() -> Iterator[BuildingType | Shipyard]:
"""
Iterator over all building types.
"""
yield from chain(BuildingType, Shipyard)
class _BuildingLocations(NamedTuple):
"""
A set of planets and systems that already contain a building.
"""
planets: set[PlanetId]
systems: set[SystemId]
# Cannot use BuildingType as key since not all buildings may have an enum value
@cache_for_current_turn
def _get_building_locations() -> defaultdict[BuildingName, _BuildingLocations]:
universe = fo.getUniverse()
ret = defaultdict(lambda: _BuildingLocations(set(), set()))
for pid in get_all_empire_planets():
planet = universe.getPlanet(pid)
for building in map(universe.getBuilding, planet.buildingIDs):
val = ret[building.buildingTypeName]
val.planets.add(pid)
val.systems.add(planet.systemID)
return ret
_prerequisites = ReadOnlyDict(
{
Shipyard.ORBITAL_DRYDOCK: Shipyard.BASE,
Shipyard.NANOROBO: Shipyard.ORBITAL_DRYDOCK,
Shipyard.GEO: Shipyard.ORBITAL_DRYDOCK,
Shipyard.ADV_ENGINE: Shipyard.ORBITAL_DRYDOCK,
Shipyard.ASTEROID_REF: Shipyard.ASTEROID,
Shipyard.ORG_ORB_INC: Shipyard.BASE,
Shipyard.ORG_CELL_GRO_CHAMB: Shipyard.ORG_ORB_INC,
Shipyard.XENO_FACILITY: Shipyard.ORG_ORB_INC,
Shipyard.ENRG_COMP: Shipyard.BASE,
Shipyard.ENRG_SOLAR: Shipyard.ENRG_COMP,
# not a technical prerequisite, but it has no purpose without a shipyard
Shipyard.NEUTRONIUM_FORGE: Shipyard.BASE,
}
)
|