From c00ce49c867c2908f93476001880c918850e2092 Mon Sep 17 00:00:00 2001 From: SimonCeder <63475501+SimonCeder@users.noreply.github.com> Date: Wed, 6 Oct 2021 16:11:02 +0200 Subject: [PATCH] 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() --- .../jsons/Civ V - Vanilla/Buildings.json | 2 - core/src/com/unciv/logic/GameInfo.kt | 6 ++ .../com/unciv/logic/automation/Automation.kt | 81 ++++++++++++++++++- .../automation/ConstructionAutomation.kt | 37 ++++++--- .../logic/automation/NextTurnAutomation.kt | 34 ++++++++ .../unciv/logic/automation/UnitAutomation.kt | 5 ++ .../src/com/unciv/logic/city/IConstruction.kt | 3 + .../logic/civilization/CivilizationInfo.kt | 20 +++++ .../com/unciv/logic/trade/TradeEvaluation.kt | 5 ++ core/src/com/unciv/models/ruleset/Building.kt | 9 +++ .../com/unciv/models/ruleset/unit/BaseUnit.kt | 8 ++ 11 files changed, 191 insertions(+), 19 deletions(-) diff --git a/android/assets/jsons/Civ V - Vanilla/Buildings.json b/android/assets/jsons/Civ V - Vanilla/Buildings.json index 8ba86d1438..df852bb6a8 100644 --- a/android/assets/jsons/Civ V - Vanilla/Buildings.json +++ b/android/assets/jsons/Civ V - Vanilla/Buildings.json @@ -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 diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 5a961fce34..42c7da8e2d 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -77,6 +77,9 @@ class GameInfo { @Transient var simulateUntilWin = false + @Transient + var spaceResources = HashSet() + //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) } diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index 886f7a5f1e..48903ab921 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -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() diff --git a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt index ecf9ce807b..54781f9e18 100644 --- a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt @@ -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 diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt index c031655b9d..136512e2a3 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt @@ -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) diff --git a/core/src/com/unciv/logic/automation/UnitAutomation.kt b/core/src/com/unciv/logic/automation/UnitAutomation.kt index ba6d2782c7..212b2e2b97 100644 --- a/core/src/com/unciv/logic/automation/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/UnitAutomation.kt @@ -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 diff --git a/core/src/com/unciv/logic/city/IConstruction.kt b/core/src/com/unciv/logic/city/IConstruction.kt index e0238247c6..b0bfdc3b06 100644 --- a/core/src/com/unciv/logic/city/IConstruction.kt +++ b/core/src/com/unciv/logic/city/IConstruction.kt @@ -13,6 +13,7 @@ interface IConstruction : INamed { fun isBuildable(cityConstructions: CityConstructions): Boolean fun shouldBeDisplayed(cityConstructions: CityConstructions): Boolean fun getResourceRequirements(): HashMap + 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 = hashMapOf() + override fun requiresResource(resource: String) = false + } diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index 34c7e411bd..75f32b5aa7 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -106,6 +106,12 @@ class CivilizationInfo { @Transient var nonStandardTerrainDamage = false + @Transient + var lastEraResourceUsedForBuilding = HashMap() + + @Transient + val lastEraResourceUsedForUnit = HashMap() + 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() { diff --git a/core/src/com/unciv/logic/trade/TradeEvaluation.kt b/core/src/com/unciv/logic/trade/TradeEvaluation.kt index 78f5979af8..66e7c9fad6 100644 --- a/core/src/com/unciv/logic/trade/TradeEvaluation.kt +++ b/core/src/com/unciv/logic/trade/TradeEvaluation.kt @@ -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 diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index 175cd3cd0c..39aae35fec 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -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 + } } diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index 663ce7523c..d25027b20a 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -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()