Construction automation optimization (#7504)

This commit is contained in:
OptimizedForDensity
2022-07-22 08:48:21 -04:00
committed by GitHub
parent 0e0e245365
commit 772e7faf54
5 changed files with 54 additions and 50 deletions

View File

@ -137,7 +137,7 @@ object Automation {
val currentChoice = city.cityConstructions.getCurrentConstruction() val currentChoice = city.cityConstructions.getCurrentConstruction()
if (currentChoice is BaseUnit && !currentChoice.isCivilian()) return city.cityConstructions.currentConstructionFromQueue if (currentChoice is BaseUnit && !currentChoice.isCivilian()) return city.cityConstructions.currentConstructionFromQueue
var militaryUnits = city.getRuleset().units.values.asSequence() var militaryUnits = city.getRuleset().units.values
.filter { !it.isCivilian() } .filter { !it.isCivilian() }
.filter { allowSpendingResource(city.civInfo, it) } .filter { allowSpendingResource(city.civInfo, it) }
@ -154,14 +154,14 @@ object Automation {
val carryingOnlyUnits = militaryUnits.filter { val carryingOnlyUnits = militaryUnits.filter {
it.hasUnique(UniqueType.CarryAirUnits) it.hasUnique(UniqueType.CarryAirUnits)
&& it.hasUnique(UniqueType.CannotAttack) && it.hasUnique(UniqueType.CannotAttack)
}.toList() }
for (unit in carryingOnlyUnits) for (unit in carryingOnlyUnits)
if (providesUnneededCarryingSlots(unit, city.civInfo)) if (providesUnneededCarryingSlots(unit, city.civInfo))
militaryUnits = militaryUnits.filterNot { it == unit } militaryUnits = militaryUnits.filterNot { it == unit }
// Only now do we filter out the constructable units because that's a heavier check // Only now do we filter out the constructable units because that's a heavier check
militaryUnits = militaryUnits.filter { it.isBuildable(city.cityConstructions) }.toList().asSequence() // gather once because we have a .any afterwards militaryUnits = militaryUnits.filter { it.isBuildable(city.cityConstructions) } // gather once because we have a .any afterwards
val chosenUnit: BaseUnit val chosenUnit: BaseUnit
if (!city.civInfo.isAtWar() if (!city.civInfo.isAtWar()
@ -183,7 +183,7 @@ object Automation {
} }
// Check the maximum force evaluation for the shortlist so we can prune useless ones (ie scouts) // Check the maximum force evaluation for the shortlist so we can prune useless ones (ie scouts)
val bestForce = bestUnitsForType.maxOf { it.getForceEvaluation() } val bestForce = bestUnitsForType.maxOf { it.getForceEvaluation() }
chosenUnit = bestUnitsForType.filter { it.uniqueTo != null || it.getForceEvaluation() > bestForce / 3 }.toList().random() chosenUnit = bestUnitsForType.filter { it.uniqueTo != null || it.getForceEvaluation() > bestForce / 3 }.random()
} }
return chosenUnit.name return chosenUnit.name
} }

View File

