AI rationing of strategic resources; Hydro Plant re-enabled (#5401)

* AI evaluation of resources

* optimizations

* sell or disband when needed for space victory

* use for all constructions

* use in trade evaluations

* .requiresResource()
This commit is contained in:
SimonCeder
2021-10-06 16:11:02 +02:00
committed by GitHub
parent e4ff3d43d6
commit c00ce49c86
11 changed files with 191 additions and 19 deletions

View File

@ -913,7 +913,6 @@
"requiredBuilding": "Bank",
"requiredTech": "Electricity"
},
/* This works and even has icon but AI cannot manage its Aluminum at this moment
{
"name": "Hydro Plant",
"requiredResource": "Aluminum",
@ -922,7 +921,6 @@
"uniques": ["Must be on [River]","[+1 Production] from [River] tiles [in this city]"],
"requiredTech": "Electricity"
},
*/
// Modern Era

View File

@ -77,6 +77,9 @@ class GameInfo {
@Transient
var simulateUntilWin = false
@Transient
var spaceResources = HashSet<String>()
//endregion
//region Pure functions
@ -312,6 +315,9 @@ class GameInfo {
}
}
spaceResources.addAll(ruleSet.buildings.values.filter { it.hasUnique("Spaceship part") }
.flatMap { it.getResourceRequirements().keys } )
barbarians.setTransients(this)
}

View File

