AI focuses production of units on cities with high production, so the weaker ones can build infrastructure

Organized ChooseNextConstruction automation
This commit is contained in:
Yair Morgenstern 2019-07-03 00:09:50 +03:00
parent 273b06aa3a
commit e81844c053
4 changed files with 236 additions and 200 deletions

View File

@ -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

View File

@ -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<ConstructionChoice>()
//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<cities*2)
|| (isAtWar && cityInfo.civInfo.gold > -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

View File

@ -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<ConstructionChoice>()
data class ConstructionChoice(val choice:String, var choiceModifier:Float,val remainingWork:Int)
fun addChoice(choices:ArrayList<ConstructionChoice>, 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)
}
}
}

View File

@ -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