mirror of
https://github.com/yairm210/Unciv.git
synced 2025-03-15 04:14:44 +07:00
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:
parent
33956673f5
commit
04196974a4
@ -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. */
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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 = {
|
||||
|
Loading…
Reference in New Issue
Block a user