@ -1,9 +1,11 @@
package com.unciv.logic.automation
import com.unciv.logic.city.CityInfo
import com.unciv.logic.city.INonPerpetualConstruction
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.BFS
import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.unit.BaseUnit
@ -60,14 +62,11 @@ object Automation {
fun chooseMilitaryUnit(city: CityInfo): String? {
var militaryUnits =
city.cityConstructions.getConstructableUnits().filter { !it.isCivilian() }
.filter { allowSpendingResource(city.civInfo, it) }
if (militaryUnits.map { it.name }
.contains(city.cityConstructions.currentConstructionFromQueue))
return city.cityConstructions.currentConstructionFromQueue
// This is so that the AI doesn't use all its aluminum on units and have none left for spaceship parts
val aluminum = city.civInfo.getCivResourcesByName()["Aluminum"]
if (aluminum != null && aluminum < 2) // mods may have no aluminum
militaryUnits.filter { !it.getResourceRequirements().containsKey("Aluminum") }
val findWaterConnectedCitiesAndEnemies =
BFS(city.getCenterTile()) { it.isWater || it.isCityCenter() }
@ -100,6 +99,80 @@ object Automation {
return chosenUnit.name
}
/** Determines whether the AI should be willing to spend strategic resources to build
* [construction] in [city], assumes that we are actually able to do so. */
fun allowSpendingResource(civInfo: CivilizationInfo, construction: INonPerpetualConstruction): Boolean {
// City states do whatever they want
if (civInfo.isCityState())
return true
// Spaceships are always allowed
if (construction.hasUnique("Spaceship part"))
return true
val requiredResources = construction.getResourceRequirements()
// Does it even require any resources?
if (requiredResources.isEmpty())
return true
val civResources = civInfo.getCivResourcesByName()
// Rule of thumb: reserve 2-3 for spaceship, then reserve half each for buildings and units
// Assume that no buildings provide any resources
for ((resource, amount) in requiredResources) {
// Also count things under construction
var futureForUnits = 0
var futureForBuildings = 0
for (city in civInfo.cities) {
val otherConstruction = city.cityConstructions.getCurrentConstruction()
if (otherConstruction is Building)
futureForBuildings += otherConstruction.getResourceRequirements()[resource] ?: 0
else
futureForUnits += otherConstruction.getResourceRequirements()[resource] ?: 0
}
// Make sure we have some for space
if (resource in civInfo.gameInfo.spaceResources && civResources[resource]!! - amount - futureForBuildings - futureForUnits
< getReservedSpaceResourceAmount(civInfo)) {
return false
}
// Assume buildings remain useful
val neededForBuilding = civInfo.lastEraResourceUsedForBuilding[resource] != null
// Don't care about old units
val neededForUnits = civInfo.lastEraResourceUsedForUnit[resource] != null
&& civInfo.lastEraResourceUsedForUnit[resource]!! >= civInfo.getEraNumber()
// No need to save for both
if (!neededForBuilding || !neededForUnits) {
continue
}
val usedForUnits = civInfo.detailedCivResources.filter { it.resource.name == resource && it.origin == "Units" }.sumOf { -it.amount }
val usedForBuildings = civInfo.detailedCivResources.filter { it.resource.name == resource && it.origin == "Buildings" }.sumOf { -it.amount }
if (construction is Building) {
// Will more than half the total resources be used for buildings after this construction?
if (civResources[resource]!! + usedForUnits < usedForBuildings + amount + futureForBuildings) {
return false
}
} else {
// Will more than half the total resources be used for units after this construction?
if (civResources[resource]!! + usedForBuildings < usedForUnits + amount + futureForUnits) {
return false
}
}
}
return true
}
fun getReservedSpaceResourceAmount(civInfo: CivilizationInfo): Int {
return if (civInfo.nation.preferredVictoryType == VictoryType.Scientific) 3 else 2
}
fun threatAssessment(assessor: CivilizationInfo, assessed: CivilizationInfo): ThreatLevel {
val powerLevelComparison =
assessed.getStatForRanking(RankingType.Force) / assessor.getStatForRanking(RankingType.Force).toFloat()

View File

@ -115,7 +115,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addWorkBoatChoice() {
val buildableWorkboatUnits = cityInfo.cityConstructions.getConstructableUnits()
.filter { it.uniques.contains(Constants.workBoatsUnique) }
.filter { it.uniques.contains(Constants.workBoatsUnique)
&& Automation.allowSpendingResource(civInfo, it) }
val canBuildWorkboat = buildableWorkboatUnits.any()
&& !cityInfo.getTiles().any { it.civilianUnit?.hasUnique(Constants.workBoatsUnique) == true }
if (!canBuildWorkboat) return
@ -140,7 +141,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val workerEquivalents = civInfo.gameInfo.ruleSet.units.values
.filter { it.uniques.any {
unique -> unique.equalsPlaceholderText(Constants.canBuildImprovements)
} && it.isBuildable(cityConstructions) }
} && it.isBuildable(cityConstructions)
&& Automation.allowSpendingResource(civInfo, it) }
if (workerEquivalents.isEmpty()) return // for mods with no worker units
if (civInfo.getIdleUnits().any { it.isAutomated() && it.hasUniqueToBuildImprovements })
return // If we have automated workers who have no work to do then it's silly to construct new workers.
@ -155,7 +157,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addCultureBuildingChoice() {
val cultureBuilding = buildableNotWonders
.filter { it.isStatRelated(Stat.Culture) }.minByOrNull { it.cost }
.filter { it.isStatRelated(Stat.Culture)
&& Automation.allowSpendingResource(civInfo, it) }.minByOrNull { it.cost }
if (cultureBuilding != null) {
var modifier = 0.5f
if (cityInfo.cityStats.currentCityStats.culture == 0f) // It won't grow if we don't help it
@ -175,7 +178,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addOtherBuildingChoice() {
val otherBuilding = buildableNotWonders.minByOrNull { it.cost }
val otherBuilding = buildableNotWonders
.filter { Automation.allowSpendingResource(civInfo, it) }.minByOrNull { it.cost }
if (otherBuilding != null) {
val modifier = 0.6f
addChoice(relativeCostEffectiveness, otherBuilding.name, modifier)
@ -211,6 +215,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
if (!buildableWonders.any()) return
val highestPriorityWonder = buildableWonders
.filter { Automation.allowSpendingResource(civInfo, it) }
.maxByOrNull { getWonderPriority(it) }!!
val citiesBuildingWonders = civInfo.cities
.count { it.cityConstructions.isBuildingWonder() }
@ -222,7 +227,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addUnitTrainingBuildingChoice() {
val unitTrainingBuilding = buildableNotWonders.asSequence()
.filter { it.hasUnique("New [] units start with [] Experience []") }.minByOrNull { it.cost }
.filter { it.hasUnique("New [] units start with [] Experience []")
&& Automation.allowSpendingResource(civInfo, it)}.minByOrNull { it.cost }
if (unitTrainingBuilding != null && (preferredVictoryType != VictoryType.Cultural || isAtWar)) {
var modifier = if (cityIsOverAverageProduction) 0.5f else 0.1f // You shouldn't be cranking out units anytime soon
if (isAtWar) modifier *= 2
@ -234,7 +240,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addDefenceBuildingChoice() {
val defensiveBuilding = buildableNotWonders.asSequence()
.filter { it.cityStrength > 0 }.minByOrNull { it.cost }
.filter { it.cityStrength > 0
&& Automation.allowSpendingResource(civInfo, it)}.minByOrNull { it.cost }
if (defensiveBuilding != null && (isAtWar || preferredVictoryType != VictoryType.Cultural)) {
var modifier = 0.2f
if (isAtWar) modifier = 0.5f
@ -250,8 +257,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addHappinessBuildingChoice() {
val happinessBuilding = buildableNotWonders.asSequence()
.filter { it.isStatRelated(Stat.Happiness)
|| it.uniques.contains("Remove extra unhappiness from annexed cities") }
.filter { (it.isStatRelated(Stat.Happiness)
|| it.uniques.contains("Remove extra unhappiness from annexed cities"))
&& Automation.allowSpendingResource(civInfo, it)}
.minByOrNull { it.cost }
if (happinessBuilding != null) {
var modifier = 1f
@ -265,7 +273,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addScienceBuildingChoice() {
if (allTechsAreResearched) return
val scienceBuilding = buildableNotWonders.asSequence()
.filter { it.isStatRelated(Stat.Science) }
.filter { it.isStatRelated(Stat.Science)
&& Automation.allowSpendingResource(civInfo, it)}
.minByOrNull { it.cost }
if (scienceBuilding != null) {
var modifier = 1.1f
@ -276,7 +285,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addGoldBuildingChoice() {
val goldBuilding = buildableNotWonders.asSequence().filter { it.isStatRelated(Stat.Gold) }
val goldBuilding = buildableNotWonders.asSequence().filter { it.isStatRelated(Stat.Gold)
&& Automation.allowSpendingResource(civInfo, it)}
.minByOrNull { it.cost }
if (goldBuilding != null) {
val modifier = if (civInfo.statsForNextTurn.gold < 0) 3f else 1.2f
@ -286,7 +296,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addProductionBuildingChoice() {
val productionBuilding = buildableNotWonders.asSequence()
.filter { it.isStatRelated(Stat.Production) }
.filter { it.isStatRelated(Stat.Production) && Automation.allowSpendingResource(civInfo, it) }
.minByOrNull { it.cost }
if (productionBuilding != null) {
addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f)
@ -294,8 +304,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addFoodBuildingChoice() {
val foodBuilding = buildableNotWonders.asSequence().filter { it.isStatRelated(Stat.Food)
|| it.uniqueObjects.any { it.placeholderText=="[]% of food is carried over after population increases" }}
val foodBuilding = buildableNotWonders.asSequence().filter { (it.isStatRelated(Stat.Food)
|| it.uniqueObjects.any { it.placeholderText=="[]% of food is carried over after population increases" })
&& Automation.allowSpendingResource(civInfo, it) }
.minByOrNull { it.cost }
if (foodBuilding != null) {
var modifier = 1f

View File

@ -46,6 +46,7 @@ object NextTurnAutomation {
exchangeLuxuries(civInfo)
issueRequests(civInfo)
adoptPolicy(civInfo) // todo can take a second - why?
freeUpSpaceResources(civInfo)
} else {
civInfo.getFreeTechForCityState()
civInfo.updateDiplomaticRelationshipForCityState()
@ -300,6 +301,39 @@ object NextTurnAutomation {
}
}
/** If we are able to build a spaceship but have already spent our resources, try disbanding
* a unit and selling a building to make room. Can happen due to trades etc */
private fun freeUpSpaceResources(civInfo: CivilizationInfo) {
// Can't build spaceships
if (!civInfo.hasUnique("Enables construction of Spaceship parts"))
return
for (resource in civInfo.gameInfo.spaceResources) {
// Have enough resources already
if (civInfo.getCivResourcesByName()[resource]!! >= Automation.getReservedSpaceResourceAmount(civInfo))
continue
val unitToDisband = civInfo.getCivUnits()
.filter { it.baseUnit.requiresResource(resource) }
.minByOrNull { it.getForceEvaluation() }
if (unitToDisband != null) {
unitToDisband.disband()
}
for (city in civInfo.cities) {
if (city.hasSoldBuildingThisTurn)
continue
val buildingToSell = civInfo.gameInfo.ruleSet.buildings.values.filter {
it.name in city.cityConstructions.builtBuildings
&& it.requiresResource(resource) }.randomOrNull()
if (buildingToSell != null) {
city.sellBuilding(buildingToSell.name)
break
}
}
}
}
private fun chooseReligiousBeliefs(civInfo: CivilizationInfo) {
choosePantheon(civInfo)
foundReligion(civInfo)

View File

@ -78,6 +78,11 @@ object UnitAutomation {
val upgradedUnit = unit.getUnitToUpgradeTo()
if (!upgradedUnit.isBuildable(unit.civInfo)) return false // for resource reasons, usually
if (upgradedUnit.getResourceRequirements().keys.any { !unit.baseUnit.requiresResource(it) }) {
// The upgrade requires new resource types, so check if we are willing to invest them
if (!Automation.allowSpendingResource(unit.civInfo, upgradedUnit)) return false
}
val upgradeAction = UnitActions.getUpgradeAction(unit)
?: return false

View File

@ -13,6 +13,7 @@ interface IConstruction : INamed {
fun isBuildable(cityConstructions: CityConstructions): Boolean
fun shouldBeDisplayed(cityConstructions: CityConstructions): Boolean
fun getResourceRequirements(): HashMap<String,Int>
fun requiresResource(resource: String): Boolean
}
interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
@ -208,4 +209,6 @@ open class PerpetualConstruction(override var name: String, val description: Str
override fun getResourceRequirements(): HashMap<String, Int> = hashMapOf()
override fun requiresResource(resource: String) = false
}

View File

@ -106,6 +106,12 @@ class CivilizationInfo {
@Transient
var nonStandardTerrainDamage = false
@Transient
var lastEraResourceUsedForBuilding = HashMap<String, Int>()
@Transient
val lastEraResourceUsedForUnit = HashMap<String, Int>()
var playerType = PlayerType.AI
/** Used in online multiplayer for human players */
@ -681,6 +687,20 @@ class CivilizationInfo {
// Cache whether this civ gets nonstandard terrain damage for performance reasons.
nonStandardTerrainDamage = getMatchingUniques("Units ending their turn on [] tiles take [] damage")
.any { gameInfo.ruleSet.terrains[it.params[0]]!!.damagePerTurn != it.params[1].toInt() }
// Cache the last era each resource is used for buildings or units respectively for AI building evaluation
for (resource in gameInfo.ruleSet.tileResources.values.filter { it.resourceType == ResourceType.Strategic }.map { it.name }) {
val applicableBuildings = gameInfo.ruleSet.buildings.values.filter { getEquivalentBuilding(it) == it && it.requiresResource(resource) }
val applicableUnits = gameInfo.ruleSet.units.values.filter { getEquivalentUnit(it) == it && it.requiresResource(resource) }
val lastEraForBuilding = applicableBuildings.map { gameInfo.ruleSet.eras[gameInfo.ruleSet.technologies[it.requiredTech]?.era()]?.eraNumber ?: 0 }.maxOrNull()
val lastEraForUnit = applicableUnits.map { gameInfo.ruleSet.eras[gameInfo.ruleSet.technologies[it.requiredTech]?.era()]?.eraNumber ?: 0 }.maxOrNull()
if (lastEraForBuilding != null)
lastEraResourceUsedForBuilding[resource] = lastEraForBuilding
if (lastEraForUnit != null)
lastEraResourceUsedForUnit[resource] = lastEraForUnit
}
}
fun updateSightAndResources() {

View File

@ -191,6 +191,11 @@ class TradeEvaluation {
else 500 // you want to take away our last lux of this type?!
}
TradeType.Strategic_Resource -> {
if (civInfo.gameInfo.spaceResources.contains(offer.name) &&
(civInfo.hasUnique("Enables construction of Spaceship parts") ||
tradePartner.hasUnique("Enables construction of Spaceship parts")))
return 10000 // We'd rather win the game, thanks
if (!civInfo.isAtWar()) return 50 * offer.amount
val canUseForUnits = civInfo.gameInfo.ruleSet.units.values

View File

@ -690,6 +690,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
if (get(stat) > 0) return true
if (getStatPercentageBonuses(null)[stat] > 0) return true
if (uniqueObjects.any { it.placeholderText == "[] per [] population []" && it.stats[stat] > 0 }) return true
if (uniqueObjects.any { it.placeholderText == "[] from [] tiles []" && it.stats[stat] > 0 }) return true
return false
}
@ -710,4 +711,12 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
resourceRequirements[unique.params[1]] = unique.params[0].toInt()
return resourceRequirements
}
override fun requiresResource(resource: String): Boolean {
if (requiredResource == resource) return true
for (unique in getMatchingUniques(UniqueType.ConsumesResources)) {
if (unique.params[1] == resource) return true
}
return false
}
}

View File

@ -540,6 +540,14 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
return resourceRequirements
}
override fun requiresResource(resource: String): Boolean {
if (requiredResource == resource) return true
for (unique in getMatchingUniques(UniqueType.ConsumesResources)) {
if (unique.params[1] == resource) return true
}
return false
}
fun isRanged() = rangedStrength > 0
fun isMelee() = !isRanged() && strength > 0
fun isMilitary() = isRanged() || isMelee()