chore: Split civilian unit automation into separate file

Also limited missile automation to air units only, doesn't make sense for land-based self-destructing units to follow missile automation
This commit is contained in:
Yair Morgenstern 2023-10-04 22:56:14 +03:00
parent f4b7822728
commit 7503493d7d
3 changed files with 215 additions and 204 deletions

View File

@ -164,11 +164,9 @@ object AirUnitAutomation {
return
}
if (unit.baseUnit.isAirUnit()) {
val pathsToCities = unit.movement.getAerialPathsToCities()
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit)
} else UnitAutomation.tryHeadTowardsEnemyCity(unit)
val pathsToCities = unit.movement.getAerialPathsToCities()
if (pathsToCities.isEmpty()) return // can't actually move anywhere else
tryMoveToCitiesToAerialAttackFrom(pathsToCities, unit)
}
private fun tryRelocateToCitiesWithEnemyNearBy(unit: MapUnit): Boolean {

View File

@ -0,0 +1,181 @@
package com.unciv.logic.automation.unit
import com.unciv.Constants
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.managers.ReligionState
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
object CivilianUnitAutomation {
fun automateCivilianUnit(unit: MapUnit) {
if (tryRunAwayIfNeccessary(unit)) return
if (unit.currentTile.isCityCenter() && unit.currentTile.getCity()!!.isCapital()
&& !unit.hasUnique(UniqueType.AddInCapital)
&& unit.civ.units.getCivUnits().any { unit.hasUnique(UniqueType.AddInCapital) }){
// First off get out of the way, then decide if you actually want to do something else
val tilesCanMoveTo = unit.movement.getDistanceToTiles()
.filter { unit.movement.canMoveTo(it.key) }
if (tilesCanMoveTo.isNotEmpty())
unit.movement.moveToTile(tilesCanMoveTo.minByOrNull { it.value.totalDistance }!!.key)
}
val tilesWhereWeWillBeCaptured = unit.currentTile.getTilesInDistance(5)
.mapNotNull { it.militaryUnit }
.filter { it.civ.isAtWarWith(unit.civ) }
.flatMap { it.movement.getReachableTilesInCurrentTurn() }
.filter { it.militaryUnit?.civ != unit.civ }
.toSet()
if (unit.hasUnique(UniqueType.FoundCity))
return SpecificUnitAutomation.automateSettlerActions(unit, tilesWhereWeWillBeCaptured)
if (unit.cache.hasUniqueToBuildImprovements)
return unit.civ.getWorkerAutomation().automateWorkerAction(unit, tilesWhereWeWillBeCaptured)
if (unit.cache.hasUniqueToCreateWaterImprovements){
if (!unit.civ.getWorkerAutomation().automateWorkBoats(unit))
UnitAutomation.tryExplore(unit)
return
}
if (unit.hasUnique(UniqueType.MayFoundReligion)
&& unit.civ.religionManager.religionState < ReligionState.Religion
&& unit.civ.religionManager.mayFoundReligionAtAll()
)
return ReligiousUnitAutomation.foundReligion(unit)
if (unit.hasUnique(UniqueType.MayEnhanceReligion)
&& unit.civ.religionManager.religionState < ReligionState.EnhancedReligion
&& unit.civ.religionManager.mayEnhanceReligionAtAll(unit)
)
return ReligiousUnitAutomation.enhanceReligion(unit)
// We try to add any unit in the capital we can, though that might not always be desirable
// For now its a simple option to allow AI to win a science victory again
if (unit.hasUnique(UniqueType.AddInCapital))
return SpecificUnitAutomation.automateAddInCapital(unit)
//todo this now supports "Great General"-like mod units not combining 'aura' and citadel
// abilities, but not additional capabilities if automation finds no use for those two
if (unit.cache.hasStrengthBonusInRadiusUnique
&& SpecificUnitAutomation.automateGreatGeneral(unit))
return
if (unit.cache.hasCitadelPlacementUnique && SpecificUnitAutomation.automateCitadelPlacer(unit))
return
if (unit.cache.hasCitadelPlacementUnique || unit.cache.hasStrengthBonusInRadiusUnique)
return SpecificUnitAutomation.automateGreatGeneralFallback(unit)
if (unit.civ.religionManager.maySpreadReligionAtAll(unit))
return ReligiousUnitAutomation.automateMissionary(unit)
if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoLimitedAction(Constants.removeHeresy))
return ReligiousUnitAutomation.automateInquisitor(unit)
val isLateGame = isLateGame(unit.civ)
// Great scientist -> Hurry research if late game
if (isLateGame) {
val hurriedResearch = UnitActions.invokeUnitAction(unit, UnitActionType.HurryResearch)
if (hurriedResearch) return
}
// Great merchant -> Conduct trade mission if late game and if not at war.
// TODO: This could be more complex to walk to the city state that is most beneficial to
// also have more influence.
if (unit.hasUnique(UniqueType.CanTradeWithCityStateForGoldAndInfluence)
// Don't wander around with the great merchant when at war. Barbs might also be a
// problem, but hopefully by the time we have a great merchant, they're under control.
&& !unit.civ.isAtWar()
&& isLateGame
) {
val tradeMissionCanBeConductedEventually =
SpecificUnitAutomation.conductTradeMission(unit)
if (tradeMissionCanBeConductedEventually)
return
}
// Great engineer -> Try to speed up wonder construction if late game
if (isLateGame &&
(unit.hasUnique(UniqueType.CanSpeedupConstruction)
|| unit.hasUnique(UniqueType.CanSpeedupWonderConstruction))) {
val wonderCanBeSpedUpEventually = SpecificUnitAutomation.speedupWonderConstruction(unit)
if (wonderCanBeSpedUpEventually)
return
}
// This has to come after the individual abilities for the great people that can also place
// instant improvements (e.g. great scientist).
if (unit.hasUnique(UniqueType.ConstructImprovementInstantly)) {
// catch great prophet for civs who can't found/enhance/spread religion
// includes great people plus moddable units
val improvementCanBePlacedEventually =
SpecificUnitAutomation.automateImprovementPlacer(unit)
if (!improvementCanBePlacedEventually)
UnitActions.invokeUnitAction(unit, UnitActionType.StartGoldenAge)
}
// TODO: The AI tends to have a lot of great generals. Maybe there should be a cutoff
// (depending on number of cities) and after that they should just be used to start golden
// ages?
return // The AI doesn't know how to handle unknown civilian units
}
private fun isLateGame(civ: Civilization): Boolean {
val researchCompletePercent =
(civ.tech.researchedTechnologies.size * 1.0f) / civ.gameInfo.ruleset.technologies.size
return researchCompletePercent >= 0.8f
}
/** Returns whether the civilian spends its turn hiding and not moving */
private fun tryRunAwayIfNeccessary(unit: MapUnit): Boolean {
// This is a little 'Bugblatter Beast of Traal': Run if we can attack an enemy
// Cheaper than determining which enemies could attack us next turn
val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys
.filter { UnitAutomation.containsEnemyMilitaryUnit(unit, it) }
if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary()
&& unit.getTile().militaryUnit == null && !unit.getTile().isCityCenter()) {
runAway(unit)
return true
}
return false
}
private fun runAway(unit: MapUnit) {
val reachableTiles = unit.movement.getDistanceToTiles()
val enterableCity = reachableTiles.keys
.firstOrNull { it.isCityCenter() && unit.movement.canMoveTo(it) }
if (enterableCity != null) {
unit.movement.moveToTile(enterableCity)
return
}
val defensiveUnit = reachableTiles.keys
.firstOrNull {
it.militaryUnit != null && it.militaryUnit!!.civ == unit.civ && it.civilianUnit == null
}
if (defensiveUnit != null) {
unit.movement.moveToTile(defensiveUnit)
return
}
val tileFurthestFromEnemy = reachableTiles.keys
.filter { unit.movement.canMoveTo(it) && unit.getDamageFromTerrain(it) < unit.health }
.maxByOrNull { countDistanceToClosestEnemy(unit, it) }
?: return // can't move anywhere!
unit.movement.moveToTile(tileFurthestFromEnemy)
}
private fun countDistanceToClosestEnemy(unit: MapUnit, tile: Tile): Int {
for (i in 1..3)
if (tile.getTilesAtDistance(i).any { UnitAutomation.containsEnemyMilitaryUnit(unit, it) })
return i
return 4
}
}

