Skip to content

Commit

Permalink
[AI] Use liberty policy (#3828)
Browse files Browse the repository at this point in the history
* First AI use of liberty
* Improved documentation of protection focus
* Some fixes and improvements to RessourceAI
* Removed obsolete foci
* Avoid a 'death-cycle' where all planets switch to protection
while influence keeps dropping deeper and deeper.
  • Loading branch information
Grummel7 committed May 9, 2022
1 parent 6807519 commit 3abfde4
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 39 deletions.
2 changes: 0 additions & 2 deletions default/python/AI/EnumsAI.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@ class FocusType:
FOCUS_GROWTH = "FOCUS_GROWTH"
FOCUS_INDUSTRY = "FOCUS_INDUSTRY"
FOCUS_RESEARCH = "FOCUS_RESEARCH"
FOCUS_TRADE = "FOCUS_TRADE"
FOCUS_CONSTRUCTION = "FOCUS_CONSTRUCTION"
FOCUS_INFLUENCE = "FOCUS_INFLUENCE"


Expand Down
165 changes: 144 additions & 21 deletions default/python/AI/PolicyAI.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# from __future__ import annotations

import freeOrionAIInterface as fo
from copy import copy
from logging import debug, error
Expand All @@ -10,6 +12,9 @@

propaganda = "PLC_PROPAGANDA"
algo_research = "PLC_ALGORITHMIC_RESEARCH"
liberty = "PLC_LIBERTY"
diversity = "PLC_DIVERSITY"
artisans = "PLC_ARTISAN_WORKSHOPS"
moderation = "PLC_MODERATION"
industrialism = "PLC_INDUSTRIALISM"
technocracy = "PLC_TECHNOCRACY"
Expand All @@ -18,26 +23,75 @@
infra1 = "PLC_PLANETARY_INFRA"
infra2 = "PLC_SYSTEM_INFRA"
infra3 = "PLC_INTERSTELLAR_INFRA"
basics = [
propaganda,
algo_research,
infra1,
# no use for the extra slot yet, and they are expensive
# infra2,
# infra3,
]


class _EmpireOutput:
def __init__(self):
self.industry = 0.0
self.research = 0.0
self.influence = 0.0
self.population_stability = 0.0

def __str__(self):
return "pp=%.1f, rp=%.1f, ip=%.1f, population_stabililty=%d" % (
self.industry,
self.research,
self.influence,
self.population_stability,
)

def is_better_than(self, other, cost_for_other: float) -> bool: # other: _EmpireOutput,
"""Return true if this is better than changing to other at given IP cost."""
aistate = get_aistate()
delta_pp = (self.industry - other.industry) * aistate.get_priority(PriorityType.RESOURCE_PRODUCTION)
delta_rp = (self.research - other.research) * aistate.get_priority(PriorityType.RESOURCE_RESEARCH)
delta_ip = (self.influence - other.influence) * aistate.get_priority(PriorityType.RESOURCE_INFLUENCE)
delta_stability = self.population_stability - other.population_stability
# delta is per round, adoption cost is a one-time cost
cost = cost_for_other * aistate.get_priority(PriorityType.RESOURCE_INFLUENCE) / 10
result = delta_pp + delta_rp + delta_ip + delta_stability + cost > 0
debug(f"is_better_than: {delta_pp} + {delta_rp} + {delta_ip} + {delta_stability} + {cost} > 0 => {result}")
return result

def add_planet(self, planet: fo.planet) -> None:
"""Add output of the given planet to this object."""
self.industry += planet.currentMeterValue(fo.meterType.targetIndustry)
self.research += planet.currentMeterValue(fo.meterType.targetResearch)
self.influence += planet.currentMeterValue(fo.meterType.targetInfluence)
current_population = planet.currentMeterValue(fo.meterType.population)
target_population = planet.currentMeterValue(fo.meterType.targetPopulation)
# current is what counts now, but look a little ahead
population = (3 * current_population + target_population) / 4
# no bonuses above 20
stability = min(planet.currentMeterValue(fo.meterType.targetHappiness), 20)
if stability < -1:
# increase both for the risk of losing the colony
population += 1
stability -= 1
elif stability >= 10:
# 10 give a lot of bonuses, increases above 10 are less important
stability = 11 + (stability - 10) / 2
self.population_stability += population * stability


class PolicyManager:
"""Policy Manager for one round"""

def __init__(self):
# resourceAvailable includes this turns production, but that is wrong for influence
def __init__(self, status_only: bool = False):
self._empire = fo.getEmpire()
self._universe = fo.getUniverse()
self._aistate = get_aistate()
# resourceAvailable includes this turns production, but that is wrong for influence
self._ip = self._empire.resourceAvailable(fo.resourceType.influence) - self._get_infl_prod()
self._adopted = set(self._empire.adoptedPolicies)
# When we continue a game in which we just adopted a policy, game state shows the policy as adopted,
# but IP still unspent. Correct it here, then calculate anew whether we want to adopt it.
if not status_only:
for entry in self._empire.turnsPoliciesAdopted:
if entry.data() == fo.currentTurn():
debug(f"reverting saved adopt {entry.key()}")
fo.issueDeadoptPolicyOrder(entry.key())
self._adopted.remove(entry.key())
self._originally_adopted = copy(self._adopted)
self._adoptable = self._get_adoptable()
empire_owned_planet_ids = PlanetUtilsAI.get_owned_planets_by_empire()
Expand All @@ -61,7 +115,9 @@ def generate_orders(self) -> None:
self._adoptable,
self._adopted,
)
self._process_basics()
self._process_social()
self._process_infrastructure()
# TBD: military policies
# we need the extra slots first
if infra1 in self._adopted:
self._process_bureaucracy()
Expand All @@ -70,12 +126,75 @@ def generate_orders(self) -> None:
debug("End of turn IP: %.2f + %.2f", self._ip, new_production)
self._determine_influence_priority(new_production)

