mirror of
https://github.com/yairm210/Unciv.git
synced 2025-01-05 21:11:35 +07:00
Improved AI chooseAttackTarget (#9979)
* Improved AI chooseAttackTarget * Added extra import statement * Restructured chooseAttackTarget to use .maxByOrNull * Refactored getUnitAttackValue * Standardised attack valuing in BattleHelper
This commit is contained in:
parent
ef8deffe67
commit
88741f95e9
@ -3,10 +3,13 @@ package com.unciv.logic.automation.unit
|
||||
import com.unciv.logic.battle.AttackableTile
|
||||
import com.unciv.logic.battle.Battle
|
||||
import com.unciv.logic.battle.BattleDamage
|
||||
import com.unciv.logic.battle.CityCombatant
|
||||
import com.unciv.logic.battle.MapUnitCombatant
|
||||
import com.unciv.logic.city.City
|
||||
import com.unciv.logic.battle.TargetHelper
|
||||
import com.unciv.logic.map.mapunit.MapUnit
|
||||
import com.unciv.models.ruleset.unique.UniqueType
|
||||
import kotlin.math.max
|
||||
|
||||
object BattleHelper {
|
||||
|
||||
@ -52,64 +55,95 @@ object BattleHelper {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Choses the best target in attackableEnemies, this could be a city or a unit.
|
||||
*/
|
||||
private fun chooseAttackTarget(unit: MapUnit, attackableEnemies: List<AttackableTile>): AttackableTile? {
|
||||
val cityTilesToAttack = attackableEnemies.filter { it.tileToAttack.isCityCenter() }
|
||||
val nonCityTilesToAttack = attackableEnemies.filter { !it.tileToAttack.isCityCenter() }
|
||||
|
||||
var highestAttackValue = 0
|
||||
val attackTile = attackableEnemies.maxByOrNull { attackableEnemy ->
|
||||
val tempAttackValue = if (attackableEnemy.tileToAttack.isCityCenter())
|
||||
getCityAttackValue(unit, attackableEnemy.tileToAttack.getCity()!!)
|
||||
else getUnitAttackValue(unit, attackableEnemy)
|
||||
highestAttackValue = max(tempAttackValue, highestAttackValue)
|
||||
tempAttackValue
|
||||
}
|
||||
// todo For air units, prefer to attack tiles with lower intercept chance
|
||||
|
||||
val capturableCity = cityTilesToAttack.firstOrNull { it.tileToAttack.getCity()!!.health == 1 }
|
||||
val cityWithHealthLeft =
|
||||
cityTilesToAttack.filter { it.tileToAttack.getCity()!!.health != 1 } // don't want ranged units to attack defeated cities
|
||||
.minByOrNull { it.tileToAttack.getCity()!!.health }
|
||||
|
||||
if (unit.baseUnit.isMelee() && capturableCity != null)
|
||||
return capturableCity // enter it quickly, top priority!
|
||||
|
||||
if (nonCityTilesToAttack.isNotEmpty()) // second priority, units
|
||||
return chooseUnitToAttack(unit, nonCityTilesToAttack)
|
||||
|
||||
if (cityWithHealthLeft != null) return cityWithHealthLeft // third priority, city
|
||||
|
||||
return null
|
||||
return if (highestAttackValue > 30) attackTile else null
|
||||
}
|
||||
|
||||
private fun chooseUnitToAttack(unit: MapUnit, attackableUnits: List<AttackableTile>): AttackableTile {
|
||||
val militaryUnits = attackableUnits.filter { it.tileToAttack.militaryUnit != null }
|
||||
/**
|
||||
* Returns a value which represents the attacker's motivation to attack a city.
|
||||
* Siege units will almost always attack cities.
|
||||
* Base value is 100(Mele) 110(Ranged) standard deviation is around 80 to 130
|
||||
*/
|
||||
private fun getCityAttackValue(attacker: MapUnit, city: City): Int {
|
||||
val attackerUnit = MapUnitCombatant(attacker)
|
||||
val cityUnit = CityCombatant(city)
|
||||
val isCityCapturable = city.health == 1
|
||||
|| attacker.baseUnit.isMelee() && city.health <= BattleDamage.calculateDamageToDefender(attackerUnit, cityUnit).coerceAtLeast(1)
|
||||
if (isCityCapturable)
|
||||
return if (attacker.baseUnit.isMelee()) 10000 // Capture the city immediatly!
|
||||
else 0 // Don't attack the city anymore since we are a ranged unit
|
||||
|
||||
if (attacker.baseUnit.isMelee() && attacker.health - BattleDamage.calculateDamageToAttacker(attackerUnit, cityUnit) * 2 <= 0)
|
||||
return 0 // We'll probably die next turn if we attack the city
|
||||
|
||||
// prioritize attacking military
|
||||
if (militaryUnits.isNotEmpty()) {
|
||||
// associate enemy units with number of hits from this unit to kill them
|
||||
val attacksToKill = militaryUnits
|
||||
.associateWith { it.tileToAttack.militaryUnit!!.health.toFloat() / BattleDamage.calculateDamageToDefender(
|
||||
MapUnitCombatant(unit),
|
||||
MapUnitCombatant(it.tileToAttack.militaryUnit!!)
|
||||
).toFloat().coerceAtLeast(1f) }
|
||||
var attackValue = 100
|
||||
// Siege units should really only attack the city
|
||||
if (attacker.baseUnit.isProbablySiegeUnit()) attackValue += 100
|
||||
// Ranged units don't take damage from the city
|
||||
else if (attacker.baseUnit.isRanged()) attackValue += 10
|
||||
// Lower health cities have a higher priority to attack ranges from -20 to 30
|
||||
attackValue -= (city.health - 60) / 2
|
||||
|
||||
// kill a unit if possible, prioritizing by attack strength
|
||||
val canKill = attacksToKill.filter { it.value <= 1 }.keys
|
||||
.sortedByDescending { it.movementLeftAfterMovingToAttackTile } // Among equal kills, prioritize the closest unit
|
||||
.maxByOrNull { MapUnitCombatant(it.tileToAttack.militaryUnit!!).getAttackingStrength() }
|
||||
if (canKill != null) return canKill
|
||||
|
||||
// otherwise pick the unit we can kill the fastest
|
||||
return attacksToKill.minBy { it.value }.key
|
||||
// Add value based on number of units around the city
|
||||
val defendingCityCiv = city.civ
|
||||
city.getCenterTile().neighbors.forEach {
|
||||
if (it.militaryUnit != null) {
|
||||
if (it.militaryUnit!!.civ.isAtWarWith(attacker.civ))
|
||||
attackValue -= 5
|
||||
if (it.militaryUnit!!.civ.isAtWarWith(defendingCityCiv))
|
||||
attackValue += 15
|
||||
}
|
||||
}
|
||||
|
||||
return attackValue
|
||||
}
|
||||
|
||||
// only civilians in attacking range - GP most important, second settlers, then anything else
|
||||
|
||||
val unitsToConsider = attackableUnits.filter { it.tileToAttack.civilianUnit!!.isGreatPerson() }
|
||||
.ifEmpty { attackableUnits.filter { it.tileToAttack.civilianUnit!!.hasUnique(UniqueType.FoundCity) } }
|
||||
.ifEmpty { attackableUnits }
|
||||
|
||||
// Melee - prioritize by distance, so we have most movement left
|
||||
if (unit.baseUnit.isMelee()){
|
||||
return unitsToConsider.maxBy { it.movementLeftAfterMovingToAttackTile }
|
||||
}
|
||||
|
||||
// We're ranged, prioritize that we can kill
|
||||
return unitsToConsider.minBy {
|
||||
Battle.getMapCombatantOfTile(it.tileToAttack)!!.getHealth()
|
||||
/**
|
||||
* Returns a value which represents the attacker's motivation to attack a unit.
|
||||
* Base value is 100 and standard deviation is around 80 to 130
|
||||
*/
|
||||
private fun getUnitAttackValue(attacker: MapUnit, attackTile: AttackableTile): Int {
|
||||
// Base attack value, there is nothing there...
|
||||
var attackValue = Int.MIN_VALUE
|
||||
// Prioritize attacking military
|
||||
val militaryUnit = attackTile.tileToAttack.militaryUnit
|
||||
val civilianUnit = attackTile.tileToAttack.civilianUnit
|
||||
if (militaryUnit != null) {
|
||||
attackValue = 100
|
||||
// Associate enemy units with number of hits from this unit to kill them
|
||||
val attacksToKill = (militaryUnit.health.toFloat() /
|
||||
BattleDamage.calculateDamageToDefender(MapUnitCombatant(attacker), MapUnitCombatant(militaryUnit))).coerceAtLeast(1f)
|
||||
// We can kill them in this turn
|
||||
if (attacksToKill <= 1) attackValue += 30
|
||||
// On average, this should take around 3 turns, so -15
|
||||
else attackValue -= (attacksToKill * 5).toInt()
|
||||
} else if (civilianUnit != null) {
|
||||
attackValue = 50
|
||||
// Only melee units should really attack/capture civilian units, ranged units take more than one turn
|
||||
if (attacker.baseUnit.isMelee()) {
|
||||
if (civilianUnit.isGreatPerson()) {
|
||||
attackValue += 150
|
||||
}
|
||||
if (civilianUnit.hasUnique(UniqueType.FoundCity)) attackValue += 60
|
||||
}
|
||||
}
|
||||
// Prioritise closer units as they are generally more threatening to this unit
|
||||
// Moving around less means we are straying less into enemy territory
|
||||
// Average should be around 2.5-5 early game and up to 35 for tanks in late game
|
||||
attackValue += (attackTile.movementLeftAfterMovingToAttackTile * 5).toInt()
|
||||
|
||||
return attackValue
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user