diff --git a/android/build.gradle b/android/build.gradle index ed628be1d8..53a53a13da 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -21,8 +21,8 @@ android { applicationId "com.unciv.app" minSdkVersion 14 targetSdkVersion 28 - versionCode 266 - versionName "2.17.12-patch1" + versionCode 267 + versionName "2.17.13" } // Had to add this crap for Travis to build, it wanted to sign the app diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index d6c413ceef..1dbe0ded79 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -1,24 +1,14 @@ package com.unciv.logic.automation -import com.badlogic.gdx.graphics.Color -import com.unciv.Constants -import com.unciv.UnCivGame import com.unciv.logic.battle.CityCombatant -import com.unciv.logic.city.CityConstructions import com.unciv.logic.city.CityInfo -import com.unciv.logic.city.SpecialConstruction -import com.unciv.logic.civilization.CityAction import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.BFS import com.unciv.logic.map.TileInfo -import com.unciv.models.gamebasics.Building -import com.unciv.models.gamebasics.VictoryType import com.unciv.models.gamebasics.unit.BaseUnit import com.unciv.models.gamebasics.unit.UnitType -import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats import kotlin.math.max -import kotlin.math.min import kotlin.math.sqrt class Automation { @@ -84,192 +74,6 @@ class Automation { } - fun chooseNextConstruction(cityConstructions: CityConstructions) { - cityConstructions.run { - if(!UnCivGame.Current.settings.autoAssignCityProduction) return - if (getCurrentConstruction() !is SpecialConstruction) return // don't want to be stuck on these forever - - val buildableNotWonders = getBuildableBuildings().filterNot { it.isWonder || it.isNationalWonder } - val buildableWonders = getBuildableBuildings().filter { it.isWonder || it.isNationalWonder } - - val civUnits = cityInfo.civInfo.getCivUnits() - val militaryUnits = civUnits.filter { !it.type.isCivilian()}.size - val workers = civUnits.filter { it.name == Constants.worker }.size.toFloat() - val cities = cityInfo.civInfo.cities.size - val canBuildWorkboat = cityInfo.cityConstructions.getConstructableUnits().map { it.name }.contains("Work Boats") - && !cityInfo.getTiles().any { it.civilianUnit?.name == "Work Boats" } - val needWorkboat = canBuildWorkboat - && cityInfo.getTiles().any { it.isWater && it.hasViewableResource(cityInfo.civInfo) && it.improvement == null } - - val isAtWar = cityInfo.civInfo.isAtWar() - val preferredVictoryType = cityInfo.civInfo.victoryType() - - data class ConstructionChoice(val choice:String, var choiceModifier:Float){ - val remainingWork:Int = getRemainingWork(choice) - } - - val relativeCostEffectiveness = ArrayList() - - //Food buildings : Granary and lighthouse and hospital - val foodBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Food) } - .minBy{ it.cost } - if (foodBuilding!=null) { - val choice = ConstructionChoice(foodBuilding.name,1f) - if (cityInfo.population.population < 5) choice.choiceModifier=1.3f - relativeCostEffectiveness.add(choice) - } - - //Production buildings : Workshop, factory - val productionBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Production) } - .minBy{it.cost} - if (productionBuilding!=null) { - relativeCostEffectiveness.add(ConstructionChoice(productionBuilding.name, 1.5f)) - } - - //Gold buildings : Market, bank - val goldBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Gold) } - .minBy{it.cost} - if (goldBuilding!=null) { - val choice = ConstructionChoice(goldBuilding.name,1.2f) - if (cityInfo.civInfo.statsForNextTurn.gold<0) { - choice.choiceModifier=3f - } - relativeCostEffectiveness.add(choice) - } - - //Science buildings - val scienceBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Science) } - .minBy{it.cost} - if (scienceBuilding!=null) { - var modifier = 1.1f - if(preferredVictoryType==VictoryType.Scientific) - modifier*=1.4f - val choice = ConstructionChoice(scienceBuilding.name,modifier) - relativeCostEffectiveness.add(choice) - } - - //Happiness - val happinessBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Happiness) } - .minBy{it.cost} - if (happinessBuilding!=null) { - val choice = ConstructionChoice(happinessBuilding.name,1f) - val civHappiness = cityInfo.civInfo.getHappiness() - if (civHappiness > 5) choice.choiceModifier = 1/2f // less desperate - if (civHappiness < 0) choice.choiceModifier = 3f // more desperate - relativeCostEffectiveness.add(choice) - } - - //Defensive building - val defensiveBuilding = buildableNotWonders.filter { it.cityStrength>0 } - .minBy { it.cost } - if(defensiveBuilding!=null && (isAtWar || preferredVictoryType!=VictoryType.Cultural)){ - 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(cityInfo.civInfo.getKnownCivs() - .any{ NextTurnAutomation().getClosestCities(cityInfo.civInfo,it).city1 == cityInfo }) - modifier *= 1.5f - - relativeCostEffectiveness.add(ConstructionChoice(defensiveBuilding.name, modifier)) - } - - val unitTrainingBuilding = buildableNotWonders.filter { it.xpForNewUnits>0} - .minBy { it.cost } - //cityInfo.civInfo.cities.sortedByDescending { } // todo don't build if this is a weak production city - if (unitTrainingBuilding!=null && (preferredVictoryType!=VictoryType.Cultural || isAtWar)) { - var modifier = 0.5f - if(isAtWar) modifier = 1f - if(preferredVictoryType==VictoryType.Domination) - modifier *= 1.3f - relativeCostEffectiveness.add(ConstructionChoice(unitTrainingBuilding.name,modifier)) - } - - //Wonders - if (buildableWonders.isNotEmpty()) { - fun getWonderPriority(wonder: Building): Float { - if(preferredVictoryType==VictoryType.Cultural - && wonder.name in listOf("Sistine Chapel","Eiffel Tower","Cristo Redentor","Neuschwanstein","Sydney Opera House")) - return 3f - if(wonder.isStatRelated(Stat.Science)){ - if(preferredVictoryType==VictoryType.Scientific) return 1.5f - else return 1.3f - } - if(wonder.isStatRelated(Stat.Happiness)) return 1.2f - if(wonder.isStatRelated(Stat.Production)) return 1.1f - return 1f - } - val wondersByPriority = buildableWonders - .sortedByDescending { getWonderPriority(it) } - val wonder = wondersByPriority.first() - val citiesBuildingWonders = cityInfo.civInfo.cities - .count { it.cityConstructions.isBuildingWonder() } - - relativeCostEffectiveness.add(ConstructionChoice(wonder.name, - 3.5f * getWonderPriority(wonder) / (citiesBuildingWonders + 1))) - } - - // culture buildings - val cultureBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Culture) }.minBy { it.cost } - if(cultureBuilding!=null){ - var modifier = 0.8f - if(preferredVictoryType==VictoryType.Cultural) modifier =1.6f - relativeCostEffectiveness.add(ConstructionChoice(cultureBuilding.name, modifier)) - } - - //other buildings - val other = buildableNotWonders.minBy{it.cost} - if (other!=null) { - relativeCostEffectiveness.add(ConstructionChoice(other.name,0.8f)) - } - - //worker - val citiesCountedTowardsWorkers = min(5, cities) // above 5 cities, extra cities won't make us want more workers - see # - if (workers < citiesCountedTowardsWorkers * 0.6f) { - relativeCostEffectiveness.add(ConstructionChoice(Constants.worker,citiesCountedTowardsWorkers/(workers+0.1f))) - } - - //Work boat - if (needWorkboat) { - relativeCostEffectiveness.add(ConstructionChoice("Work Boats",0.6f)) - } - - //Army - if((!isAtWar && cityInfo.civInfo.statsForNextTurn.gold>0 && militaryUnits -50)) { - val militaryUnit = chooseMilitaryUnit(cityInfo) - val unitsToCitiesRatio = cities.toFloat() / (militaryUnits + 1) - // most buildings and civ units contribute the the civ's growth, military units are anti-growth - val militaryChoice = ConstructionChoice(militaryUnit, unitsToCitiesRatio / 2) - if (isAtWar) militaryChoice.choiceModifier = unitsToCitiesRatio * 2 - else if (preferredVictoryType == VictoryType.Domination) militaryChoice.choiceModifier = unitsToCitiesRatio * 1.5f - relativeCostEffectiveness.add(militaryChoice) - } - - val production = cityInfo.cityStats.currentCityStats.production - - val theChosenOne:String - if(relativeCostEffectiveness.isEmpty()){ // choose one of the special constructions instead - // add science! - if(SpecialConstruction.science.isBuildable(cityConstructions)) - theChosenOne="Science" - else if(SpecialConstruction.gold.isBuildable(cityConstructions)) - theChosenOne="Gold" - else theChosenOne = "Nothing" - } - else if(relativeCostEffectiveness.any { it.remainingWork < production*30 }) { - relativeCostEffectiveness.removeAll { it.remainingWork >= production * 30 } - theChosenOne = relativeCostEffectiveness.minBy { it.remainingWork/it.choiceModifier }!!.choice - } - // it's possible that this is a new city and EVERYTHING is way expensive - ignore modifiers, go for the cheapest. - // Nobody can plan 30 turns ahead, I don't care how cost-efficient you are. - else theChosenOne = relativeCostEffectiveness.minBy { it.remainingWork }!!.choice - - currentConstruction = theChosenOne - cityInfo.civInfo.addNotification("Work has started on [$currentConstruction]", Color.BROWN, CityAction(cityInfo.location)) - } - } - fun evaluteCombatStrength(civInfo: CivilizationInfo): Int { // Since units become exponentially stronger per combat strength increase, we square em all diff --git a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt new file mode 100644 index 0000000000..d901719416 --- /dev/null +++ b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt @@ -0,0 +1,232 @@ +package com.unciv.logic.automation + +import com.badlogic.gdx.graphics.Color +import com.unciv.Constants +import com.unciv.UnCivGame +import com.unciv.logic.city.CityConstructions +import com.unciv.logic.city.SpecialConstruction +import com.unciv.logic.civilization.CityAction +import com.unciv.models.gamebasics.Building +import com.unciv.models.gamebasics.VictoryType +import com.unciv.models.stats.Stat +import kotlin.math.min + +class ConstructionAutomation(val cityConstructions: CityConstructions){ + + val cityInfo = cityConstructions.cityInfo + val civInfo = cityInfo.civInfo + + val buildableNotWonders = cityConstructions.getBuildableBuildings().filterNot { it.isWonder || it.isNationalWonder } + val buildableWonders = cityConstructions.getBuildableBuildings().filter { it.isWonder || it.isNationalWonder } + + val civUnits = civInfo.getCivUnits() + val militaryUnits = civUnits.filter { !it.type.isCivilian()}.size + val workers = civUnits.filter { it.name == Constants.worker }.size.toFloat() + val cities = civInfo.cities.size + val canBuildWorkboat = cityInfo.cityConstructions.getConstructableUnits().map { it.name }.contains("Work Boats") + && !cityInfo.getTiles().any { it.civilianUnit?.name == "Work Boats" } + val needWorkboat = canBuildWorkboat + && cityInfo.getTiles().any { it.isWater && it.hasViewableResource(civInfo) && it.improvement == null } + + val isAtWar = civInfo.isAtWar() + val preferredVictoryType = civInfo.victoryType() + + val averageProduction = civInfo.cities.map { it.cityStats.currentCityStats.production }.average() + val cityIsOverAverageProduction = cityInfo.cityStats.currentCityStats.production >= averageProduction + + val relativeCostEffectiveness = ArrayList() + + data class ConstructionChoice(val choice:String, var choiceModifier:Float,val remainingWork:Int) + + fun addChoice(choices:ArrayList, choice:String, choiceModifier: Float){ + choices.add(ConstructionChoice(choice,choiceModifier,cityConstructions.getRemainingWork(choice))) + } + + fun chooseNextConstruction() { + if (!UnCivGame.Current.settings.autoAssignCityProduction) return + if (cityConstructions.getCurrentConstruction() !is SpecialConstruction) return // don't want to be stuck on these forever + + addFoodBuildingChoice() + addProductionBuildingChoice() + addGoldBuildingChoice() + addScienceBuildingChoice() + addHappinessBuildingChoice() + addDefenceBuildingChoice() + addUnitTrainingBuildingChoice() + addWondersChoice() + addCultureBuildingChoice() + addWorkerChoice() + addWorkBoatChoice() + addMilitaryUnitChoice() + + val production = cityInfo.cityStats.currentCityStats.production + + val theChosenOne: String + if (relativeCostEffectiveness.isEmpty()) { // choose one of the special constructions instead + // add science! + if (SpecialConstruction.science.isBuildable(cityConstructions)) + theChosenOne = "Science" + else if (SpecialConstruction.gold.isBuildable(cityConstructions)) + theChosenOne = "Gold" + else theChosenOne = "Nothing" + } else if (relativeCostEffectiveness.any { it.remainingWork < production * 30 }) { + relativeCostEffectiveness.removeAll { it.remainingWork >= production * 30 } + theChosenOne = relativeCostEffectiveness.minBy { it.remainingWork / it.choiceModifier }!!.choice + } + // it's possible that this is a new city and EVERYTHING is way expensive - ignore modifiers, go for the cheapest. + // Nobody can plan 30 turns ahead, I don't care how cost-efficient you are. + else theChosenOne = relativeCostEffectiveness.minBy { it.remainingWork }!!.choice + + civInfo.addNotification("Work has started on [$theChosenOne]", Color.BROWN, CityAction(cityInfo.location)) + cityConstructions.currentConstruction = theChosenOne + } + + private fun addMilitaryUnitChoice() { + if ((!isAtWar && civInfo.statsForNextTurn.gold > 0 && militaryUnits < cities * 2) + || (isAtWar && civInfo.gold > -50)) { + val militaryUnit = Automation().chooseMilitaryUnit(cityInfo) + val unitsToCitiesRatio = cities.toFloat() / (militaryUnits + 1) + // most buildings and civ units contribute the the civ's growth, military units are anti-growth + var modifier = unitsToCitiesRatio / 2 + if (preferredVictoryType == VictoryType.Domination) modifier *= 3 + else if (isAtWar) modifier *= unitsToCitiesRatio * 2 + if (!cityIsOverAverageProduction) modifier /= 5 // higher production cities will deal with this + + if (cityInfo.getCenterTile().civilianUnit?.name == Constants.settler + && cityInfo.getCenterTile().getTilesInDistance(5).none { it.militaryUnit?.civInfo == civInfo }) + modifier = 5f // there's a settler just sitting here, doing nothing - BAD + + addChoice(relativeCostEffectiveness, militaryUnit, modifier) + } + } + + private fun addWorkBoatChoice() { + if (needWorkboat) { + addChoice(relativeCostEffectiveness, "Work Boats", 0.6f) + } + } + + private fun addWorkerChoice() { + val citiesCountedTowardsWorkers = min(5, cities) // above 5 cities, extra cities won't make us want more workers + if (workers < citiesCountedTowardsWorkers * 0.6f) { + var modifier = citiesCountedTowardsWorkers / (workers + 0.1f) + if (!cityIsOverAverageProduction) modifier /= 5 // higher production cities will deal with this + addChoice(relativeCostEffectiveness, Constants.worker, modifier) + } + } + + private fun addCultureBuildingChoice() { + val cultureBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Culture) }.minBy { it.cost } + if (cultureBuilding != null) { + var modifier = 0.8f + if (preferredVictoryType == VictoryType.Cultural) modifier = 1.6f + addChoice(relativeCostEffectiveness, cultureBuilding.name, modifier) + } + } + + private fun addWondersChoice() { + if (buildableWonders.isNotEmpty()) { + fun getWonderPriority(wonder: Building): Float { + if (preferredVictoryType == VictoryType.Cultural + && wonder.name in listOf("Sistine Chapel", "Eiffel Tower", "Cristo Redentor", "Neuschwanstein", "Sydney Opera House")) + return 3f + if (wonder.isStatRelated(Stat.Science)) { + if (preferredVictoryType == VictoryType.Scientific) return 1.5f + else return 1.3f + } + if (wonder.isStatRelated(Stat.Happiness)) return 1.2f + if (wonder.isStatRelated(Stat.Production)) return 1.1f + return 1f + } + + val wondersByPriority = buildableWonders + .sortedByDescending { getWonderPriority(it) } + val wonder = wondersByPriority.first() + val citiesBuildingWonders = civInfo.cities + .count { it.cityConstructions.isBuildingWonder() } + + var modifier = 3.5f * getWonderPriority(wonder) / (citiesBuildingWonders + 1) + if (!cityIsOverAverageProduction) modifier /= 5 // higher production cities will deal with this + addChoice(relativeCostEffectiveness, wonder.name, modifier) + } + } + + private fun addUnitTrainingBuildingChoice() { + val unitTrainingBuilding = buildableNotWonders.filter { it.xpForNewUnits > 0 } + .minBy { 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 + if (preferredVictoryType == VictoryType.Domination) + modifier *= 1.3f + addChoice(relativeCostEffectiveness, unitTrainingBuilding.name, modifier) + } + } + + private fun addDefenceBuildingChoice() { + val defensiveBuilding = buildableNotWonders.filter { it.cityStrength > 0 } + .minBy { it.cost } + if (defensiveBuilding != null && (isAtWar || preferredVictoryType != VictoryType.Cultural)) { + 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().filter { it.cities.isNotEmpty() } + .any { NextTurnAutomation().getClosestCities(civInfo, it).city1 == cityInfo }) + modifier *= 1.5f + + addChoice(relativeCostEffectiveness, defensiveBuilding.name, modifier) + } + } + + private fun addHappinessBuildingChoice() { + val happinessBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Happiness) } + .minBy { it.cost } + if (happinessBuilding != null) { + var modifier = 1f + val civHappiness = civInfo.getHappiness() + if (civHappiness > 5) modifier = 1 / 2f // less desperate + if (civHappiness < 0) modifier = 3f // more desperate + addChoice(relativeCostEffectiveness, happinessBuilding.name, modifier) + } + } + + private fun addScienceBuildingChoice() { + val scienceBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Science) } + .minBy { it.cost } + if (scienceBuilding != null) { + var modifier = 1.1f + if (preferredVictoryType == VictoryType.Scientific) + modifier *= 1.4f + addChoice(relativeCostEffectiveness, scienceBuilding.name, modifier) + } + } + + private fun addGoldBuildingChoice() { + val goldBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Gold) } + .minBy { it.cost } + if (goldBuilding != null) { + val modifier = if (civInfo.statsForNextTurn.gold < 0) 3f else 1.2f + addChoice(relativeCostEffectiveness, goldBuilding.name, modifier) + } + } + + private fun addProductionBuildingChoice() { + val productionBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Production) } + .minBy { it.cost } + if (productionBuilding != null) { + addChoice(relativeCostEffectiveness, productionBuilding.name, 1.5f) + } + } + + private fun addFoodBuildingChoice() { + val foodBuilding = buildableNotWonders.filter { it.isStatRelated(Stat.Food) } + .minBy { it.cost } + if (foodBuilding != null) { + var modifier = 1f + if (cityInfo.population.population < 5) modifier = 1.3f + addChoice(relativeCostEffectiveness, foodBuilding.name, modifier) + } + } + +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/city/CityConstructions.kt b/core/src/com/unciv/logic/city/CityConstructions.kt index cb522d21c1..a6d9bb148b 100644 --- a/core/src/com/unciv/logic/city/CityConstructions.kt +++ b/core/src/com/unciv/logic/city/CityConstructions.kt @@ -2,7 +2,7 @@ package com.unciv.logic.city import com.badlogic.gdx.graphics.Color import com.unciv.Constants -import com.unciv.logic.automation.Automation +import com.unciv.logic.automation.ConstructionAutomation import com.unciv.models.gamebasics.Building import com.unciv.models.gamebasics.GameBasics import com.unciv.models.gamebasics.tr @@ -243,7 +243,7 @@ class CityConstructions { fun chooseNextConstruction() { if(currentConstructionIsUserSet) return - Automation().chooseNextConstruction(this) + ConstructionAutomation(this).chooseNextConstruction() } //endregion