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
|