diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index 22bb415052..b0d4ce4929 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -107,7 +107,9 @@ object Automation { fun tryTrainMilitaryUnit(city: City) { if (city.isPuppet) return - val chosenUnitName = chooseMilitaryUnit(city) + if ((city.cityConstructions.getCurrentConstruction() as? BaseUnit)?.isMilitary() == true) + return // already training a military unit + val chosenUnitName = chooseMilitaryUnit(city, city.civInfo.gameInfo.ruleSet.units.values.asSequence()) if (chosenUnitName != null) city.cityConstructions.currentConstructionFromQueue = chosenUnitName } @@ -133,57 +135,55 @@ object Automation { return totalCarriableUnits < totalCarryingSlots } - fun chooseMilitaryUnit(city: City): String? { + fun chooseMilitaryUnit(city: City, availableUnits: Sequence): String? { val currentChoice = city.cityConstructions.getCurrentConstruction() if (currentChoice is BaseUnit && !currentChoice.isCivilian()) return city.cityConstructions.currentConstructionFromQueue - var militaryUnits = city.getRuleset().units.values - .filter { !it.isCivilian() } - .filter { allowSpendingResource(city.civInfo, it) } - - val findWaterConnectedCitiesAndEnemies = - BFS(city.getCenterTile()) { it.isWater || it.isCityCenter() } - findWaterConnectedCitiesAndEnemies.stepToEnd() - if (findWaterConnectedCitiesAndEnemies.getReachedTiles().none { - (it.isCityCenter() && it.getOwner() != city.civInfo) - || (it.militaryUnit != null && it.militaryUnit!!.civInfo != city.civInfo) - }) // there is absolutely no reason for you to make water units on this body of water. - militaryUnits = militaryUnits.filter { !it.isWaterUnit() } - - - val carryingOnlyUnits = militaryUnits.filter { - it.hasUnique(UniqueType.CarryAirUnits) - && it.hasUnique(UniqueType.CannotAttack) + // if not coastal, removeShips == true so don't even consider ships + var removeShips = true + if (city.isCoastal()) { + // in the future this could be simplified by assigning every distinct non-lake body of + // water their own ID like a continent ID + val findWaterConnectedCitiesAndEnemies = + BFS(city.getCenterTile()) { it.isWater || it.isCityCenter() } + findWaterConnectedCitiesAndEnemies.stepToEnd() + removeShips = findWaterConnectedCitiesAndEnemies.getReachedTiles().none { + (it.isCityCenter() && it.getOwner() != city.civInfo) + || (it.militaryUnit != null && it.militaryUnit!!.civInfo != city.civInfo) + } // there is absolutely no reason for you to make water units on this body of water. } - 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) } // gather once because we have a .any afterwards + val militaryUnits = availableUnits + .filter { it.isMilitary() } + .filterNot { removeShips && it.isWaterUnit() } + .filter { allowSpendingResource(city.civInfo, it) } + .filterNot { + // filter out carrier-type units that can't attack if we don't need them + (it.hasUnique(UniqueType.CarryAirUnits) && it.hasUnique(UniqueType.CannotAttack)) + && providesUnneededCarryingSlots(it, city.civInfo) + } + // Only now do we filter out the constructable units because that's a heavier check + .filter { it.isBuildable(city.cityConstructions) } + .toList() val chosenUnit: BaseUnit if (!city.civInfo.isAtWar() - && city.civInfo.cities.any { it.getCenterTile().militaryUnit == null } - && militaryUnits.any { it.isRanged() } // this is for city defence so get a ranged unit if we can + && city.civInfo.cities.any { it.getCenterTile().militaryUnit == null } + && militaryUnits.any { it.isRanged() } // this is for city defence so get a ranged unit if we can ) { chosenUnit = militaryUnits .filter { it.isRanged() } .maxByOrNull { it.cost }!! } else { // randomize type of unit and take the most expensive of its kind - val availableTypes = militaryUnits - .map { it.unitType } - .distinct() - if (availableTypes.none()) return null - val bestUnitsForType = availableTypes.map { type -> - militaryUnits - .filter { unit -> unit.unitType == type } - .maxByOrNull { unit -> unit.cost }!! + val bestUnitsForType = hashMapOf() + for (unit in militaryUnits) { + if (bestUnitsForType[unit.unitType] == null || bestUnitsForType[unit.unitType]!!.cost < unit.cost) { + bestUnitsForType[unit.unitType] = unit + } } // 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 }.random() + val bestForce = bestUnitsForType.maxOfOrNull { it.value.getForceEvaluation() } ?: return null + chosenUnit = bestUnitsForType.filterValues { it.uniqueTo != null || it.getForceEvaluation() > bestForce / 3 }.values.random() } return chosenUnit.name } diff --git a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt index 5fe52006e5..5b27cd6c3c 100644 --- a/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/city/ConstructionAutomation.kt @@ -24,11 +24,17 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private val cityInfo = cityConstructions.city private val civInfo = cityInfo.civInfo - private val buildings = cityInfo.getRuleset().buildings.values + private val buildableBuildings = hashMapOf() + private val buildableUnits = hashMapOf() + private val buildings = cityInfo.getRuleset().buildings.values.asSequence() + private val nonWonders = buildings.filterNot { it.isAnyWonder() } + .filterNot { buildableBuildings[it.name] == false } // if we already know that this building can't be built here then don't even consider it + private val statBuildings = nonWonders.filter { !it.isEmpty() && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } private val wonders = buildings.filter { it.isAnyWonder() } - private val units = cityInfo.getRuleset().units.values + private val units = cityInfo.getRuleset().units.values.asSequence() + .filterNot { buildableUnits[it.name] == false } // if we already know that this unit can't be built here then don't even consider it private val civUnits = civInfo.units.getCivUnits() private val militaryUnits = civUnits.count { it.baseUnit.isMilitary() } @@ -57,8 +63,14 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ choices.add(ConstructionChoice(choice, choiceModifier, cityConstructions.getRemainingWork(choice))) } - private fun Collection.isBuildable(): Collection { - return this.filter { it.isBuildable(cityConstructions) } + private fun Sequence.filterBuildable(): Sequence { + return this.filter { + val cache = if (it is Building) buildableBuildings else buildableUnits + if (cache[it.name] == null) { + cache[it.name] = it.isBuildable(cityConstructions) + } + cache[it.name]!! + } } @@ -115,7 +127,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ if (!isAtWar && (civInfo.stats.statsForNextTurn.gold < 0 || militaryUnits > max(5, cities * 2))) return if (civInfo.gold < -50) return - val militaryUnit = Automation.chooseMilitaryUnit(cityInfo) ?: return + val militaryUnit = Automation.chooseMilitaryUnit(cityInfo, units) ?: return val unitsToCitiesRatio = cities.toFloat() / (militaryUnits + 1) // most buildings and civ units contribute the the civ's growth, military units are anti-growth var modifier = sqrt(unitsToCitiesRatio) / 2 @@ -137,8 +149,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val buildableWorkboatUnits = units .filter { it.hasUnique(UniqueType.CreateWaterImprovements) - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) - }.isBuildable() + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) + }.filterBuildable() val alreadyHasWorkBoat = buildableWorkboatUnits.any() && !cityInfo.getTiles().any { it.civilianUnit?.hasUnique(UniqueType.CreateWaterImprovements) == true @@ -153,7 +165,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ if (!bfs.getReachedTiles() .any { tile -> tile.hasViewableResource(civInfo) && tile.improvement == null && tile.getOwner() == civInfo - && tile.tileResource.getImprovements().any { + && tile.tileResource.getImprovements().any { tile.improvementFunctions.canBuildImprovement(tile.ruleset.tileImprovements[it]!!, civInfo) } } @@ -169,8 +181,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val workerEquivalents = units .filter { it.hasUnique(UniqueType.BuildImprovements) - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) - }.isBuildable() + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) + }.filterBuildable() if (workerEquivalents.none()) return // for mods with no worker units // For the first 3 cities, dedicate a worker, from then on only build another worker if you have 12 cities. @@ -184,10 +196,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ } private fun addCultureBuildingChoice() { - val cultureBuilding = nonWonders - .filter { it.isStatRelated(Stat.Culture) - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) - }.isBuildable() + val cultureBuilding = statBuildings + .filter { it.isStatRelated(Stat.Culture) } + .filterBuildable() .minByOrNull { it.cost } if (cultureBuilding != null) { var modifier = 0.5f @@ -199,17 +210,19 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ } private fun addSpaceshipPartChoice() { - val spaceshipPart = (nonWonders + units).filter { it.name in spaceshipParts }.isBuildable() - if (spaceshipPart.isNotEmpty()) { + if (!civInfo.hasUnique(UniqueType.EnablesConstructionOfSpaceshipParts)) return + val spaceshipPart = (nonWonders + units).filter { it.name in spaceshipParts }.filterBuildable().firstOrNull() + if (spaceshipPart != null) { val modifier = 2f - addChoice(relativeCostEffectiveness, spaceshipPart.first().name, modifier) + addChoice(relativeCostEffectiveness, spaceshipPart.name, modifier) } } private fun addOtherBuildingChoice() { val otherBuilding = nonWonders .filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable().minByOrNull { it.cost } + .filterBuildable() + .minByOrNull { it.cost } if (otherBuilding != null) { val modifier = 0.6f addChoice(relativeCostEffectiveness, otherBuilding.name, modifier) @@ -219,16 +232,16 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun getWonderPriority(wonder: Building): Float { // Only start building if we are the city that would complete it the soonest if (wonder.hasUnique(UniqueType.TriggersCulturalVictory) - && cityInfo == civInfo.cities.minByOrNull { - it.cityConstructions.turnsToConstruction(wonder.name) - }!! + && cityInfo == civInfo.cities.minByOrNull { + it.cityConstructions.turnsToConstruction(wonder.name) + }!! ) { return 10f } if (wonder.name in buildingsForVictory) return 5f if (civInfo.wantsToFocusOn(Victory.Focus.Culture) - // TODO: Moddability + // TODO: Moddability && wonder.name in listOf("Sistine Chapel", "Eiffel Tower", "Cristo Redentor", "Neuschwanstein", "Sydney Opera House")) return 3f if (wonder.isStatRelated(Stat.Science)) { @@ -236,7 +249,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ return if (civInfo.wantsToFocusOn(Victory.Focus.Science)) 1.5f else 1.3f } - if (wonder.name == "Manhattan Project") { + if (wonder.hasUnique(UniqueType.EnablesNuclearWeapons)) { return if (civInfo.wantsToFocusOn(Victory.Focus.Military)) 2f else 1.3f } @@ -250,7 +263,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val highestPriorityWonder = wonders .filter { Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable().maxByOrNull { getWonderPriority(it as Building) } + .filterBuildable() + .maxByOrNull { getWonderPriority(it as Building) } ?: return val citiesBuildingWonders = civInfo.cities @@ -265,7 +279,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val unitTrainingBuilding = nonWonders .filter { it.hasUnique(UniqueType.UnitStartingExperience) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) - }.isBuildable() + } + .filterBuildable() .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 @@ -280,7 +295,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ val defensiveBuilding = nonWonders .filter { it.cityStrength > 0 && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) - }.isBuildable() + } + .filterBuildable() .minByOrNull { it.cost } if (defensiveBuilding != null && (isAtWar || !civInfo.wantsToFocusOn(Victory.Focus.Culture))) { var modifier = 0.2f @@ -301,7 +317,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ .filter { (it.isStatRelated(Stat.Happiness) || it.hasUnique(UniqueType.RemoveAnnexUnhappiness)) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable() + .filterBuildable() .minByOrNull { it.cost } if (happinessBuilding != null) { var modifier = 1f @@ -315,10 +331,11 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addScienceBuildingChoice() { if (allTechsAreResearched) return - val scienceBuilding = nonWonders + val scienceBuilding = statBuildings .filter { it.isStatRelated(Stat.Science) - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable().minByOrNull { it.cost } + && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } + .filterBuildable() + .minByOrNull { it.cost } if (scienceBuilding != null) { var modifier = 1.1f if (civInfo.wantsToFocusOn(Victory.Focus.Science)) @@ -328,9 +345,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ } private fun addGoldBuildingChoice() { - val goldBuilding = nonWonders.filter { it.isStatRelated(Stat.Gold) - && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable().minByOrNull { it.cost } + val goldBuilding = statBuildings.filter { it.isStatRelated(Stat.Gold) } + .filterBuildable() + .minByOrNull { it.cost } if (goldBuilding != null) { val modifier = if (civInfo.stats.statsForNextTurn.gold < 0) 3f else 1.2f addChoice(relativeCostEffectiveness, goldBuilding.name, modifier) @@ -338,9 +355,10 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ } private fun addProductionBuildingChoice() { - val productionBuilding = nonWonders - .filter { it.isStatRelated(Stat.Production) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) } - .isBuildable().minByOrNull { it.cost } + val productionBuilding = statBuildings + .filter { it.isStatRelated(Stat.Production) } + .filterBuildable() + .minByOrNull { it.cost } if (productionBuilding != null) { addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f) } @@ -353,7 +371,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ (it.isStatRelated(Stat.Food) || it.hasUnique(UniqueType.CarryOverFood, conditionalState) ) && Automation.allowAutomatedConstruction(civInfo, cityInfo, it) - }.isBuildable().minByOrNull { it.cost } + }.filterBuildable().minByOrNull { it.cost } if (foodBuilding != null) { var modifier = 1f if (cityInfo.population.population < 5) modifier = 1.3f diff --git a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt index aad735290f..38e199bc14 100644 --- a/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/civilization/NextTurnAutomation.kt @@ -89,7 +89,7 @@ object NextTurnAutomation { chooseReligiousBeliefs(civInfo) } - reassignWorkedTiles(civInfo) // second most expensive + automateCities(civInfo) // second most expensive trainSettler(civInfo) tryVoteForDiplomaticVictory(civInfo) } @@ -892,7 +892,7 @@ object NextTurnAutomation { for (city in civInfo.cities) UnitAutomation.tryBombardEnemy(city) } - private fun reassignWorkedTiles(civInfo: Civilization) { + private fun automateCities(civInfo: Civilization) { for (city in civInfo.cities) { if (city.isPuppet && city.population.population > 9 && !city.isInResistance()) { @@ -901,9 +901,13 @@ object NextTurnAutomation { city.reassignAllPopulation() + if (city.health < city.getMaxHealth()) { + Automation.tryTrainMilitaryUnit(city) // need defenses if city is under attack + if (city.cityConstructions.constructionQueue.isNotEmpty()) + continue // found a unit to build so move on + } + city.cityConstructions.chooseNextConstruction() - if (city.health < city.getMaxHealth()) - Automation.tryTrainMilitaryUnit(city) // override previous decision if city is under attack } } diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 143176a378..0f5f54aca0 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -476,8 +476,8 @@ class Civilization : IsPartOfGameInfoSerialization { if (baseBuilding.replaces != null) return getEquivalentBuilding(baseBuilding.replaces!!) - for (building in gameInfo.ruleSet.buildings.values) - if (building.replaces == baseBuilding.name && building.uniqueTo == civName) + for (building in cache.uniqueBuildings) + if (building.replaces == baseBuilding.name) return building return baseBuilding } @@ -492,8 +492,8 @@ class Civilization : IsPartOfGameInfoSerialization { if (baseUnit.replaces != null) return getEquivalentUnit(baseUnit.replaces!!) // Equivalent of unique unit is the equivalent of the replaced unit - for (unit in gameInfo.ruleSet.units.values) - if (unit.replaces == baseUnit.name && unit.uniqueTo == civName) + for (unit in cache.uniqueUnits) + if (unit.replaces == baseUnit.name) return unit return baseUnit } diff --git a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt index 39ea1a28f1..ff3e4aa7ee 100644 --- a/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt +++ b/core/src/com/unciv/logic/civilization/transients/CivInfoTransientCache.kt @@ -9,12 +9,14 @@ import com.unciv.logic.civilization.PlayerType import com.unciv.logic.civilization.Proximity import com.unciv.logic.map.MapShape import com.unciv.logic.map.tile.Tile +import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.tile.ResourceSupplyList import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.UniqueTarget import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueType +import com.unciv.models.ruleset.unit.BaseUnit /** CivInfo class was getting too crowded */ class CivInfoTransientCache(val civInfo: Civilization) { @@ -25,6 +27,13 @@ class CivInfoTransientCache(val civInfo: Civilization) { @Transient val lastEraResourceUsedForUnit = java.util.HashMap() + /** Easy way to look up a Civilization's unique units and buildings */ + @Transient + val uniqueUnits = hashSetOf() + + @Transient + val uniqueBuildings = hashSetOf() + fun setTransients(){ val ruleset = civInfo.gameInfo.ruleSet for (resource in ruleset.tileResources.values.asSequence().filter { it.resourceType == ResourceType.Strategic }.map { it.name }) { @@ -39,6 +48,18 @@ class CivInfoTransientCache(val civInfo: Civilization) { if (lastEraForUnit != null) lastEraResourceUsedForUnit[resource] = lastEraForUnit } + + for (building in ruleset.buildings.values) { + if (building.uniqueTo == civInfo.civName) { + uniqueBuildings.add(building) + } + } + + for (unit in ruleset.units.values) { + if (unit.uniqueTo == civInfo.civName) { + uniqueUnits.add(unit) + } + } } fun updateSightAndResources() { diff --git a/core/src/com/unciv/models/ruleset/Building.kt b/core/src/com/unciv/models/ruleset/Building.kt index b859d211c7..47ed226c96 100644 --- a/core/src/com/unciv/models/ruleset/Building.kt +++ b/core/src/com/unciv/models/ruleset/Building.kt @@ -577,7 +577,7 @@ class Building : RulesetStatsObject(), INonPerpetualConstruction { if (uniqueTo != null && uniqueTo != civInfo.civName) yield(RejectionReasonType.UniqueToOtherNation.toInstance("Unique to $uniqueTo")) - if (civInfo.gameInfo.ruleSet.buildings.values.any { it.uniqueTo == civInfo.civName && it.replaces == name }) + if (civInfo.cache.uniqueBuildings.any { it.replaces == name }) yield(RejectionReasonType.ReplacedByOurUnique.toInstance()) if (requiredTech != null && !civInfo.tech.isResearched(requiredTech!!)) diff --git a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt index ed1949a629..d184f1c0f7 100644 --- a/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt +++ b/core/src/com/unciv/models/ruleset/unit/BaseUnit.kt @@ -147,7 +147,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction { if (uniqueTo != null && uniqueTo != civInfo.civName) yield(RejectionReasonType.UniqueToOtherNation.toInstance("Unique to $uniqueTo")) - if (ruleSet.units.values.any { it.uniqueTo == civInfo.civName && it.replaces == name }) + if (civInfo.cache.uniqueUnits.any { it.replaces == name }) yield(RejectionReasonType.ReplacedByOurUnique.toInstance("Our unique unit replaces this")) if (!civInfo.gameInfo.gameParameters.nuclearWeaponsEnabled && isNuclearWeapon())