@ -1,11 +1,10 @@
package com.unciv.logic.automation package com.unciv.logic.automation
import com.unciv.UncivGame
import com.unciv.logic.city.CityConstructions import com.unciv.logic.city.CityConstructions
import com.unciv.logic.city.INonPerpetualConstruction
import com.unciv.logic.city.PerpetualConstruction import com.unciv.logic.city.PerpetualConstruction
import com.unciv.logic.civilization.CityAction import com.unciv.logic.civilization.CityAction
import com.unciv.logic.civilization.NotificationIcon import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.BFS import com.unciv.logic.map.BFS
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.MilestoneType import com.unciv.models.ruleset.MilestoneType
@ -22,13 +21,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private val cityInfo = cityConstructions.cityInfo private val cityInfo = cityConstructions.cityInfo
private val civInfo = cityInfo.civInfo private val civInfo = cityInfo.civInfo
private val buildableBuildings = cityConstructions.getBuildableBuildings().toList() private val buildings = cityInfo.getRuleset().buildings.values
private val buildableNotWonders = buildableBuildings private val nonWonders = buildings.filterNot { it.isAnyWonder() }
.filterNot { it.isAnyWonder() } private val wonders = buildings.filter { it.isAnyWonder() }
private val buildableWonders = buildableBuildings
.filter { it.isAnyWonder() }
private val buildableUnits = cityConstructions.getConstructableUnits() private val units = cityInfo.getRuleset().units.values
private val civUnits = civInfo.getCivUnits() private val civUnits = civInfo.getCivUnits()
private val militaryUnits = civUnits.count { it.baseUnit.isMilitary() } private val militaryUnits = civUnits.count { it.baseUnit.isMilitary() }
@ -58,6 +55,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
choices.add(ConstructionChoice(choice, choiceModifier, cityConstructions.getRemainingWork(choice))) choices.add(ConstructionChoice(choice, choiceModifier, cityConstructions.getRemainingWork(choice)))
} }
private fun Collection<INonPerpetualConstruction>.isBuildable(): Collection<INonPerpetualConstruction> {
return this.filter { it.isBuildable(cityConstructions) }
}
fun chooseNextConstruction() { fun chooseNextConstruction() {
if (cityConstructions.getCurrentConstruction() !is PerpetualConstruction) return // don't want to be stuck on these forever if (cityConstructions.getCurrentConstruction() !is PerpetualConstruction) return // don't want to be stuck on these forever
@ -140,11 +141,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addWorkBoatChoice() { private fun addWorkBoatChoice() {
val buildableWorkboatUnits = buildableUnits val buildableWorkboatUnits = units
.filter { .filter {
it.hasUnique(UniqueType.CreateWaterImprovements) it.hasUnique(UniqueType.CreateWaterImprovements)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
} }.isBuildable()
val alreadyHasWorkBoat = buildableWorkboatUnits.any() val alreadyHasWorkBoat = buildableWorkboatUnits.any()
&& !cityInfo.getTiles().any { && !cityInfo.getTiles().any {
it.civilianUnit?.hasUnique(UniqueType.CreateWaterImprovements) == true it.civilianUnit?.hasUnique(UniqueType.CreateWaterImprovements) == true
@ -172,11 +173,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addWorkerChoice() { private fun addWorkerChoice() {
val workerEquivalents = buildableUnits val workerEquivalents = units
.filter { .filter {
it.hasUnique(UniqueType.BuildImprovements) it.hasUnique(UniqueType.BuildImprovements)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
} }.isBuildable()
if (workerEquivalents.none()) return // for mods with no worker units if (workerEquivalents.none()) return // for mods with no worker units
if (workers < cities) { if (workers < cities) {
@ -187,9 +188,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addCultureBuildingChoice() { private fun addCultureBuildingChoice() {
val cultureBuilding = buildableNotWonders val cultureBuilding = nonWonders
.filter { it.isStatRelated(Stat.Culture) .filter { it.isStatRelated(Stat.Culture)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost } && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.isBuildable().minByOrNull { it.cost }
if (cultureBuilding != null) { if (cultureBuilding != null) {
var modifier = 0.5f var modifier = 0.5f
if (cityInfo.cityStats.currentCityStats.culture == 0f) // It won't grow if we don't help it if (cityInfo.cityStats.currentCityStats.culture == 0f) // It won't grow if we don't help it
@ -200,16 +202,17 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addSpaceshipPartChoice() { private fun addSpaceshipPartChoice() {
val spaceshipPart = (buildableNotWonders + buildableUnits).firstOrNull { it.name in spaceshipParts } val spaceshipPart = (nonWonders + units).filter { it.name in spaceshipParts }.isBuildable()
if (spaceshipPart != null) { if (spaceshipPart.isNotEmpty()) {
val modifier = 2f val modifier = 2f
addChoice(relativeCostEffectiveness, spaceshipPart.name, modifier) addChoice(relativeCostEffectiveness, spaceshipPart.first().name, modifier)
} }
} }
private fun addOtherBuildingChoice() { private fun addOtherBuildingChoice() {
val otherBuilding = buildableNotWonders val otherBuilding = nonWonders
.filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost } .filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.isBuildable().minByOrNull { it.cost }
if (otherBuilding != null) { if (otherBuilding != null) {
val modifier = 0.6f val modifier = 0.6f
addChoice(relativeCostEffectiveness, otherBuilding.name, modifier) addChoice(relativeCostEffectiveness, otherBuilding.name, modifier)
@ -246,25 +249,26 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addWondersChoice() { private fun addWondersChoice() {
if (!buildableWonders.any()) return if (!wonders.any()) return
val highestPriorityWonder = buildableWonders val highestPriorityWonder = wonders
.filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } .filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.maxByOrNull { getWonderPriority(it) } .isBuildable().maxByOrNull { getWonderPriority(it as Building) }
?: return ?: return
val citiesBuildingWonders = civInfo.cities val citiesBuildingWonders = civInfo.cities
.count { it.cityConstructions.isBuildingWonder() } .count { it.cityConstructions.isBuildingWonder() }
var modifier = 2f * getWonderPriority(highestPriorityWonder) / (citiesBuildingWonders + 1) var modifier = 2f * getWonderPriority(highestPriorityWonder as Building) / (citiesBuildingWonders + 1)
if (!cityIsOverAverageProduction) modifier /= 5 // higher production cities will deal with this if (!cityIsOverAverageProduction) modifier /= 5 // higher production cities will deal with this
addChoice(relativeCostEffectiveness, highestPriorityWonder.name, modifier) addChoice(relativeCostEffectiveness, highestPriorityWonder.name, modifier)
} }
private fun addUnitTrainingBuildingChoice() { private fun addUnitTrainingBuildingChoice() {
val unitTrainingBuilding = buildableNotWonders.asSequence() val unitTrainingBuilding = nonWonders
.filter { it.hasUnique(UniqueType.UnitStartingExperience) .filter { it.hasUnique(UniqueType.UnitStartingExperience)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost } && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.isBuildable().minByOrNull { it.cost }
if (unitTrainingBuilding != null && (!civInfo.wantsToFocusOn(Victory.Focus.Culture) || isAtWar)) { if (unitTrainingBuilding != null && (!civInfo.wantsToFocusOn(Victory.Focus.Culture) || isAtWar)) {
var modifier = if (cityIsOverAverageProduction) 0.5f else 0.1f // You shouldn't be cranking out units anytime soon var modifier = if (cityIsOverAverageProduction) 0.5f else 0.1f // You shouldn't be cranking out units anytime soon
if (isAtWar) modifier *= 2 if (isAtWar) modifier *= 2
@ -275,17 +279,17 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addDefenceBuildingChoice() { private fun addDefenceBuildingChoice() {
val defensiveBuilding = buildableNotWonders.asSequence() val defensiveBuilding = nonWonders
.filter { it.cityStrength > 0 .filter { it.cityStrength > 0
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it)}.minByOrNull { it.cost } && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)}
.isBuildable().minByOrNull { it.cost }
if (defensiveBuilding != null && (isAtWar || !civInfo.wantsToFocusOn(Victory.Focus.Culture))) { if (defensiveBuilding != null && (isAtWar || !civInfo.wantsToFocusOn(Victory.Focus.Culture))) {
var modifier = 0.2f var modifier = 0.2f
if (isAtWar) modifier = 0.5f if (isAtWar) modifier = 0.5f
// If this city is the closest city to another civ, that makes it a likely candidate for attack // If this city is the closest city to another civ, that makes it a likely candidate for attack
if (civInfo.getKnownCivs() if (civInfo.getKnownCivs()
.map { NextTurnAutomation.getClosestCities(civInfo, it) } .mapNotNull { NextTurnAutomation.getClosestCities(civInfo, it) }
.filterNotNull()
.any { it.city1 == cityInfo }) .any { it.city1 == cityInfo })
modifier *= 1.5f modifier *= 1.5f
@ -294,11 +298,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addHappinessBuildingChoice() { private fun addHappinessBuildingChoice() {
val happinessBuilding = buildableNotWonders.asSequence() val happinessBuilding = nonWonders
.filter { (it.isStatRelated(Stat.Happiness) .filter { (it.isStatRelated(Stat.Happiness)
|| it.uniques.contains("Remove extra unhappiness from annexed cities")) || it.uniques.contains("Remove extra unhappiness from annexed cities"))
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost } .isBuildable().minByOrNull { it.cost }
if (happinessBuilding != null) { if (happinessBuilding != null) {
var modifier = 1f var modifier = 1f
val civHappiness = civInfo.getHappiness() val civHappiness = civInfo.getHappiness()
@ -310,10 +314,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addScienceBuildingChoice() { private fun addScienceBuildingChoice() {
if (allTechsAreResearched) return if (allTechsAreResearched) return
val scienceBuilding = buildableNotWonders.asSequence() val scienceBuilding = nonWonders
.filter { it.isStatRelated(Stat.Science) .filter { it.isStatRelated(Stat.Science)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost } .isBuildable().minByOrNull { it.cost }
if (scienceBuilding != null) { if (scienceBuilding != null) {
var modifier = 1.1f var modifier = 1.1f
if (civInfo.wantsToFocusOn(Victory.Focus.Science)) if (civInfo.wantsToFocusOn(Victory.Focus.Science))
@ -323,9 +327,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addGoldBuildingChoice() { private fun addGoldBuildingChoice() {
val goldBuilding = buildableNotWonders.asSequence().filter { it.isStatRelated(Stat.Gold) val goldBuilding = nonWonders.filter { it.isStatRelated(Stat.Gold)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost } .isBuildable().minByOrNull { it.cost }
if (goldBuilding != null) { if (goldBuilding != null) {
val modifier = if (civInfo.statsForNextTurn.gold < 0) 3f else 1.2f val modifier = if (civInfo.statsForNextTurn.gold < 0) 3f else 1.2f
addChoice(relativeCostEffectiveness, goldBuilding.name, modifier) addChoice(relativeCostEffectiveness, goldBuilding.name, modifier)
@ -333,9 +337,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
} }
private fun addProductionBuildingChoice() { private fun addProductionBuildingChoice() {
val productionBuilding = buildableNotWonders.asSequence() val productionBuilding = nonWonders
.filter { it.isStatRelated(Stat.Production) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } .filter { it.isStatRelated(Stat.Production) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost } .isBuildable().minByOrNull { it.cost }
if (productionBuilding != null) { if (productionBuilding != null) {
addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f) addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f)
} }
@ -343,12 +347,12 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addFoodBuildingChoice() { private fun addFoodBuildingChoice() {
val conditionalState = StateForConditionals(civInfo, cityInfo) val conditionalState = StateForConditionals(civInfo, cityInfo)
val foodBuilding = buildableNotWonders.asSequence() val foodBuilding = nonWonders
.filter { .filter {
(it.isStatRelated(Stat.Food) (it.isStatRelated(Stat.Food)
|| it.hasUnique(UniqueType.CarryOverFood, conditionalState) || it.hasUnique(UniqueType.CarryOverFood, conditionalState)
) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) ) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
}.minByOrNull { it.cost } }.isBuildable().minByOrNull { it.cost }
if (foodBuilding != null) { if (foodBuilding != null) {
var modifier = 1f var modifier = 1f
if (cityInfo.population.population < 5) modifier = 1.3f if (cityInfo.population.population < 5) modifier = 1.3f
@ -370,12 +374,12 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
// The performance of the regular getMatchingUniques is better, since it only tries to find one unique, // The performance of the regular getMatchingUniques is better, since it only tries to find one unique,
// while the canBePurchasedWithStat tries (at time of writing) *6* different uniques. // while the canBePurchasedWithStat tries (at time of writing) *6* different uniques.
val missionary = buildableUnits val missionary = units
.firstOrNull { it -> it.getMatchingUniques("Can [] [] times").any { it.params[0] == "Spread Religion"} .firstOrNull { it -> it.getMatchingUniques("Can [] [] times").any { it.params[0] == "Spread Religion"}
&& it.canBePurchasedWithStat(cityInfo, Stat.Faith) } && it.canBePurchasedWithStat(cityInfo, Stat.Faith) }
val inquisitor = buildableUnits val inquisitor = units
.firstOrNull { it.hasUnique("Prevents spreading of religion to the city it is next to") .firstOrNull { it.hasUnique("Prevents spreading of religion to the city it is next to")
&& it.canBePurchasedWithStat(cityInfo, Stat.Faith) } && it.canBePurchasedWithStat(cityInfo, Stat.Faith) }
@ -395,8 +399,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val buildMissionary = possibleSpreadReligionTargets.toList().size.toFloat() / 15 + modifier val buildMissionary = possibleSpreadReligionTargets.toList().size.toFloat() / 15 + modifier
if (buildMissionary > buildInquisitor && missionary != null) faithConstruction.add(missionary) if (buildMissionary > buildInquisitor && missionary != null && missionary.isBuildable(cityConstructions)) faithConstruction.add(missionary)
else if(inquisitor != null) faithConstruction.add(inquisitor) else if (inquisitor != null && inquisitor.isBuildable(cityConstructions)) faithConstruction.add(inquisitor)
} }
} }

View File

@ -19,6 +19,7 @@ interface IConstruction : INamed {
} }
interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques { interface INonPerpetualConstruction : IConstruction, INamed, IHasUniques {
var cost: Int
val hurryCostModifier: Int val hurryCostModifier: Int
var requiredTech: String? var requiredTech: String?

View File

@ -31,8 +31,8 @@ import kotlin.math.pow
class Building : RulesetStatsObject(), INonPerpetualConstruction { class Building : RulesetStatsObject(), INonPerpetualConstruction {
override var requiredTech: String? = null override var requiredTech: String? = null
override var cost: Int = 0
var cost: Int = 0
var maintenance = 0 var maintenance = 0
private var percentStatBonus: Stats? = null private var percentStatBonus: Stats? = null
var specialistSlots: Counter<String> = Counter() var specialistSlots: Counter<String> = Counter()
@ -181,7 +181,6 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)): Stats { localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)): Stats {
// Calls the clone function of the NamedStats this class is derived from, not a clone function of this class // Calls the clone function of the NamedStats this class is derived from, not a clone function of this class
val stats = cloneStats() val stats = cloneStats()
val civInfo = city.civInfo
for (unique in localUniqueCache.get("StatsFromObject", city.getMatchingUniques(UniqueType.StatsFromObject))) { for (unique in localUniqueCache.get("StatsFromObject", city.getMatchingUniques(UniqueType.StatsFromObject))) {
if (!matchesFilter(unique.params[1])) continue if (!matchesFilter(unique.params[1])) continue

View File

@ -28,7 +28,7 @@ import kotlin.math.pow
in contrast to MapUnit, which is a specific unit of a certain type that appears on the map */ in contrast to MapUnit, which is a specific unit of a certain type that appears on the map */
class BaseUnit : RulesetObject(), INonPerpetualConstruction { class BaseUnit : RulesetObject(), INonPerpetualConstruction {
var cost: Int = 0 override var cost: Int = 0
override var hurryCostModifier: Int = 0 override var hurryCostModifier: Int = 0
var movement: Int = 0 var movement: Int = 0
var strength: Int = 0 var strength: Int = 0