Better declare war and city battle decisions (hopefully) for AI

This commit is contained in:
Yair Morgenstern 2020-04-29 10:42:35 +03:00
parent 38333c2600
commit 3865972737
2 changed files with 82 additions and 48 deletions

View File

@ -8,11 +8,13 @@ import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.map.BFS
import com.unciv.logic.map.MapUnit
import com.unciv.logic.trade.*
import com.unciv.models.ruleset.VictoryType
import com.unciv.models.ruleset.tech.Technology
import com.unciv.models.translations.tr
import java.lang.reflect.Modifier
import kotlin.math.min
object NextTurnAutomation{
@ -394,24 +396,56 @@ object NextTurnAutomation{
.filterNot { it == civInfo || it.cities.isEmpty() || !civInfo.getDiplomacyManager(it).canDeclareWar() }
if (enemyCivs.isEmpty()) return
var enemyCivsToEvaluate = enemyCivs.filter { getMinDistanceBetweenCities(civInfo, it) <= 7 }
val civWithBestMotivationToAttack = enemyCivs
.map { Pair(it, motivationToAttack(civInfo, it)) }
.maxBy { it.second }!!
var isFightingFarAway = false
if (enemyCivsToEvaluate.isEmpty()) { // so, there ARE civs we can declare war on, just not close by
if (civInfo.victoryType() != VictoryType.Domination) return // No point in attacking civs that are too far away
enemyCivsToEvaluate = enemyCivs
isFightingFarAway = true
if (civWithBestMotivationToAttack.second >= 20)
civInfo.getDiplomacyManager(civWithBestMotivationToAttack.first).declareWar()
}
private fun motivationToAttack(civInfo:CivilizationInfo, otherCiv:CivilizationInfo): Int {
val ourCombatStrength = Automation.evaluteCombatStrength(civInfo).toFloat()
val theirCombatStrength = Automation.evaluteCombatStrength(otherCiv)
if (theirCombatStrength > ourCombatStrength) return 0
val modifierMap = HashMap<String, Int>()
val combatStrengthRatio = ourCombatStrength / theirCombatStrength
val combatStrengthModifier = when {
combatStrengthRatio > 3f -> 30
combatStrengthRatio > 2f -> 20
combatStrengthRatio > 1.5f -> 10
else -> 0
}
modifierMap["Relative combat strength"] = combatStrengthModifier
val closestCities = getClosestCities(civInfo, otherCiv)
val ourCity = closestCities.city1
val theirCity = closestCities.city2
if (closestCities.aerialDistance > 7)
modifierMap["Far away cities"] = -10
val landPathBFS = BFS(ourCity.getCenterTile()) {
val owner = it.getOwner();
it.isLand && !it.getBaseTerrain().impassable
&& (owner == otherCiv || owner == null || civInfo.canEnterTiles(owner))
}
val weakestCloseCiv = enemyCivsToEvaluate.minBy { Automation.evaluteCombatStrength(it) }!!
val weakestCloseCivCombatStrength = Automation.evaluteCombatStrength(weakestCloseCiv)
val ourCombatStrength = Automation.evaluteCombatStrength(civInfo)
landPathBFS.stepUntilDestination(theirCity.getCenterTile())
if (!landPathBFS.hasReachedTile(theirCity.getCenterTile()))
modifierMap["No land path"] = -10
val amountWeNeedToBeStronger =
if (civInfo.victoryType() == VictoryType.Domination && !isFightingFarAway) 1.5f else 2f
if(civInfo.getDiplomacyManager(otherCiv).hasFlag(DiplomacyFlags.ResearchAgreement))
modifierMap["Research Agreement"] = -5
if (weakestCloseCivCombatStrength * amountWeNeedToBeStronger < ourCombatStrength)
civInfo.getDiplomacyManager(weakestCloseCiv).declareWar()
if(civInfo.getDiplomacyManager(otherCiv).resourcesFromTrade().any { it.amount > 0 })
modifierMap["Receiving trade resources"] = -5
if(theirCity.getTiles().none { it.neighbors.any { it.getOwner()==theirCity.civInfo && it.getCity() != theirCity } })
modifierMap["Isolated city"] = 15
return modifierMap.values.sum()
}
private fun automateUnits(civInfo: CivilizationInfo) {

View File

@ -281,9 +281,37 @@ object UnitAutomation {
.firstOrNull { unit.movement.canReach(it) }
if (closestReachableEnemyCity != null) {
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
return headTowardsEnemyCity(unit, closestReachableEnemyCity)
}
return false
}
private fun headTowardsEnemyCity(unit: MapUnit, closestReachableEnemyCity: TileInfo): Boolean {
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val unitRange = unit.getRange()
if (unitRange > 2) { // long-ranged unit, should never be in a bombardable position
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2).toSet()
val tileToMoveTo =
unitDistanceToTiles.asSequence()
.filter {
it.key.aerialDistanceTo(closestReachableEnemyCity) <=
unitRange && it.key !in tilesInBombardRange
}
.minBy { it.value.totalDistance }?.key
// move into position far away enough that the bombard doesn't hurt
if (tileToMoveTo != null) {
unit.movement.headTowards(tileToMoveTo)
return true
}
return false // didn't move
}
val numberOfUnitsAroundCity = closestReachableEnemyCity.getTilesInDistance(4)
.count { it.militaryUnit != null && it.militaryUnit!!.civInfo == unit.civInfo }
if (numberOfUnitsAroundCity < 3) {
// don't head straight to the city, try to head to landing grounds -
// this is against tha AI's brilliant plan of having everyone embarked and attacking via sea when unnecessary.
val tileToHeadTo = closestReachableEnemyCity.getTilesInDistanceRange(3..4)
@ -291,43 +319,15 @@ object UnitAutomation {
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it) }
if (tileToHeadTo != null) // no need to worry, keep going as the movement alg. says
if (tileToHeadTo != null) { // no need to worry, keep going as the movement alg. says
unit.movement.headTowards(tileToHeadTo)
else {
val unitRange = unit.getRange()
if (unitRange > 2) { // should never be in a bombardable position
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2).toSet()
val tileToMoveTo =
unitDistanceToTiles.asSequence()
.filter {
it.key.aerialDistanceTo(closestReachableEnemyCity) <=
unitRange && it.key !in tilesInBombardRange
}
.minBy { it.value.totalDistance }?.key
// move into position far away enough that the bombard doesn't hurt
if (tileToMoveTo != null)
unit.movement.headTowards(tileToMoveTo)
} else { // unit range <= 2
// calculate total damage of units in surrounding 4-spaces from enemy city (so we can attack a city from 2 directions at once)
val militaryUnitsAroundEnemyCity =
closestReachableEnemyCity.getTilesInDistance(3)
.map { it.militaryUnit }.filterNotNull()
.filter { it.civInfo == unit.civInfo }
//todo: use CONSTANT for 20
var totalAttackOnCityPerTurn = -20 // cities heal 20 per turn, so anything below that its useless
val enemyCityCombatant = CityCombatant(closestReachableEnemyCity.getCity()!!)
for (militaryUnit in militaryUnitsAroundEnemyCity) {
totalAttackOnCityPerTurn += BattleDamage.calculateDamageToDefender(MapUnitCombatant(militaryUnit), enemyCityCombatant)
}
if (totalAttackOnCityPerTurn * 3 > closestReachableEnemyCity.getCity()!!.health) // if we can defeat it in 3 turns with the current units,
unit.movement.headTowards(closestReachableEnemyCity) // go for it!
}
return true
}
return true
}
return false
unit.movement.headTowards(closestReachableEnemyCity) // go for it!
return true
}
fun tryBombardEnemy(city: CityInfo): Boolean {