File: calculate_stability.py

package info (click to toggle)
freeorion 0.5.1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, 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-- 9,593 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

import AIDependencies
import PolicyAI
from buildings import BuildingType, iterate_buildings_types
from colonization.colony_score import debug_rating
from EnumsAI import FocusType
from freeorion_tools import (
    get_game_rule_int,
    get_named_int,
    get_named_real,
    get_species_stability,
)
from freeorion_tools.caching import cache_for_current_turn
from PlanetUtilsAI import dislike_factor
from turn_state import get_empire_planets_by_species, have_worldtree, luxury_resources
from universe.system_network import within_n_jumps

_size_modifier = {
    fo.planetSize.tiny: get_game_rule_int("RULE_TINY_SIZE_STABILITY", 2),
    fo.planetSize.small: get_game_rule_int("RULE_SMALL_SIZE_STABILITY", 1),
    fo.planetSize.medium: get_game_rule_int("RULE_MEDIUM_SIZE_STABILITY", 0),
    fo.planetSize.large: get_game_rule_int("RULE_LARGE_SIZE_STABILITY", -1),
    fo.planetSize.huge: get_game_rule_int("RULE_HUGE_SIZE_STABILITY", -2),
    fo.planetSize.gasGiant: get_game_rule_int("RULE_GAS_GIANT_SIZE_STABILITY", 0),
    fo.planetSize.asteroids: 0,  # no rule for asteroids yet(?)
}

_environment_modifier = {
    fo.planetEnvironment.good: get_game_rule_int("RULE_GOOD_ENVIRONMENT_STABILITY", 2),
    fo.planetEnvironment.adequate: get_game_rule_int("RULE_ADEQUATE_ENVIRONMENT_STABILITY", 1),
    fo.planetEnvironment.poor: get_game_rule_int("RULE_POOR_ENVIRONMENT_STABILITY", 0),
    fo.planetEnvironment.hostile: get_game_rule_int("RULE_HOSTILE_ENVIRONMENT_STABILITY", -1),
    # That is used by the script. It doesn't matter in practice, AI shouldn't try to settle uninhabitable planets.
    fo.planetEnvironment.uninhabitable: get_game_rule_int("RULE_HOSTILE_ENVIRONMENT_STABILITY", -1),
}


def calculate_stability(planet: fo.planet, species: fo.species) -> float:
    """
    Calculate the focus-independent stability species should have on planet.
    Distance to capital never give negatives, since we could build a regional admin, if it would.
    Supply-connection to nearest regional admin still TBD.
    """
    baseline = fo.getGameRules().getDouble("RULE_BASELINE_PLANET_STABILITY")
    species_mod = get_species_stability(species.name)
    home_bonus = AIDependencies.STABILITY_HOMEWORLD_BONUS if planet.id in species.homeworlds else 0.0
    policies = _evaluate_policies(species)
    specials = _evaluate_specials(planet, species)
    buildings = _evaluate_buildings(planet, species)
    xenophobia = _evaluate_xenophobia(planet, species)
    administration = _evaluate_administration(planet, species)
    rules = _size_modifier[planet.size] + _environment_modifier[planet.environmentForSpecies(species.name)]
    result = baseline + species_mod + home_bonus + policies + specials + buildings + xenophobia + administration + rules
    # missing: supply connection check, artisan bonus, anything else?
    debug_rating(
        f"stability of {species.name} on {planet} would be {result:.1f} (base={baseline:.1f}, "
        f"species={species_mod:.1f}, specials={specials:.1f}, home={home_bonus:.1f}, policies={policies:.1f}, "
        f"buildings={buildings:.1f}, xeno={xenophobia:.1f}, admin={administration:.1f}), rules={rules}"
    )
    return result


@cache_for_current_turn
def _evaluate_policies(species: fo.species) -> float:
    empire = fo.getEmpire()
    like_value = AIDependencies.STABILITY_PER_LIKED_FOCUS
    dislike_value = like_value * dislike_factor()
    result = sum(like_value for p in empire.adoptedPolicies if p in species.likes)
    result -= sum(dislike_value for p in empire.adoptedPolicies if p in species.dislikes)
    if PolicyAI.bureaucracy in empire.adoptedPolicies:
        result += get_named_real("PLC_BUREAUCRACY_STABILITY_FLAT")
    if PolicyAI.diversity in empire.adoptedPolicies:
        current_species = get_empire_planets_by_species()
        # The evaluated planet may add another species
        num_species = len(current_species) + (1 if species not in current_species else 0)
        diversity_value = num_species - get_named_int("PLC_DIVERSITY_THRESHOLD")
        diversity_scaling = get_named_real("PLC_DIVERSITY_SCALING")
        result += diversity_value * diversity_scaling
    if PolicyAI.capital_markets in empire.adoptedPolicies:
        for special, planets in luxury_resources().items():
            if special in species.likes and any(planet.focus == FocusType.FOCUS_INFLUENCE for planet in planets):
                result += get_named_real("CAPITAL_MARKETS_INFLUENCE_BONUS_SCALING")
    # TBD: add conformance, indoctrination, etc. when the AI learns to use them
    return result


