Improve AI performance vs barbarians; AI settlers (#5562)

* AI more effective against barbarians

* Discourage settler death marches

* game speed

* optimization
This commit is contained in:
SimonCeder 2021-10-27 06:07:09 +02:00 committed by GitHub
parent 33956673f5
commit 04196974a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 20 deletions

View File

@ -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. */

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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