mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-12 00:39:56 +07:00
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:
@ -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
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
Reference in New Issue
Block a user