def _evaluate_specials(planet: fo.planet, species: fo.species) -> float:
    universe = fo.getUniverse()
    system = universe.getSystem(planet.systemID)
    result = 0.0
    for pid in system.planetIDs:
        if pid == planet.id:
            value = AIDependencies.STABILITY_PER_LIKED_SPECIAL_ON_PLANET
            eval_planet = planet
        else:
            value = AIDependencies.STABILITY_PER_LIKED_SPECIAL_IN_SYSTEM
            eval_planet = universe.getPlanet(pid)
        for special in eval_planet.specials:
            if special in species.likes:
                result += value
            if special in species.dislikes:
                result -= value
    if have_worldtree() and not AIDependencies.not_affect_by_special(AIDependencies.WORLDTREE_SPECIAL, species.name):
        result += AIDependencies.STABILITY_BY_WORLDTREE
    gaia = AIDependencies.GAIA_SPECIAL
    if gaia in planet.specials and not AIDependencies.not_affect_by_special(gaia, species):
        result += 5  # TBD add named real
    return result


@cache_for_current_turn
def _count_building(planet: fo.planet) -> dict[str, tuple[int, int, int]]:
    """Returns Mapping from BuildingType to number of buildings on planet, in system and elsewhere."""
    universe = fo.getUniverse()
    system = universe.getSystem(planet.systemID)
    planet_pid = {planet.id}
    system_pids = {pid for pid in system.planetIDs if pid != planet.id}
    # TODO: add all buildings to BuildingType, so we get them all here
    result = {}
    for building_type in iterate_buildings_types():
        # So far we only consider conquering / settling this planet. By the time we have it,
        # queued buildings are likely finished as well, so we consider them here already.
        all_pids = building_type.built_or_queued_at()
        result[building_type.value] = (
            len(all_pids & planet_pid),
            len(all_pids & system_pids),
            len(all_pids - system_pids - planet_pid),
        )
    return result


def _evaluate_buildings(planet: fo.planet, species: fo.species) -> float:
    result = 0.0
    for name, numbers in _count_building(planet).items():
        if name in species.likes:
            result += (
                numbers[0] * AIDependencies.STABILITY_PER_LIKED_BUILDING_ON_PLANET
                + numbers[1] * AIDependencies.STABILITY_PER_LIKED_BUILDING_IN_SYSTEM
                + numbers[2] ** 0.5 * AIDependencies.STABILITY_BASE_LIKED_BUILDING_ELSEWHERE
            )
        elif name in species.dislikes:
            result -= dislike_factor() * (
                numbers[0] * AIDependencies.STABILITY_PER_LIKED_BUILDING_ON_PLANET
                + numbers[1] * AIDependencies.STABILITY_PER_LIKED_BUILDING_IN_SYSTEM
                + numbers[2] ** 0.5 * AIDependencies.STABILITY_BASE_LIKED_BUILDING_ELSEWHERE
            )
    return result


def _evaluate_xenophobia(planet, species) -> float:
    if AIDependencies.Tags.XENOPHOBIC not in species.tags:
        return 0.0
    universe = fo.getUniverse()
    max_jumps = get_named_int("XENOPHOBIC_MAX_JUMPS")
    relevant_systems = within_n_jumps(planet.systemID, max_jumps)
    penalty_perjump = get_named_real("XENOPHOBIC_TARGET_STABILITY_PERJUMP")
    result = 0.0
    for sys_id in relevant_systems:
        system = universe.getSystem(sys_id)
        for pid in system.planetIDs:
            planet_species = universe.getPlanet(pid).speciesName
            # TODO Do not count owned, different species planets when Racial Purity is adopted. AI doesn't adopt Racial Purity yet.
            if planet_species not in ("SP_EXOBOT", species.name, ""):
                result += penalty_perjump * (1 + max_jumps - universe.jumpDistance(planet.systemID, pid))
    return result


def _evaluate_administration(planet: fo.planet, species: fo.species) -> float:
    result = 0.0
    palace = BuildingType.PALACE
    universe = fo.getUniverse()
    if AIDependencies.Tags.INDEPENDENT not in species.tags:
        admin_systems = BuildingType.REGIONAL_ADMIN.built_or_queued_at_sys() | palace.built_or_queued_at_sys()
        jumps_to_admin = min((universe.jumpDistance(planet.systemID, admin) for admin in admin_systems), default=99)
        # cap at 5, if it is 6 or more, we could build a Regional Admin on this planet
        # TODO: check if the planet would be supply-connected to the nearest administration
        # disconnected gives namedReal("DISCONNECTED_FROM_CAPITAL_AND_REGIONAL_ADMIN_STABILITY_PENALTY")
        result += 5 - min(jumps_to_admin, 5)
    if palace.built_at():  # bonus is only given the capital actually contains the palace
        # When rebuilding a palace, the planet only becomes the capital the turn after the palace is finished.
        # So we cannot use fo.getEmpire().capitalID here, but for the calculation we do consider it the capital.
        capital = universe.getPlanet(list(palace.built_at())[0])
        if species.name == capital.speciesName:
            result += 5.0
    return result