File: buildings.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 (263 lines) | stat: -rw-r--r-- 9,715 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
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,
    }
)