From 04196974a45c38a49bd5502f23835af442b34e68 Mon Sep 17 00:00:00 2001 From: SimonCeder <63475501+SimonCeder@users.noreply.github.com> Date: Wed, 27 Oct 2021 06:07:09 +0200 Subject: [PATCH] Improve AI performance vs barbarians; AI settlers (#5562) * AI more effective against barbarians * Discourage settler death marches * game speed * optimization --- .../com/unciv/logic/automation/Automation.kt | 41 +++++++++++++++++-- .../automation/ConstructionAutomation.kt | 5 ++- .../automation/SpecificUnitAutomation.kt | 35 +++++++++++----- .../unciv/logic/automation/UnitAutomation.kt | 38 +++++++++++++++-- core/src/com/unciv/logic/city/CityInfo.kt | 2 + core/src/com/unciv/logic/map/TileInfo.kt | 9 ++++ .../unciv/ui/worldscreen/unit/UnitActions.kt | 3 +- 7 files changed, 113 insertions(+), 20 deletions(-) diff --git a/core/src/com/unciv/logic/automation/Automation.kt b/core/src/com/unciv/logic/automation/Automation.kt index 9f4baddb20..f57106f27d 100644 --- a/core/src/com/unciv/logic/automation/Automation.kt +++ b/core/src/com/unciv/logic/automation/Automation.kt @@ -6,6 +6,7 @@ import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.map.BFS import com.unciv.logic.map.MapUnit import com.unciv.logic.map.TileInfo +import com.unciv.logic.map.TileMap import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.VictoryType import com.unciv.models.ruleset.tile.ResourceType @@ -118,14 +119,46 @@ object Automation { .distinct() .toList() if (availableTypes.isEmpty()) return null - val randomType = availableTypes.random() - chosenUnit = militaryUnits - .filter { it.unitType == randomType } - .maxByOrNull { it.cost }!! + val bestUnitsForType = availableTypes.map { type -> militaryUnits + .filter { unit -> unit.unitType == type } + .maxByOrNull { unit -> unit.cost }!! } + // 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() } return chosenUnit.name } + /** Determines whether [civInfo] should be allocating military to fending off barbarians */ + fun afraidOfBarbarians(civInfo: CivilizationInfo): Boolean { + if (civInfo.isCityState() || civInfo.isBarbarian()) + return false + + // If there are no barbarians we are not afraid + if (civInfo.gameInfo.gameParameters.noBarbarians) + return false + + val multiplier = if (civInfo.gameInfo.gameParameters.ragingBarbarians) 1.3f + else 1f // We're slightly more afraid of raging barbs + + // If it is late in the game we are not afraid + if (civInfo.gameInfo.turns > 120 * civInfo.gameInfo.gameParameters.gameSpeed.modifier * multiplier) + return false + + // If we have a lot of, or no cities we are not afraid + if (civInfo.cities.isEmpty() || civInfo.cities.count() >= 4 * multiplier) + return false + + // If we have vision of our entire starting continent (ish) we are not afraid + civInfo.gameInfo.tileMap.assignContinents(TileMap.AssignContinentsMode.Ensure) + val startingContinent = civInfo.getCapital().getCenterTile().getContinent() + if (civInfo.gameInfo.tileMap.continentSizes[startingContinent]!! < civInfo.viewableTiles.count()) + return false + + // Otherwise we're afraid + return true + } + /** Determines whether the AI should be willing to spend strategic resources to build * [construction] in [city], assumes that we are actually able to do so. */ diff --git a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt index 9d12eb064a..93e384bfbf 100644 --- a/core/src/com/unciv/logic/automation/ConstructionAutomation.kt +++ b/core/src/com/unciv/logic/automation/ConstructionAutomation.kt @@ -11,6 +11,7 @@ import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.VictoryType import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.stats.Stat +import kotlin.math.max import kotlin.math.min import kotlin.math.sqrt @@ -97,7 +98,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ private fun addMilitaryUnitChoice() { if (!isAtWar && !cityIsOverAverageProduction) return // don't make any military units here. Infrastructure first! - if ((!isAtWar && civInfo.statsForNextTurn.gold > 0 && militaryUnits < cities * 2) + if ((!isAtWar && civInfo.statsForNextTurn.gold > 0 && militaryUnits < max(5, cities * 2)) || (isAtWar && civInfo.gold > -50)) { val militaryUnit = Automation.chooseMilitaryUnit(cityInfo) if (militaryUnit == null) return @@ -106,6 +107,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions){ var modifier = sqrt(unitsToCitiesRatio) / 2 if (preferredVictoryType == VictoryType.Domination) modifier *= 3 else if (isAtWar) modifier *= unitsToCitiesRatio * 2 + + if (Automation.afraidOfBarbarians(civInfo)) modifier = 2f // military units are pro-growth if pressured by barbs if (!cityIsOverAverageProduction) modifier /= 5 // higher production cities will deal with this val civilianUnit = cityInfo.getCenterTile().civilianUnit diff --git a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt index 8a0e3e4376..83ea486b95 100644 --- a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt @@ -12,6 +12,8 @@ import com.unciv.models.ruleset.tile.TileResource import com.unciv.models.stats.Stat import com.unciv.models.stats.Stats import com.unciv.ui.worldscreen.unit.UnitActions +import kotlin.math.max +import kotlin.math.min object SpecificUnitAutomation { @@ -147,14 +149,6 @@ object SpecificUnitAutomation { } fun automateSettlerActions(unit: MapUnit) { - if (unit.civInfo.gameInfo.turns == 0) { // Special case, we want AI to settle in place on turn 1. - val foundCityAction = UnitActions.getFoundCityAction(unit, unit.getTile()) - if(foundCityAction?.action != null) { - foundCityAction.action.invoke() - return - } - } - if (unit.getTile().militaryUnit == null // Don't move until you're accompanied by a military unit && !unit.civInfo.isCityState() // ..unless you're a city state that was unable to settle its city on turn 1 && unit.getDamageFromTerrain() < unit.health) return // Also make sure we won't die waiting @@ -181,7 +175,11 @@ object SpecificUnitAutomation { val nearbyTileRankings = unit.getTile().getTilesInDistance(7) .associateBy({ it }, { Automation.rankTile(it, unit.civInfo) }) - val possibleCityLocations = unit.getTile().getTilesInDistance(5) + val distanceFromHome = if (unit.civInfo.cities.isEmpty()) 0 + else unit.civInfo.cities.minOf { it.getCenterTile().aerialDistanceTo(unit.getTile()) } + val range = max(1, min(5, 8 - distanceFromHome)) // Restrict vision when far from home to avoid death marches + + val possibleCityLocations = unit.getTile().getTilesInDistance(range) .filter { val tileOwner = it.getOwner() it.isLand && !it.isImpassible() && (tileOwner == null || tileOwner == unit.civInfo) // don't allow settler to settle inside other civ's territory @@ -194,6 +192,19 @@ object SpecificUnitAutomation { .map { it.tileResource }.filter { it.resourceType == ResourceType.Luxury } .distinct() + if (unit.civInfo.gameInfo.turns == 0) { // Special case, we want AI to settle in place on turn 1. + val foundCityAction = UnitActions.getFoundCityAction(unit, unit.getTile()) + // Depending on era and difficulty we might start with more than one settler. In that case settle the one with the best location + val otherSettlers = unit.civInfo.getCivUnits().filter { it.currentMovement > 0 && it.baseUnit == unit.baseUnit } + if(foundCityAction?.action != null && + otherSettlers.none { + rankTileAsCityCenter(it.getTile(), nearbyTileRankings, emptySequence()) > rankTileAsCityCenter(unit.getTile(), nearbyTileRankings, emptySequence()) + } ) { + foundCityAction.action.invoke() + return + } + } + val citiesByRanking = possibleCityLocations .map { Pair(it, rankTileAsCityCenter(it, nearbyTileRankings, luxuryResourcesInCivArea)) } .sortedByDescending { it.second }.toList() @@ -205,7 +216,11 @@ object SpecificUnitAutomation { return@firstOrNull pathSize in 1..3 }?.first - if (bestCityLocation == null) { // We got a badass over here, all tiles within 5 are taken? Screw it, random walk. + if (bestCityLocation == null) { // We got a badass over here, all tiles within 5 are taken? + // Try to move towards the frontier + val frontierCity = unit.civInfo.cities.maxByOrNull { it.getFrontierScore() } + if (frontierCity != null && frontierCity.getFrontierScore() > 0 && unit.movement.canReach(frontierCity.getCenterTile())) + unit.movement.headTowards(frontierCity.getCenterTile()) if (UnitAutomation.tryExplore(unit)) return // try to find new areas UnitAutomation.wander(unit) // go around aimlessly return diff --git a/core/src/com/unciv/logic/automation/UnitAutomation.kt b/core/src/com/unciv/logic/automation/UnitAutomation.kt index 1dff6386d7..3821673b90 100644 --- a/core/src/com/unciv/logic/automation/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/UnitAutomation.kt @@ -19,9 +19,9 @@ object UnitAutomation { return unit.movement.canMoveTo(tile) && (tile.getOwner() == null || !tile.getOwner()!!.isCityState()) && tile.neighbors.any { it.position !in unit.civInfo.exploredTiles } - && unit.movement.canReach(tile) - && (!unit.civInfo.isCityState() || tile.neighbors.any { it.getOwner() == unit.civInfo } // Don't want city-states exploring far outside their borders - && unit.getDamageFromTerrain(tile) <= 0) // Don't take unnecessary damage + && (!unit.civInfo.isCityState() || tile.neighbors.any { it.getOwner() == unit.civInfo }) // Don't want city-states exploring far outside their borders + && unit.getDamageFromTerrain(tile) <= 0 // Don't take unnecessary damage + && unit.movement.canReach(tile) // expensive, evaluate last } internal fun tryExplore(unit: MapUnit): Boolean { @@ -61,6 +61,36 @@ object UnitAutomation { return true } + // "Fog busting" is a strategy where you put your units slightly outside your borders to discourage barbarians from spawning + private fun tryFogBust(unit: MapUnit): Boolean { + if (!Automation.afraidOfBarbarians(unit.civInfo)) return false // Not if we're not afraid + + val reachableTilesThisTurn = + unit.movement.getDistanceToTiles().keys.filter { isGoodTileForFogBusting(unit, it) } + if (reachableTilesThisTurn.any()) { + unit.movement.headTowards(reachableTilesThisTurn.random()) // Just pick one + return true + } + + // Nothing immediate, lets look further. Number increases exponentially with distance - at 10 this took a looong time + for (tile in unit.currentTile.getTilesInDistance(5)) + if (isGoodTileForFogBusting(unit, tile)) { + unit.movement.headTowards(tile) + return true + } + return false + } + + private fun isGoodTileForFogBusting(unit: MapUnit, tile: TileInfo): Boolean { + return unit.movement.canMoveTo(tile) + && tile.getOwner() == null + && tile.neighbors.all { it.getOwner() == null } + && tile.position in unit.civInfo.exploredTiles + && tile.getTilesInDistance(2).any { it.getOwner() == unit.civInfo } + && unit.getDamageFromTerrain(tile) <= 0 + && unit.movement.canReach(tile) // expensive, evaluate last + } + @JvmStatic fun wander(unit: MapUnit, stayInTerritory: Boolean = false) { val unitDistanceToTiles = unit.movement.getDistanceToTiles() @@ -184,6 +214,8 @@ object UnitAutomation { // else, try to go to unreached tiles if (tryExplore(unit)) return + if (tryFogBust(unit)) return + // Idle CS units should wander so they don't obstruct players so much if (unit.civInfo.isCityState()) wander(unit, stayInTerritory = true) diff --git a/core/src/com/unciv/logic/city/CityInfo.kt b/core/src/com/unciv/logic/city/CityInfo.kt index 8783a969a1..3f3042ce8e 100644 --- a/core/src/com/unciv/logic/city/CityInfo.kt +++ b/core/src/com/unciv/logic/city/CityInfo.kt @@ -266,6 +266,8 @@ class CityInfo { fun isInResistance() = resistanceCounter > 0 + /** @return the number of tiles 4 out from this city that could hold a city, ie how lonely this city is */ + fun getFrontierScore() = getCenterTile().getTilesAtDistance(4).count { it.canBeSettled() && (it.getOwner() == null || it.getOwner() == civInfo ) } fun getRuleset() = civInfo.gameInfo.ruleSet diff --git a/core/src/com/unciv/logic/map/TileInfo.kt b/core/src/com/unciv/logic/map/TileInfo.kt index 5b649429ee..caaacb4460 100644 --- a/core/src/com/unciv/logic/map/TileInfo.kt +++ b/core/src/com/unciv/logic/map/TileInfo.kt @@ -576,6 +576,15 @@ open class TileInfo { return min(distance, wrappedDistance).toInt() } + fun canBeSettled(): Boolean { + if (isWater || isImpassible()) + return false + if (getTilesInDistance(2).any { it.isCityCenter() } || + getTilesAtDistance(3).any { it.isCityCenter() && it.getContinent() == getContinent() }) + return false + return true + } + /** Shows important properties of this tile for debugging _only_, it helps to see what you're doing */ override fun toString(): String { val lineList = arrayListOf("TileInfo @$position") diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index 7851871d18..e5abaed854 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -164,8 +164,7 @@ object UnitActions { if (!unit.hasUnique(UniqueType.FoundCity) || tile.isWater || tile.isImpassible()) return null if (unit.currentMovement <= 0 || - tile.getTilesInDistance(2).any { it.isCityCenter() } || - tile.getTilesAtDistance(3).any { it.isCityCenter() && it.getContinent() == tile.getContinent() }) + !tile.canBeSettled()) return UnitAction(UnitActionType.FoundCity, action = null) val foundAction = {