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()
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 { allowSpendingResource(city.civInfo, it) }
@ -154,14 +154,14 @@ object Automation {
val carryingOnlyUnits = militaryUnits.filter {
it.hasUnique(UniqueType.CarryAirUnits)
&& it.hasUnique(UniqueType.CannotAttack)
}.toList()
}
for (unit in carryingOnlyUnits)
if (providesUnneededCarryingSlots(unit, city.civInfo))
militaryUnits = militaryUnits.filterNot { it == unit }
// 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
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)
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
}

View File

@ -1,11 +1,10 @@
package com.unciv.logic.automation
import com.unciv.UncivGame
import com.unciv.logic.city.CityConstructions
import com.unciv.logic.city.INonPerpetualConstruction
import com.unciv.logic.city.PerpetualConstruction
import com.unciv.logic.civilization.CityAction
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.PlayerType
import com.unciv.logic.map.BFS
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.MilestoneType
@ -22,13 +21,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private val cityInfo = cityConstructions.cityInfo
private val civInfo = cityInfo.civInfo
private val buildableBuildings = cityConstructions.getBuildableBuildings().toList()
private val buildableNotWonders = buildableBuildings
.filterNot { it.isAnyWonder() }
private val buildableWonders = buildableBuildings
.filter { it.isAnyWonder() }
private val buildings = cityInfo.getRuleset().buildings.values
private val nonWonders = buildings.filterNot { it.isAnyWonder() }
private val wonders = buildings.filter { it.isAnyWonder() }
private val buildableUnits = cityConstructions.getConstructableUnits()
private val units = cityInfo.getRuleset().units.values
private val civUnits = civInfo.getCivUnits()
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)))
}
private fun Collection<INonPerpetualConstruction>.isBuildable(): Collection<INonPerpetualConstruction> {
return this.filter { it.isBuildable(cityConstructions) }
}
fun chooseNextConstruction() {
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() {
val buildableWorkboatUnits = buildableUnits
val buildableWorkboatUnits = units
.filter {
it.hasUnique(UniqueType.CreateWaterImprovements)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
}
}.isBuildable()
val alreadyHasWorkBoat = buildableWorkboatUnits.any()
&& !cityInfo.getTiles().any {
it.civilianUnit?.hasUnique(UniqueType.CreateWaterImprovements) == true
@ -172,11 +173,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addWorkerChoice() {
val workerEquivalents = buildableUnits
val workerEquivalents = units
.filter {
it.hasUnique(UniqueType.BuildImprovements)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
}
}.isBuildable()
if (workerEquivalents.none()) return // for mods with no worker units
if (workers < cities) {
@ -187,9 +188,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addCultureBuildingChoice() {
val cultureBuilding = buildableNotWonders
val cultureBuilding = nonWonders
.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) {
var modifier = 0.5f
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() {
val spaceshipPart = (buildableNotWonders + buildableUnits).firstOrNull { it.name in spaceshipParts }
if (spaceshipPart != null) {
val spaceshipPart = (nonWonders + units).filter { it.name in spaceshipParts }.isBuildable()
if (spaceshipPart.isNotEmpty()) {
val modifier = 2f
addChoice(relativeCostEffectiveness, spaceshipPart.name, modifier)
addChoice(relativeCostEffectiveness, spaceshipPart.first().name, modifier)
}
}
private fun addOtherBuildingChoice() {
val otherBuilding = buildableNotWonders
.filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }.minByOrNull { it.cost }
val otherBuilding = nonWonders
.filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.isBuildable().minByOrNull { it.cost }
if (otherBuilding != null) {
val modifier = 0.6f
addChoice(relativeCostEffectiveness, otherBuilding.name, modifier)
@ -246,25 +249,26 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addWondersChoice() {
if (!buildableWonders.any()) return
if (!wonders.any()) return
val highestPriorityWonder = buildableWonders
val highestPriorityWonder = wonders
.filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.maxByOrNull { getWonderPriority(it) }
.isBuildable().maxByOrNull { getWonderPriority(it as Building) }
?: return
val citiesBuildingWonders = civInfo.cities
.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
addChoice(relativeCostEffectiveness, highestPriorityWonder.name, modifier)
}
private fun addUnitTrainingBuildingChoice() {
val unitTrainingBuilding = buildableNotWonders.asSequence()
val unitTrainingBuilding = nonWonders
.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)) {
var modifier = if (cityIsOverAverageProduction) 0.5f else 0.1f // You shouldn't be cranking out units anytime soon
if (isAtWar) modifier *= 2
@ -275,17 +279,17 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addDefenceBuildingChoice() {
val defensiveBuilding = buildableNotWonders.asSequence()
val defensiveBuilding = nonWonders
.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))) {
var modifier = 0.2f
if (isAtWar) modifier = 0.5f
// If this city is the closest city to another civ, that makes it a likely candidate for attack
if (civInfo.getKnownCivs()
.map { NextTurnAutomation.getClosestCities(civInfo, it) }
.filterNotNull()
.mapNotNull { NextTurnAutomation.getClosestCities(civInfo, it) }
.any { it.city1 == cityInfo })
modifier *= 1.5f
@ -294,11 +298,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addHappinessBuildingChoice() {
val happinessBuilding = buildableNotWonders.asSequence()
val happinessBuilding = nonWonders
.filter { (it.isStatRelated(Stat.Happiness)
|| it.uniques.contains("Remove extra unhappiness from annexed cities"))
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost }
.isBuildable().minByOrNull { it.cost }
if (happinessBuilding != null) {
var modifier = 1f
val civHappiness = civInfo.getHappiness()
@ -310,10 +314,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addScienceBuildingChoice() {
if (allTechsAreResearched) return
val scienceBuilding = buildableNotWonders.asSequence()
val scienceBuilding = nonWonders
.filter { it.isStatRelated(Stat.Science)
&& Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost }
.isBuildable().minByOrNull { it.cost }
if (scienceBuilding != null) {
var modifier = 1.1f
if (civInfo.wantsToFocusOn(Victory.Focus.Science))
@ -323,9 +327,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
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) }
.minByOrNull { it.cost }
.isBuildable().minByOrNull { it.cost }
if (goldBuilding != null) {
val modifier = if (civInfo.statsForNextTurn.gold < 0) 3f else 1.2f
addChoice(relativeCostEffectiveness, goldBuilding.name, modifier)
@ -333,9 +337,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
}
private fun addProductionBuildingChoice() {
val productionBuilding = buildableNotWonders.asSequence()
val productionBuilding = nonWonders
.filter { it.isStatRelated(Stat.Production) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) }
.minByOrNull { it.cost }
.isBuildable().minByOrNull { it.cost }
if (productionBuilding != null) {
addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f)
}
@ -343,12 +347,12 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
private fun addFoodBuildingChoice() {
val conditionalState = StateForConditionals(civInfo, cityInfo)
val foodBuilding = buildableNotWonders.asSequence()
val foodBuilding = nonWonders
.filter {
(it.isStatRelated(Stat.Food)
|| it.hasUnique(UniqueType.CarryOverFood, conditionalState)
) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it)
}.minByOrNull { it.cost }
}.isBuildable().minByOrNull { it.cost }
if (foodBuilding != null) {
var modifier = 1f
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,
// 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"}
&& 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")
&& it.canBePurchasedWithStat(cityInfo, Stat.Faith) }
@ -395,8 +399,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){
val buildMissionary = possibleSpreadReligionTargets.toList().size.toFloat() / 15 + modifier
if (buildMissionary > buildInquisitor && missionary != null) faithConstruction.add(missionary)
else if(inquisitor != null) faithConstruction.add(inquisitor)
if (buildMissionary > buildInquisitor && missionary != null && missionary.isBuildable(cityConstructions)) faithConstruction.add(missionary)
else if (inquisitor != null && inquisitor.isBuildable(cityConstructions)) faithConstruction.add(inquisitor)
}
}

View File

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

View File

@ -31,8 +31,8 @@ import kotlin.math.pow
class Building : RulesetStatsObject(), INonPerpetualConstruction {
override var requiredTech: String? = null
override var cost: Int = 0
var cost: Int = 0
var maintenance = 0
private var percentStatBonus: Stats? = null
var specialistSlots: Counter<String> = Counter()
@ -181,7 +181,6 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction {
localUniqueCache: LocalUniqueCache = LocalUniqueCache(false)): Stats {
// Calls the clone function of the NamedStats this class is derived from, not a clone function of this class
val stats = cloneStats()
val civInfo = city.civInfo
for (unique in localUniqueCache.get("StatsFromObject", city.getMatchingUniques(UniqueType.StatsFromObject))) {
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 */
class BaseUnit : RulesetObject(), INonPerpetualConstruction {
var cost: Int = 0
override var cost: Int = 0
override var hurryCostModifier: Int = 0
var movement: Int = 0
var strength: Int = 0