From 0db070a25f081e9eff87ede6a0d6b08de63c1f01 Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Mon, 28 Aug 2023 02:49:57 -0500 Subject: [PATCH] Ai nuke improvement (#9968) * Improved Nuke AI * AI can only nuke visible tiles now * Removed an extra space * Removed commented changes from another feature in testing * Removed commented changes from another feature in testing again * AI now doesn't calculate the value of nuking a tile while at peace * Removed extra change related to attacking cities. --- .../automation/unit/SpecificUnitAutomation.kt | 84 +++++++++++++++---- .../logic/automation/unit/UnitAutomation.kt | 6 +- core/src/com/unciv/logic/battle/Battle.kt | 2 + 3 files changed, 73 insertions(+), 19 deletions(-) diff --git a/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt index a42a851ac1..e93d23b9dc 100644 --- a/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/SpecificUnitAutomation.kt @@ -7,6 +7,7 @@ import com.unciv.logic.battle.GreatGeneralImplementation 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.diplomacy.DiplomaticModifiers import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.tile.Tile @@ -494,27 +495,78 @@ object SpecificUnitAutomation { } fun automateNukes(unit: MapUnit) { - val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange()) - for (tile in tilesInRange) { - // For now AI will only use nukes against cities because in all honesty that's the best use for them. - if (tile.isCityCenter() - && tile.getOwner()!!.isAtWarWith(unit.civ) - && tile.getCity()!!.health > tile.getCity()!!.getMaxHealth() / 2 - && Battle.mayUseNuke(MapUnitCombatant(unit), tile)) { - val blastRadius = unit.getNukeBlastRadius() - val tilesInBlastRadius = tile.getTilesInDistance(blastRadius) - val civsInBlastRadius = tilesInBlastRadius.mapNotNull { it.getOwner() } + - tilesInBlastRadius.mapNotNull { it.getFirstUnit()?.civ } - // Don't nuke if it means we will be declaring war on someone! - if (civsInBlastRadius.none { it != unit.civ && !it.isAtWarWith(unit.civ) }) { - Battle.NUKE(MapUnitCombatant(unit), tile) - return - } + if (!unit.civ.isAtWar()) return + // We should *Almost* never want to nuke our own city, so don't consider it + val tilesInRange = unit.currentTile.getTilesInDistanceRange(2..unit.getRange()) + var highestTileNukeValue = 0 + var tileToNuke: Tile? = null + tilesInRange.forEach { + val value = getNukeLocationValue(unit, it) + if (value > highestTileNukeValue) { + highestTileNukeValue = value + tileToNuke = it } } + if (highestTileNukeValue > 0) { + Battle.NUKE(MapUnitCombatant(unit), tileToNuke!!) + } tryRelocateToNearbyAttackableCities(unit) } + /** + * Ranks the tile to nuke based off of all tiles in it's blast radius + * By default the value is -500 to prevent inefficient nuking. + */ + fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int { + val civ = nuke.civ + if (!Battle.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE + val blastRadius = nuke.getNukeBlastRadius() + val tilesInBlastRadius = tile.getTilesInDistance(blastRadius) + val civsInBlastRadius = tilesInBlastRadius.mapNotNull { it.getOwner() } + + tilesInBlastRadius.mapNotNull { it.getFirstUnit()?.civ } + + // Don't nuke if it means we will be declaring war on someone! + if (civsInBlastRadius.any { it != civ && !it.isAtWarWith(civ) }) return -100000 + // If there are no enemies to hit, don't nuke + if (!civsInBlastRadius.any { it.isAtWarWith(civ) }) return -100000 + + // Launching a Nuke uses resources, therefore don't launch it by default + var explosionValue = -500 + + // Returns either ourValue or thierValue depending on if the input Civ matches the Nuke's Civ + fun evaluateCivValue(targetCiv: Civilization, ourValue: Int, theirValue: Int): Int { + if (targetCiv == civ) // We are nuking something that we own! + return ourValue + return theirValue // We are nuking an enemy! + } + for (targetTile in tilesInBlastRadius) { + // We can only account for visible units + if (tile.isVisible(civ)) { + if (targetTile.militaryUnit != null && !targetTile.militaryUnit!!.isInvisible(civ)) + explosionValue += evaluateCivValue(targetTile.militaryUnit?.civ!!, -150, 50) + if (targetTile.civilianUnit != null && !targetTile.civilianUnit!!.isInvisible(civ)) + explosionValue += evaluateCivValue(targetTile.civilianUnit?.civ!!, -100, 25) + } + // Never nuke our own Civ, don't nuke single enemy civs as well + if (targetTile.isCityCenter() + && !(targetTile.getCity()!!.health <= 50f + && targetTile.neighbors.any {it.militaryUnit?.civ == civ})) // Prefer not to nuke cities that we are about to take + explosionValue += evaluateCivValue(targetTile.getCity()?.civ!!, -100000, 250) + else if (targetTile.owningCity != null) { + val owningCiv = targetTile.owningCity?.civ!! + // If there is a tile to add fallout to there is a 50% chance it will get fallout + if (!(tile.isWater || tile.isImpassible() || targetTile.terrainFeatures.any { it == "Fallout" })) + explosionValue += evaluateCivValue(owningCiv, -40, 10) + // If there is an improvment to pillage + if (targetTile.improvement != null && !targetTile.improvementIsPillaged) + explosionValue += evaluateCivValue(owningCiv, -40, 20) + } + // If the value is too low end the search early + if (explosionValue < -1000) return explosionValue + } + return explosionValue + } + // This really needs to be changed, to have better targeting for missiles fun automateMissile(unit: MapUnit) { if (BattleHelper.tryAttackNearbyEnemy(unit)) return diff --git a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt index c4740b3da4..4900ec0e8d 100644 --- a/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/unit/UnitAutomation.kt @@ -178,7 +178,7 @@ object UnitAutomation { if (unit.baseUnit.isAirUnit() && unit.canIntercept()) return SpecificUnitAutomation.automateFighter(unit) - if (unit.baseUnit.isAirUnit()) + if (unit.baseUnit.isAirUnit() && !unit.baseUnit.isNuclearWeapon()) return SpecificUnitAutomation.automateBomber(unit) if (unit.baseUnit.isNuclearWeapon()) @@ -383,7 +383,7 @@ object UnitAutomation { unit.movement.headTowards(encampmentToHeadTowards) return true } - + private fun tryHealUnit(unit: MapUnit): Boolean { if (unit.baseUnit.isRanged() && unit.hasUnique(UniqueType.HealsEvenAfterAction)) return false // will heal anyway, and attacks don't hurt @@ -633,7 +633,7 @@ object UnitAutomation { .sum() // City heals 20 per turn if (expectedDamagePerTurn < city.health && // If we can take immediately, go for it - (expectedDamagePerTurn <= 20 || city.health / (expectedDamagePerTurn-20) > 5)){ // otherwise check if we can take within a couple of turns + (expectedDamagePerTurn <= 20 || city.health / (expectedDamagePerTurn-20) > 5)){ // otherwise check if we can take within a couple of turns // We won't be able to take this even with 5 turns of continuous damage! // don't head straight to the city, try to head to landing grounds - diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index 474500b3e6..8e0d8e02ed 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -759,6 +759,8 @@ object Battle { */ fun mayUseNuke(nuke: MapUnitCombatant, targetTile: Tile): Boolean { if (nuke.getTile() == targetTile) return false + // Can only nuke visible Tiles + if (!targetTile.isVisible(nuke.getCivInfo())) return false var canNuke = true val attackerCiv = nuke.getCivInfo()