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.CityFocus
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.BFS
import com.unciv.logic.map.TileMap
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.Building
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.tile.ResourceType
import com.unciv.models.ruleset.tile.TileImprovement
@ -141,12 +141,23 @@ object Automation {
// if not coastal, removeShips == true so don't even consider ships
var removeShips = true
var isMissingNavalUnitsForCityDefence = false
fun isNavalMeleeUnit(unit: BaseUnit) = unit.isMelee() && unit.type.isWaterUnit()
if (city.isCoastal()) {
// in the future this could be simplified by assigning every distinct non-lake body of
// water their own ID like a continent ID
val findWaterConnectedCitiesAndEnemies =
BFS(city.getCenterTile()) { it.isWater || it.isCityCenter() }
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 {
(it.isCityCenter() && it.getOwner() != city.civ)
|| (it.militaryUnit != null && it.militaryUnit!!.civ != city.civ)
@ -174,7 +185,13 @@ object Automation {
chosenUnit = militaryUnits
.filter { it.isRanged() }
.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>()
for (unit in militaryUnits) {
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
}
@JvmStatic
fun wander(unit: MapUnit, stayInTerritory: Boolean = false, tilesToAvoid:Set<Tile> = setOf()) {
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
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
if (tryHeadTowardsEnemyCity(unit)) return
if (tryGarrisoningUnit(unit)) return
if (tryGarrisoningRangedLandUnit(unit)) return
if (tryStationingMeleeNavalUnit(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
val citiesWithoutGarrison = unit.civ.cities.filter {
val centerTile = it.getCenterTile()
@ -750,8 +751,10 @@ object UnitAutomation {
} else {
if (unit.getTile().isCityCenter() &&
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
citiesWithoutGarrison.asSequence()
val citiesWithoutGarrisonThatNeedDefending = citiesWithoutGarrison.asSequence()
.filter { isCityThatNeedsDefendingInWartime(it) }
if (citiesWithoutGarrisonThatNeedDefending.any()) citiesWithoutGarrisonThatNeedDefending
else citiesWithoutGarrison.asSequence()
}
val closestReachableCityNeedsDefending = citiesToTry
@ -762,6 +765,35 @@ object UnitAutomation {
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.
It also explores, but also has other functions, like healing if necessary. */
fun automatedExplore(unit: MapUnit) {