View File

@ -11,15 +11,11 @@ import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.battle.TargetHelper
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.managers.ReligionState
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
@ -155,7 +151,7 @@ object UnitAutomation {
if (unit.isCivilian()) {
automateCivilianUnit(unit)
CivilianUnitAutomation.automateCivilianUnit(unit)
return
}
@ -174,7 +170,7 @@ object UnitAutomation {
if ((unit.hasUnique(UniqueType.BuildImprovements) || unit.hasUnique(UniqueType.FoundCity) ||
unit.hasUnique(UniqueType.ReligiousUnit) || unit.hasUnique(UniqueType.CreateWaterImprovements))
&& !unit.civ.isAtWar()){
automateCivilianUnit(unit)
CivilianUnitAutomation.automateCivilianUnit(unit)
return
}
@ -187,7 +183,7 @@ object UnitAutomation {
if (unit.baseUnit.isNuclearWeapon())
return AirUnitAutomation.automateNukes(unit)
if (unit.hasUnique(UniqueType.SelfDestructs))
if (unit.baseUnit.isAirUnit() && unit.hasUnique(UniqueType.SelfDestructs))
return AirUnitAutomation.automateMissile(unit)
if (tryGoToRuinAndEncampment(unit) && unit.currentMovement == 0f) return
@ -235,127 +231,6 @@ object UnitAutomation {
wander(unit, stayInTerritory = true)
}
private fun automateCivilianUnit(unit: MapUnit) {
if (tryRunAwayIfNeccessary(unit)) return
if (unit.currentTile.isCityCenter() && unit.currentTile.getCity()!!.isCapital()
&& !unit.hasUnique(UniqueType.AddInCapital)
&& unit.civ.units.getCivUnits().any { unit.hasUnique(UniqueType.AddInCapital) }){
// First off get out of the way, then decide if you actually want to do something else
val tilesCanMoveTo = unit.movement.getDistanceToTiles()
.filter { unit.movement.canMoveTo(it.key) }
if (tilesCanMoveTo.isNotEmpty())
unit.movement.moveToTile(tilesCanMoveTo.minByOrNull { it.value.totalDistance }!!.key)
}
val tilesWhereWeWillBeCaptured = unit.currentTile.getTilesInDistance(5)
.mapNotNull { it.militaryUnit }
.filter { it.civ.isAtWarWith(unit.civ) }
.flatMap { it.movement.getReachableTilesInCurrentTurn() }
.filter { it.militaryUnit?.civ != unit.civ }
.toSet()
if (unit.hasUnique(UniqueType.FoundCity))
return SpecificUnitAutomation.automateSettlerActions(unit, tilesWhereWeWillBeCaptured)
if (unit.cache.hasUniqueToBuildImprovements)
return unit.civ.getWorkerAutomation().automateWorkerAction(unit, tilesWhereWeWillBeCaptured)
if (unit.cache.hasUniqueToCreateWaterImprovements){
if (!unit.civ.getWorkerAutomation().automateWorkBoats(unit))
tryExplore(unit)
return
}
if (unit.hasUnique(UniqueType.MayFoundReligion)
&& unit.civ.religionManager.religionState < ReligionState.Religion
&& unit.civ.religionManager.mayFoundReligionAtAll()
)
return ReligiousUnitAutomation.foundReligion(unit)
if (unit.hasUnique(UniqueType.MayEnhanceReligion)
&& unit.civ.religionManager.religionState < ReligionState.EnhancedReligion
&& unit.civ.religionManager.mayEnhanceReligionAtAll(unit)
)
return ReligiousUnitAutomation.enhanceReligion(unit)
// We try to add any unit in the capital we can, though that might not always be desirable
// For now its a simple option to allow AI to win a science victory again
if (unit.hasUnique(UniqueType.AddInCapital))
return SpecificUnitAutomation.automateAddInCapital(unit)
//todo this now supports "Great General"-like mod units not combining 'aura' and citadel
// abilities, but not additional capabilities if automation finds no use for those two
if (unit.cache.hasStrengthBonusInRadiusUnique
&& SpecificUnitAutomation.automateGreatGeneral(unit))
return
if (unit.cache.hasCitadelPlacementUnique && SpecificUnitAutomation.automateCitadelPlacer(unit))
return
if (unit.cache.hasCitadelPlacementUnique || unit.cache.hasStrengthBonusInRadiusUnique)
return SpecificUnitAutomation.automateGreatGeneralFallback(unit)
if (unit.civ.religionManager.maySpreadReligionAtAll(unit))
return ReligiousUnitAutomation.automateMissionary(unit)
if (unit.hasUnique(UniqueType.PreventSpreadingReligion) || unit.canDoLimitedAction(Constants.removeHeresy))
return ReligiousUnitAutomation.automateInquisitor(unit)
val isLateGame = isLateGame(unit.civ)
// Great scientist -> Hurry research if late game
if (isLateGame) {
val hurriedResearch = UnitActions.invokeUnitAction(unit, UnitActionType.HurryResearch)
if (hurriedResearch) return
}
// Great merchant -> Conduct trade mission if late game and if not at war.
// TODO: This could be more complex to walk to the city state that is most beneficial to
// also have more influence.
if (unit.hasUnique(UniqueType.CanTradeWithCityStateForGoldAndInfluence)
// Don't wander around with the great merchant when at war. Barbs might also be a
// problem, but hopefully by the time we have a great merchant, they're under control.
&& !unit.civ.isAtWar()
&& isLateGame
) {
val tradeMissionCanBeConductedEventually =
SpecificUnitAutomation.conductTradeMission(unit)
if (tradeMissionCanBeConductedEventually)
return
}
// Great engineer -> Try to speed up wonder construction if late game
if (isLateGame &&
(unit.hasUnique(UniqueType.CanSpeedupConstruction)
|| unit.hasUnique(UniqueType.CanSpeedupWonderConstruction))) {
val wonderCanBeSpedUpEventually = SpecificUnitAutomation.speedupWonderConstruction(unit)
if (wonderCanBeSpedUpEventually)
return
}
// This has to come after the individual abilities for the great people that can also place
// instant improvements (e.g. great scientist).
if (unit.hasUnique(UniqueType.ConstructImprovementInstantly)
) {
// catch great prophet for civs who can't found/enhance/spread religion
// includes great people plus moddable units
val improvementCanBePlacedEventually =
SpecificUnitAutomation.automateImprovementPlacer(unit)
if (!improvementCanBePlacedEventually)
UnitActions.invokeUnitAction(unit, UnitActionType.StartGoldenAge)
}
// TODO: The AI tends to have a lot of great generals. Maybe there should be a cutoff
// (depending on number of cities) and after that they should just be used to start golden
// ages?
return // The AI doesn't know how to handle unknown civilian units
}
private fun isLateGame(civ: Civilization): Boolean {
val researchCompletePercent =
(civ.tech.researchedTechnologies.size * 1.0f) / civ.gameInfo.ruleset.technologies.size
return researchCompletePercent >= 0.8f
}
/** @return true only if the unit has 0 movement left */
private fun tryAttacking(unit: MapUnit): Boolean {
@ -389,21 +264,7 @@ object UnitAutomation {
val currentUnitTile = unit.getTile()
val nearbyRangedEnemyUnits = unit.currentTile.getTilesInDistance(3)
.flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } }
val tilesInRangeOfAttack = nearbyRangedEnemyUnits
.flatMap { it.getTile().getTilesInDistance(it.getRange()) }
val tilesWithinBombardmentRange = unit.currentTile.getTilesInDistance(3)
.filter { it.isCityCenter() && it.getCity()!!.civ.isAtWarWith(unit.civ) }
.flatMap { it.getTilesInDistance(it.getCity()!!.range) }
val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(3)
.filter { unit.getDamageFromTerrain(it) > 0 }
val dangerousTiles = (tilesInRangeOfAttack + tilesWithinBombardmentRange + tilesWithTerrainDamage).toHashSet()
val dangerousTiles = getDangerousTiles(unit)
val viableTilesForHealing = unitDistanceToTiles.keys
.filter { it !in dangerousTiles && unit.movement.canMoveTo(it) }
@ -449,6 +310,23 @@ object UnitAutomation {
return true
}
private fun getDangerousTiles(unit: MapUnit): HashSet<Tile> {
val nearbyRangedEnemyUnits = unit.currentTile.getTilesInDistance(3)
.flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } }
val tilesInRangeOfAttack = nearbyRangedEnemyUnits
.flatMap { it.getTile().getTilesInDistance(it.getRange()) }
val tilesWithinBombardmentRange = unit.currentTile.getTilesInDistance(3)
.filter { it.isCityCenter() && it.getCity()!!.civ.isAtWarWith(unit.civ) }
.flatMap { it.getTilesInDistance(it.getCity()!!.range) }
val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(3)
.filter { unit.getDamageFromTerrain(it) > 0 }
return (tilesInRangeOfAttack + tilesWithinBombardmentRange + tilesWithTerrainDamage).toHashSet()
}
fun tryPillageImprovement(unit: MapUnit): Boolean {
if (unit.isCivilian()) return false
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
@ -720,7 +598,6 @@ object UnitAutomation {
)
}
return false
}
private fun tryGarrisoningRangedLandUnit(unit: MapUnit): Boolean {
@ -731,14 +608,6 @@ object UnitAutomation {
&& unit.movement.canMoveTo(centerTile)
}
fun isCityThatNeedsDefendingInWartime(city: City): Boolean {
if (city.health < city.getMaxHealth()) return true // this city is under attack!
for (enemyCivCity in unit.civ.diplomacy.values
.filter { it.diplomaticStatus == DiplomaticStatus.War }
.map { it.otherCiv() }.flatMap { it.cities })
if (city.getCenterTile().aerialDistanceTo(enemyCivCity.getCenterTile()) <= 5) return true // this is an edge city that needs defending
return false
}
val citiesToTry = if (!unit.civ.isAtWar()) {
if (unit.getTile().isCityCenter()) return true // It's always good to have a unit in the city center, so if you haven't found anyone around to attack, forget it.
@ -760,6 +629,15 @@ object UnitAutomation {
return true
}
private fun isCityThatNeedsDefendingInWartime(city: City): Boolean {
if (city.health < city.getMaxHealth()) return true // this city is under attack!
for (enemyCivCity in city.civ.diplomacy.values
.filter { it.diplomaticStatus == DiplomaticStatus.War }
.map { it.otherCiv() }.flatMap { it.cities })
if (city.getCenterTile().aerialDistanceTo(enemyCivCity.getCenterTile()) <= 5) return true // this is an edge city that needs defending
return false
}
private fun tryStationingMeleeNavalUnit(unit: MapUnit): Boolean {
fun isMeleeNaval(mapUnit: MapUnit) = mapUnit.baseUnit.isMelee() && mapUnit.type.isWaterUnit()
@ -799,54 +677,8 @@ object UnitAutomation {
unit.action = null
}
/** Returns whether the civilian spends its turn hiding and not moving */
private fun tryRunAwayIfNeccessary(unit: MapUnit): Boolean {
// This is a little 'Bugblatter Beast of Traal': Run if we can attack an enemy
// Cheaper than determining which enemies could attack us next turn
val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys
.filter { containsEnemyMilitaryUnit(unit, it) }
if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary()
&& unit.getTile().militaryUnit == null && !unit.getTile().isCityCenter()) {
runAway(unit)
return true
}
return false
}
private fun runAway(unit: MapUnit) {
val reachableTiles = unit.movement.getDistanceToTiles()
val enterableCity = reachableTiles.keys
.firstOrNull { it.isCityCenter() && unit.movement.canMoveTo(it) }
if (enterableCity != null) {
unit.movement.moveToTile(enterableCity)
return
}
val defensiveUnit = reachableTiles.keys
.firstOrNull {
it.militaryUnit != null && it.militaryUnit!!.civ == unit.civ && it.civilianUnit == null
}
if (defensiveUnit != null) {
unit.movement.moveToTile(defensiveUnit)
return
}
val tileFurthestFromEnemy = reachableTiles.keys
.filter { unit.movement.canMoveTo(it) && unit.getDamageFromTerrain(it) < unit.health }
.maxByOrNull { countDistanceToClosestEnemy(unit, it) }
?: return // can't move anywhere!
unit.movement.moveToTile(tileFurthestFromEnemy)
}
private fun countDistanceToClosestEnemy(unit: MapUnit, tile: Tile): Int {
for (i in 1..3)
if (tile.getTilesAtDistance(i).any { containsEnemyMilitaryUnit(unit, it) })
return i
return 4
}
private fun containsEnemyMilitaryUnit(unit: MapUnit, tile: Tile) =
internal fun containsEnemyMilitaryUnit(unit: MapUnit, tile: Tile) =
tile.militaryUnit != null
&& tile.militaryUnit!!.civ.isAtWarWith(unit.civ)