def _process_basics(self) -> None:
for policy in basics:
if policy in self._adoptable:
self._adopt(policy)
def _process_social(self) -> None:
"""Process social policies."""
self._process_liberty()
# The last two we simply adopt when we have a slot and enough IP.
# Note that this will adopt propaganda in turn 1
# TBD check for dislike? algo_research is currently disliked by Chato
if self._can_adopt(propaganda):
self._adopt(propaganda)
if self._can_adopt(algo_research):
self._adopt(algo_research)

def _process_liberty(self) -> None:
"""
Adopt or deadopt liberty, may replace propaganda.
Liberty is available very early, but to help setting up basic policies we keep propaganda at least
until we have infra1. Getting a second slot before that should be nearly impossible.
Centralization is always kept only for one turn and if population has a strong opinion on it,
stability calculation may be extremely different from normal turns, leading to a change that would
possibly be reverted next turn, so we skip processing liberty while centralization is adopted.
"""
if (
infra1 in self._adopted
and centralization not in self._adopted
and (liberty in self._adopted or self._can_adopt(liberty, propaganda))
):
adoption_cost = fo.getPolicy(liberty).adoptionCost()
# liberty will generally make species less happy, but generates research
debug("Checking liberty, first without change:")
current_output = self._calculate_empire_output()
if liberty in self._adopted:
self._deadopt(liberty)
without_liberty = self._calculate_empire_output()
if current_output.is_better_than(without_liberty, 0):
self._universe.updateMeterEstimates(self._populated_planet_ids)
self._adopt(liberty)
elif self._can_adopt(liberty):
self._adopt(liberty)
with_liberty = self._calculate_empire_output()
if current_output.is_better_than(with_liberty, adoption_cost):
self._universe.updateMeterEstimates(self._populated_planet_ids)
self._deadopt(liberty)
else:
# Can only adopt it by replacing propaganda
self._deadopt(propaganda)
self._adopt(liberty)
with_liberty = self._calculate_empire_output()
if current_output.is_better_than(with_liberty, adoption_cost):
self._universe.updateMeterEstimates(self._populated_planet_ids)
self._deadopt(liberty)
self._adopt(propaganda)

def _calculate_empire_output(self) -> _EmpireOutput:
# TBD: here we could use the stability-adapted update, that would be much better than trying
# to rate the stability effects again production effects.
self._universe.updateMeterEstimates(self._populated_planet_ids)
result = _EmpireOutput()
for pid in self._populated_planet_ids:
result.add_planet(self._universe.getPlanet(pid))
debug(f"Empire output: {result}")
return result

def _process_infrastructure(self) -> None:
"""Handle infrastructure policies."""
if infra1 in self._adoptable:
self._adopt(infra1)
# TBD infra2 / 3, then use the additional slot

