AI: Build melee naval units to defend coastal cities and move them there

This commit is contained in:
Yair Morgenstern 2023-08-10 12:00:03 +03:00
parent 654b9f80f2
commit 27a5bd0cc5
2 changed files with 55 additions and 6 deletions

View File

@ -2,13 +2,13 @@ package com.unciv.logic.automation
import com.unciv.logic.city.City import com.unciv.logic.city.City
import com.unciv.logic.city.CityFocus import com.unciv.logic.city.CityFocus
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.logic.civilization.Civilization import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.BFS import com.unciv.logic.map.BFS
import com.unciv.logic.map.TileMap import com.unciv.logic.map.TileMap
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Building import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.models.ruleset.Victory import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.tile.ResourceType import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileImprovement import com.unciv.models.ruleset.tile.TileImprovement
@ -141,12 +141,23 @@ object Automation {
// if not coastal, removeShips == true so don't even consider ships // if not coastal, removeShips == true so don't even consider ships
var removeShips = true var removeShips = true
var isMissingNavalUnitsForCityDefence = false
fun isNavalMeleeUnit(unit: BaseUnit) = unit.isMelee() && unit.type.isWaterUnit()
if (city.isCoastal()) { if (city.isCoastal()) {
// in the future this could be simplified by assigning every distinct non-lake body of // in the future this could be simplified by assigning every distinct non-lake body of
// water their own ID like a continent ID // water their own ID like a continent ID
val findWaterConnectedCitiesAndEnemies = val findWaterConnectedCitiesAndEnemies =
BFS(city.getCenterTile()) { it.isWater || it.isCityCenter() } BFS(city.getCenterTile()) { it.isWater || it.isCityCenter() }
findWaterConnectedCitiesAndEnemies.stepToEnd() findWaterConnectedCitiesAndEnemies.stepToEnd()
val numberOfOurConnectedCities = findWaterConnectedCitiesAndEnemies.getReachedTiles()
.count { it.isCityCenter() && it.getOwner() == city.civ }
val numberOfOurNavalMeleeUnits = findWaterConnectedCitiesAndEnemies.getReachedTiles().asSequence()
.flatMap { it.getUnits() }
.count { isNavalMeleeUnit(it.baseUnit) }
isMissingNavalUnitsForCityDefence = numberOfOurConnectedCities > numberOfOurNavalMeleeUnits
removeShips = findWaterConnectedCitiesAndEnemies.getReachedTiles().none { removeShips = findWaterConnectedCitiesAndEnemies.getReachedTiles().none {
(it.isCityCenter() && it.getOwner() != city.civ) (it.isCityCenter() && it.getOwner() != city.civ)
|| (it.militaryUnit != null && it.militaryUnit!!.civ != city.civ) || (it.militaryUnit != null && it.militaryUnit!!.civ != city.civ)
@ -174,7 +185,13 @@ object Automation {
chosenUnit = militaryUnits chosenUnit = militaryUnits
.filter { it.isRanged() } .filter { it.isRanged() }
.maxByOrNull { it.cost }!! .maxByOrNull { it.cost }!!
} else { // randomize type of unit and take the most expensive of its kind }
else if (isMissingNavalUnitsForCityDefence && militaryUnits.any { isNavalMeleeUnit(it) }){
chosenUnit = militaryUnits
.filter { isNavalMeleeUnit(it) }
.maxBy { it.cost }
}
else { // randomize type of unit and take the most expensive of its kind
val bestUnitsForType = hashMapOf<String, BaseUnit>() val bestUnitsForType = hashMapOf<String, BaseUnit>()
for (unit in militaryUnits) { for (unit in militaryUnits) {
if (bestUnitsForType[unit.unitType] == null || bestUnitsForType[unit.unitType]!!.cost < unit.cost) { if (bestUnitsForType[unit.unitType] == null || bestUnitsForType[unit.unitType]!!.cost < unit.cost) {

View File

@ -109,7 +109,6 @@ object UnitAutomation {
&& unit.movement.canReach(tile) // expensive, evaluate last && unit.movement.canReach(tile) // expensive, evaluate last
} }
@JvmStatic
fun wander(unit: MapUnit, stayInTerritory: Boolean = false, tilesToAvoid:Set<Tile> = setOf()) { fun wander(unit: MapUnit, stayInTerritory: Boolean = false, tilesToAvoid:Set<Tile> = setOf()) {
val unitDistanceToTiles = unit.movement.getDistanceToTiles() val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val reachableTiles = unitDistanceToTiles val reachableTiles = unitDistanceToTiles
@ -205,7 +204,9 @@ object UnitAutomation {
// Focus all units without a specific target on the enemy city closest to one of our cities // Focus all units without a specific target on the enemy city closest to one of our cities
if (tryHeadTowardsEnemyCity(unit)) return if (tryHeadTowardsEnemyCity(unit)) return
if (tryGarrisoningUnit(unit)) return if (tryGarrisoningRangedLandUnit(unit)) return
if (tryStationingMeleeNavalUnit(unit)) return
if (unit.health < 80 && tryHealUnit(unit)) return if (unit.health < 80 && tryHealUnit(unit)) return
@ -727,7 +728,7 @@ object UnitAutomation {
} }
private fun tryGarrisoningUnit(unit: MapUnit): Boolean { private fun tryGarrisoningRangedLandUnit(unit: MapUnit): Boolean {
if (unit.baseUnit.isMelee() || unit.baseUnit.isWaterUnit()) return false // don't garrison melee units, they're not that good at it if (unit.baseUnit.isMelee() || unit.baseUnit.isWaterUnit()) return false // don't garrison melee units, they're not that good at it
val citiesWithoutGarrison = unit.civ.cities.filter { val citiesWithoutGarrison = unit.civ.cities.filter {
val centerTile = it.getCenterTile() val centerTile = it.getCenterTile()
@ -750,8 +751,10 @@ object UnitAutomation {
} else { } else {
if (unit.getTile().isCityCenter() && if (unit.getTile().isCityCenter() &&
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
citiesWithoutGarrison.asSequence() val citiesWithoutGarrisonThatNeedDefending = citiesWithoutGarrison.asSequence()
.filter { isCityThatNeedsDefendingInWartime(it) } .filter { isCityThatNeedsDefendingInWartime(it) }
if (citiesWithoutGarrisonThatNeedDefending.any()) citiesWithoutGarrisonThatNeedDefending
else citiesWithoutGarrison.asSequence()
} }
val closestReachableCityNeedsDefending = citiesToTry val closestReachableCityNeedsDefending = citiesToTry
@ -762,6 +765,35 @@ object UnitAutomation {
return true return true
} }
private fun tryStationingMeleeNavalUnit(unit: MapUnit): Boolean {
fun isMeleeNaval(mapUnit: MapUnit) = mapUnit.baseUnit.isMelee() && mapUnit.type.isWaterUnit()
if (!isMeleeNaval(unit)) return false
val closeCity = unit.getTile().getTilesInDistance(3)
.firstOrNull { it.isCityCenter() }
// We're the closest unit to this city, we should stay here :)
if (closeCity != null && closeCity.getTilesInDistance(3)
.flatMap { it.getUnits() }
.firstOrNull {isMeleeNaval(it)} == unit
&& unit.movement.canReach(closeCity)) {
unit.movement.headTowards(closeCity)
return true
}
val citiesWithoutNavalDefence = unit.civ.cities.filter { it.isCoastal() }
.filter { it.getCenterTile().aerialDistanceTo(unit.getTile()) < 20 } // Not too far away
.filter { it.getCenterTile().getTilesInDistance(3)
.flatMap { it.getUnits() }
.none { isMeleeNaval(it) }}
val reachableCity = citiesWithoutNavalDefence.firstOrNull {
unit.movement.canReach(it.getCenterTile())
} ?: return false
unit.movement.headTowards(reachableCity.getCenterTile())
return true
}
/** This is what a unit with the 'explore' action does. /** This is what a unit with the 'explore' action does.
It also explores, but also has other functions, like healing if necessary. */ It also explores, but also has other functions, like healing if necessary. */
fun automatedExplore(unit: MapUnit) { fun automatedExplore(unit: MapUnit) {