def _process_bureaucracy(self) -> None:
"""Handle adoption and regular re-adoption of bureaucracy and its prerequisite centralization."""
if bureaucracy in self._adopted:
if fo.currentTurn() >= self._empire.turnsPoliciesAdopted[bureaucracy] + self._max_turn_bureaucracy:
self._deadopt(bureaucracy)
Expand All @@ -84,7 +203,7 @@ def _process_bureaucracy(self) -> None:
self._try_adopt_bureaucracy()
# We try not to adopt it when we cannot replace it, but enemy action may
# make the best plans fails. Keeping it will usually cost too much influence.
self._check_deadopt_centralization()
self._maybe_deadopt_centralization()

def _try_adopt_centralization(self) -> None:
"""Try to adopt centralization as a prerequisite for bureaucracy.
Expand Down Expand Up @@ -119,7 +238,7 @@ def _try_adopt_bureaucracy(self) -> None:
self._deadopt(centralization)
self._adopt(infra1)

def _check_deadopt_centralization(self) -> None:
def _maybe_deadopt_centralization(self) -> None:
"""Deadopt centralization after one turn to get rid of the IP cost and get
the slot for technocracy or industrialism. So far, we never keep it for more than one turn."""
turns_adopted = self._empire.turnsPoliciesAdopted
Expand Down Expand Up @@ -153,10 +272,13 @@ def _techno_or_industry(self) -> None:
)

def _determine_influence_priority(self, new_production: float) -> None:
# avoid wildly varying priorities while we adopt the basics: simply use 10 for the first 12 turns
if fo.currentTurn() <= 12:
"""Determine and set influence priority."""
# avoid wildly varying priorities while we adopt the basics: simply a fixed value for some turns
if fo.currentTurn() <= 15:
if fo.currentTurn() == 1:
self._set_priority(10.0, True)
# I'd prefer 20, but that makes Abadoni switch to Influence in turn 3.
# TBD work on ResourceAI again...
self._set_priority(18.0, True)
return
# How much IP would we have if we keep the current production for 3 turns?
forecast = self._ip + 3 * new_production
Expand Down Expand Up @@ -189,6 +311,7 @@ def _determine_influence_priority(self, new_production: float) -> None:
self._set_priority(priority, False)

def _set_priority(self, calculated_priority: float, ignore_old: bool) -> None:
"""Set and log influence priority."""
if ignore_old:
new_priority = calculated_priority
else:
Expand Down Expand Up @@ -287,4 +410,4 @@ def generate_policy_orders() -> None:


def print_status() -> None:
PolicyManager().print_status()
PolicyManager(True).print_status()
43 changes: 30 additions & 13 deletions default/python/AI/ResourcesAI.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,17 @@
GROWTH = FocusType.FOCUS_GROWTH
PROTECTION = FocusType.FOCUS_PROTECTION
INFLUENCE = FocusType.FOCUS_INFLUENCE
_focus_names = {
INDUSTRY: "Industry",
RESEARCH: "Research",
GROWTH: "Growth",
PROTECTION: "Defense",
INFLUENCE: "Influence",
}


def _focus_name(focus: str) -> str:
_known_names = {
INDUSTRY: "Industry",
RESEARCH: "Research",
GROWTH: "Growth",
PROTECTION: "Defense",
INFLUENCE: "Influence",
}
return _known_names.get(focus, focus)


class PlanetFocusInfo:
Expand Down Expand Up @@ -101,19 +105,23 @@ def bake_future_focus(self, pid, focus, update=True, force=False):
if (focus == INDUSTRY or focus == RESEARCH) and not force:
idx = 0 if focus == INDUSTRY else 1
# check for influence instead
debug("possible_output of %s: %s", pinfo.planet.name, str(pinfo.possible_output))
focus_gain = pinfo.possible_output[focus][idx] - pinfo.possible_output[INFLUENCE][idx]
influence_gain = pinfo.possible_output[INFLUENCE][2] - pinfo.possible_output[focus][2]
debug(
f"{pinfo.planet.name} current: {_focus_name(pinfo.current_focus)}, requested: {_focus_name(focus)}, "
f"requested_gain: {focus_gain}, influence_gain: {influence_gain}"
)
if influence_gain * self.priority[2] > focus_gain * self.priority[idx]:
debug(
f"Chosing influence over {_focus_names.get(focus, 'unknown')}."
f"Choosing influence over {_focus_name(focus)}."
f" {influence_gain:.2f} * {self.priority[2]:.1f}"
f" > {focus_gain:.2f} * {self.priority[idx]:.1f}"
)
focus = INFLUENCE
last_turn = fo.currentTurn()
if ( # define idx constants for accessing these tuples
focus != PROTECTION
and focus != pinfo.current_focus
and policy_is_adopted(bureaucracy)
and pinfo.current_output[2] + 0.2 < pinfo.possible_output[pinfo.current_focus][2]
and pinfo.planet.LastTurnColonized != last_turn
Expand Down Expand Up @@ -356,8 +364,8 @@ def print_table(self, priority_ratio):
"pID (%3d) %22s" % (pid, pinfo.planet.name[-22:]),
"c: %5.1f / %5.1f" % (curren_rp, current_pp),
"cT: %5.1f / %5.1f" % (ot_rp, ot_pp),
"cF: %8s" % _focus_names.get(old_focus, "unknown"),
"nF: %8s" % _focus_names.get(new_focus, "unset"),
"cF: %8s" % _focus_name(old_focus),
"nF: %8s" % _focus_name(new_focus),
"cT: %5.1f / %5.1f" % (nt_rp, nt_pp),
)
self.print_table_footer(priority_ratio)
Expand Down Expand Up @@ -438,6 +446,15 @@ def weighted_sum_output(op):
def assess_protection_focus(pinfo, priority):
"""Return True if planet should use Protection Focus."""
this_planet = pinfo.planet
# this is unrelated to military threats
stability_bonus = (pinfo.current_focus == PROTECTION) * fo.getNamedValue("PROTECION_FOCUS_STABILITY_BONUS")
# industry and research produce nothing below 0
threshold = -1 * (pinfo.current_focus not in (INDUSTRY, RESEARCH))
# Negative IP lowers stability. Trying to counter this by setting planets to Protection just makes it worse!
ip = fo.getEmpire().resourceAvailable(fo.resourceType.influence)
if ip >= 0 and this_planet.currentMeterValue(fo.meterType.targetHappiness) < threshold + stability_bonus:
debug("Advising Protection Focus at %s to avoid rebellion", this_planet)
return True
aistate = get_aistate()
sys_status = aistate.systemStatus.get(this_planet.systemID, {})
threat_from_supply = (
Expand Down Expand Up @@ -875,8 +892,8 @@ def set_planet_industry_research_influence_foci(focus_manager, priority_ratio):
c_pp,
ot_rp,
ot_pp,
_focus_names.get(old_focus, "unknown"),
_focus_names[RESEARCH],
_focus_name(old_focus),
_focus_name(RESEARCH),
nt_rp,
nt_pp,
ratio,
Expand Down
2 changes: 1 addition & 1 deletion default/scripting/species/common/planet_defense.macros
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ PROTECTION_FOCUS_DEFENSE
priority = [[TARGET_AFTER_2ND_SCALING_PRIORITY]]
effects = [
SetMaxDefense value = Value * 2
SetTargetHappiness value = Value + 15
SetTargetHappiness value = Value + (NamedReal name = "PROTECION_FOCUS_STABILITY_BONUS" value = 15.0)
]
'''

Expand Down
4 changes: 2 additions & 2 deletions default/stringtables/en.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6454,7 +6454,7 @@ PROTECTION_FOCUS_TITLE
Protection Focus

PROTECTION_FOCUS_TEXT
The Protection focus represents a planet wholly focused on defending itself. [[metertype METER_TROOPS]], Planetary [[metertype METER_DEFENSE]], and Planetary [[metertype METER_SHIELD]] are doubled.
The Protection focus represents a planet wholly focused on defending itself. [[metertype METER_TROOPS]], Planetary [[metertype METER_DEFENSE]], and Planetary [[metertype METER_SHIELD]] are doubled. [[metertype METER_TARGET_HAPPINESS]] is increased by [[value PROTECION_FOCUS_STABILITY_BONUS]].

PSYCHIC_FOCUS_TITLE
Psychic Domination Focus
Expand Down Expand Up @@ -10054,7 +10054,7 @@ FOCUS_PROTECTION
Protection

FOCUS_PROTECTION_DESC
Increases shields, defenses and troops.
Increases shields, defenses, troops and stability.

# Focus types
FOCUS_DOMINATION
Expand Down

0 comments on commit 3abfde4

Please sign in to comment.