Better barbarian automation (#1560)

This commit is contained in:
Vladimir Tanakov 2020-01-12 21:48:34 +03:00 committed by Yair Morgenstern
parent 02ec64f14f
commit 725edc2a31
11 changed files with 518 additions and 295 deletions

View File

@ -0,0 +1,200 @@
package com.unciv.logic.automation
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.BattleDamage
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.PathsToTilesWithinTurn
import com.unciv.logic.map.TileInfo
import com.unciv.models.AttackableTile
import com.unciv.models.UnitAction
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unit.UnitType
import com.unciv.ui.worldscreen.unit.UnitActions
class BarbarianAutomation(val civInfo: CivilizationInfo) {
private val battleHelper = BattleHelper()
private val battleDamage = BattleDamage()
fun automate() {
// ranged go first, after melee and then everyone else
civInfo.getCivUnits().filter { it.type.isRanged() }.forEach(::automateUnit)
civInfo.getCivUnits().filter { it.type.isMelee() }.forEach(::automateUnit)
civInfo.getCivUnits().filter { !it.type.isRanged() && !it.type.isMelee() }.forEach(::automateUnit)
}
private fun automateUnit(unit: MapUnit) {
when {
unit.currentTile.improvement == Constants.barbarianEncampment -> automateEncampment(unit)
unit.type == UnitType.Scout -> automateScout(unit)
else -> automateCombatUnit(unit)
}
}
private fun automateEncampment(unit: MapUnit) {
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
// 1 - trying to upgrade
if (tryUpgradeUnit(unit, unitActions)) return
// 2 - trying to attack somebody
if (battleHelper.tryAttackNearbyEnemy(unit)) return
// 3 - at least fortifying
unit.fortifyIfCan()
}
private fun automateCombatUnit(unit: MapUnit) {
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val nearEnemyTiles = battleHelper.getAttackableEnemies(unit, unitDistanceToTiles)
// 1 - heal or fortifying if death is near
if (unit.health < 50) {
val possibleDamage = nearEnemyTiles
.map {
battleDamage.calculateDamageToAttacker(MapUnitCombatant(unit),
Battle.getMapCombatantOfTile(it.tileToAttack)!!)
}
.sum()
val possibleHeal = unit.rankTileForHealing(unit.currentTile)
if (possibleDamage > possibleHeal) {
// run
val furthestTile = findFurthestTile(unit, unitDistanceToTiles, nearEnemyTiles)
unit.movement.moveToTile(furthestTile)
} else {
// heal
unit.fortifyIfCan()
}
return
}
// 2 - trying to upgrade
if (tryUpgradeUnit(unit, unitActions)) return
// 3 - trying to attack enemy
// if a embarked melee unit can land and attack next turn, do not attack from water.
if (battleHelper.tryDisembarkUnitToAttackPosition(unit, unitDistanceToTiles)) return
if (battleHelper.tryAttackNearbyEnemy(unit)) return
// 4 - trying to pillage tile or route
if (tryPillageImprovement(unit, unitDistanceToTiles, unitActions)) return
// 5 - heal the unit if needed
if (unit.health < 100) {
healUnit(unit, unitDistanceToTiles)
return
}
// 6 - wander
UnitAutomation().wander(unit, unitDistanceToTiles)
}
private fun automateScout(unit: MapUnit) {
val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val nearEnemyTiles = battleHelper.getAttackableEnemies(unit, unitDistanceToTiles)
// 1 - heal or run if death is near
if (unit.health < 50) {
if (nearEnemyTiles.isNotEmpty()) {
// run
val furthestTile = findFurthestTile(unit, unitDistanceToTiles, nearEnemyTiles)
unit.movement.moveToTile(furthestTile)
} else {
// heal
unit.fortifyIfCan()
}
return
}
// 2 - trying to capture someone
// TODO
// 3 - trying to pillage tile or trade route
if (tryPillageImprovement(unit, unitDistanceToTiles, unitActions)) return
// 4 - heal the unit if needed
if (unit.health < 100) {
healUnit(unit, unitDistanceToTiles)
return
}
// 5 - wander
UnitAutomation().wander(unit, unitDistanceToTiles)
}
private fun findFurthestTile(
unit: MapUnit,
unitDistanceToTiles: PathsToTilesWithinTurn,
nearEnemyTiles: List<AttackableTile>
): TileInfo {
val possibleTiles = unitDistanceToTiles.keys.filter { unit.movement.canMoveTo(it) }
val enemies = nearEnemyTiles.mapNotNull { it.tileToAttack.militaryUnit }
var furthestTile: Pair<TileInfo, Float> = possibleTiles.random() to 0f
for (enemy in enemies) {
for (tile in possibleTiles) {
val distance = enemy.movement.getMovementCostBetweenAdjacentTiles(enemy.currentTile, tile, enemy.civInfo)
if (distance > furthestTile.second) {
furthestTile = tile to distance
}
}
}
return furthestTile.first
}
private fun healUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
val currentUnitTile = unit.getTile()
val bestTilesForHealing = unitDistanceToTiles.keys
.filter { unit.movement.canMoveTo(it) }
.groupBy { unit.rankTileForHealing(it) }
.maxBy { it.key }
// within the tiles with best healing rate, we'll prefer one which has the highest defensive bonuses
val bestTileForHealing = bestTilesForHealing?.value?.maxBy { it.getDefensiveBonus() }
if (bestTileForHealing != null
&& currentUnitTile != bestTileForHealing
&& unit.rankTileForHealing(bestTileForHealing) > unit.rankTileForHealing(currentUnitTile)) {
unit.movement.moveToTile(bestTileForHealing)
}
unit.fortifyIfCan()
}
private fun tryUpgradeUnit(unit: MapUnit, unitActions: List<UnitAction>): Boolean {
if (unit.baseUnit().upgradesTo != null) {
val upgradedUnit = unit.civInfo.gameInfo.ruleSet.units[unit.baseUnit().upgradesTo!!]!!
if (upgradedUnit.isBuildable(unit.civInfo)) {
val upgradeAction = unitActions.firstOrNull { it.type == UnitActionType.Upgrade }
if (upgradeAction != null && upgradeAction.canAct) {
upgradeAction.action?.invoke()
return true
}
}
}
return false
}
private fun tryPillageImprovement(
unit: MapUnit,
unitDistanceToTiles: PathsToTilesWithinTurn,
unitActions: List<UnitAction>
): Boolean {
val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles
.filter { it.value.totalDistance < unit.currentMovement }.keys
.filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit, it) }
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxBy { it.getDefensiveBonus() }!!
if (unit.getTile() != tileToPillage) {
unit.movement.moveToTile(tileToPillage)
}
unitActions.first { it.type == UnitActionType.Pillage }.action?.invoke()
return true
}
}

View File

@ -0,0 +1,146 @@
package com.unciv.logic.automation
import com.unciv.Constants
import com.unciv.logic.battle.Battle
import com.unciv.logic.battle.BattleDamage
import com.unciv.logic.battle.ICombatant
import com.unciv.logic.battle.MapUnitCombatant
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.PathsToTilesWithinTurn
import com.unciv.logic.map.TileInfo
import com.unciv.models.AttackableTile
class BattleHelper {
fun tryAttackNearbyEnemy(unit: MapUnit): Boolean {
val attackableEnemies = getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
// Only take enemies we can fight without dying
.filter {
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
}
val enemyTileToAttack = chooseAttackTarget(unit, attackableEnemies)
if (enemyTileToAttack != null) {
Battle.moveAndAttack(MapUnitCombatant(unit), enemyTileToAttack)
return true
}
return false
}
fun getAttackableEnemies(
unit: MapUnit,
unitDistanceToTiles: PathsToTilesWithinTurn,
tilesToCheck: List<TileInfo>? = null
): ArrayList<AttackableTile> {
val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles)
.filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) }
val rangeOfAttack = unit.getRange()
val attackableTiles = ArrayList<AttackableTile>()
// The >0.1 (instead of >0) solves a bug where you've moved 2/3 road tiles,
// you come to move a third (distance is less that remaining movements),
// and then later we round it off to a whole.
// So the poor unit thought it could attack from the tile, but when it comes to do so it has no movement points!
// Silly floats, basically
val unitMustBeSetUp = unit.hasUnique("Must set up to ranged attack")
val tilesToAttackFrom = if (unit.type.isAirUnit()) sequenceOf(unit.currentTile)
else
unitDistanceToTiles.asSequence()
.filter {
val movementPointsToExpendAfterMovement = if (unitMustBeSetUp) 1 else 0
val movementPointsToExpendHere = if (unitMustBeSetUp && unit.action != Constants.unitActionSetUp) 1 else 0
val movementPointsToExpendBeforeAttack = if (it.key == unit.currentTile) movementPointsToExpendHere else movementPointsToExpendAfterMovement
unit.currentMovement - it.value.totalDistance - movementPointsToExpendBeforeAttack > 0.1
} // still got leftover movement points after all that, to attack (0.1 is because of Float nonsense, see MapUnit.moveToTile(...)
.map { it.key }
.filter { unit.movement.canMoveTo(it) || it == unit.getTile() }
for (reachableTile in tilesToAttackFrom) { // tiles we'll still have energy after we reach there
val tilesInAttackRange =
if (unit.hasUnique("Ranged attacks may be performed over obstacles") || unit.type.isAirUnit())
reachableTile.getTilesInDistance(rangeOfAttack)
else reachableTile.getViewableTiles(rangeOfAttack, unit.type.isWaterUnit())
attackableTiles += tilesInAttackRange.asSequence().filter { it in tilesWithEnemies }
.map { AttackableTile(reachableTile, it) }
}
return attackableTiles
}
fun containsAttackableEnemy(tile: TileInfo, combatant: ICombatant): Boolean {
if (combatant is MapUnitCombatant) {
if (combatant.unit.isEmbarked()) {
if (tile.isWater) return false // can't attack water units while embarked, only land
if (combatant.isRanged()) return false
}
if (combatant.unit.hasUnique("Can only attack water")) {
if (tile.isLand) return false
// trying to attack lake-to-coast or vice versa
if ((tile.baseTerrain == Constants.lakes) != (combatant.getTile().baseTerrain == Constants.lakes))
return false
}
}
val tileCombatant = Battle.getMapCombatantOfTile(tile) ?: return false
if (tileCombatant.getCivInfo() == combatant.getCivInfo()) return false
if (!combatant.getCivInfo().isAtWarWith(tileCombatant.getCivInfo())) return false
//only submarine and destroyer can attack submarine
//garrisoned submarine can be attacked by anyone, or the city will be in invincible
if (tileCombatant.isInvisible() && !tile.isCityCenter()) {
if (combatant is MapUnitCombatant
&& combatant.unit.hasUnique("Can attack submarines")
&& combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)) {
return true
}
return false
}
return true
}
fun tryDisembarkUnitToAttackPosition(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if (!unit.type.isMelee() || !unit.type.isLandUnit() || !unit.isEmbarked()) return false
val attackableEnemiesNextTurn = getAttackableEnemies(unit, unitDistanceToTiles)
// Only take enemies we can fight without dying
.filter {
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
}
.filter { it.tileToAttackFrom.isLand }
val enemyTileToAttackNextTurn = chooseAttackTarget(unit, attackableEnemiesNextTurn)
if (enemyTileToAttackNextTurn != null) {
unit.movement.moveToTile(enemyTileToAttackNextTurn.tileToAttackFrom)
return true
}
return false
}
private fun chooseAttackTarget(unit: MapUnit, attackableEnemies: List<AttackableTile>): AttackableTile? {
val cityTilesToAttack = attackableEnemies.filter { it.tileToAttack.isCityCenter() }
val nonCityTilesToAttack = attackableEnemies.filter { !it.tileToAttack.isCityCenter() }
// todo For air units, prefer to attack tiles with lower intercept chance
var enemyTileToAttack: AttackableTile? = null
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
.minBy { it.tileToAttack.getCity()!!.health }
if (unit.type.isMelee() && capturableCity != null)
enemyTileToAttack = capturableCity // enter it quickly, top priority!
else if (nonCityTilesToAttack.isNotEmpty()) // second priority, units
enemyTileToAttack = nonCityTilesToAttack.minBy { Battle.getMapCombatantOfTile(it.tileToAttack)!!.getHealth() }
else if (cityWithHealthLeft != null) enemyTileToAttack = cityWithHealthLeft // third priority, city
return enemyTileToAttack
}
}

View File

@ -19,27 +19,32 @@ class NextTurnAutomation{
/** Top-level AI turn tasklist */ /** Top-level AI turn tasklist */
fun automateCivMoves(civInfo: CivilizationInfo) { fun automateCivMoves(civInfo: CivilizationInfo) {
respondToDemands(civInfo) if (civInfo.isBarbarian()) {
respondToTradeRequests(civInfo) BarbarianAutomation(civInfo).automate()
if(civInfo.isMajorCiv()) {
offerPeaceTreaty(civInfo)
exchangeTechs(civInfo)
exchangeLuxuries(civInfo)
issueRequests(civInfo)
adoptPolicy(civInfo)
} else { } else {
getFreeTechForCityStates(civInfo) respondToDemands(civInfo)
respondToTradeRequests(civInfo)
if(civInfo.isMajorCiv()) {
offerPeaceTreaty(civInfo)
exchangeTechs(civInfo)
exchangeLuxuries(civInfo)
issueRequests(civInfo)
adoptPolicy(civInfo)
} else {
getFreeTechForCityStates(civInfo)
}
chooseTechToResearch(civInfo)
updateDiplomaticRelationship(civInfo)
declareWar(civInfo)
automateCityBombardment(civInfo)
useGold(civInfo)
automateUnits(civInfo)
reassignWorkedTiles(civInfo)
trainSettler(civInfo)
} }
chooseTechToResearch(civInfo)
updateDiplomaticRelationship(civInfo)
declareWar(civInfo)
automateCityBombardment(civInfo)
useGold(civInfo)
automateUnits(civInfo)
reassignWorkedTiles(civInfo)
trainSettler(civInfo)
civInfo.popupAlerts.clear() // AIs don't care about popups. civInfo.popupAlerts.clear() // AIs don't care about popups.
} }

View File

@ -15,6 +15,8 @@ import com.unciv.ui.worldscreen.unit.UnitActions
class SpecificUnitAutomation{ class SpecificUnitAutomation{
private val battleHelper = BattleHelper()
private fun hasWorkableSeaResource(tileInfo: TileInfo, civInfo: CivilizationInfo): Boolean { private fun hasWorkableSeaResource(tileInfo: TileInfo, civInfo: CivilizationInfo): Boolean {
return tileInfo.hasViewableResource(civInfo) && tileInfo.isWater && tileInfo.improvement==null return tileInfo.hasViewableResource(civInfo) && tileInfo.isWater && tileInfo.improvement==null
} }
@ -186,14 +188,14 @@ class SpecificUnitAutomation{
.flatMap { it.airUnits }.filter { it.civInfo.isAtWarWith(unit.civInfo) } .flatMap { it.airUnits }.filter { it.civInfo.isAtWarWith(unit.civInfo) }
if(enemyAirUnitsInRange.isNotEmpty()) return // we need to be on standby in case they attack if(enemyAirUnitsInRange.isNotEmpty()) return // we need to be on standby in case they attack
if(UnitAutomation().tryAttackNearbyEnemy(unit)) return if(battleHelper.tryAttackNearbyEnemy(unit)) return
val immediatelyReachableCities = tilesInRange val immediatelyReachableCities = tilesInRange
.filter { it.isCityCenter() && it.getOwner()==unit.civInfo && unit.movement.canMoveTo(it)} .filter { it.isCityCenter() && it.getOwner()==unit.civInfo && unit.movement.canMoveTo(it)}
for(city in immediatelyReachableCities){ for(city in immediatelyReachableCities){
if(city.getTilesInDistance(unit.getRange()) if(city.getTilesInDistance(unit.getRange())
.any { UnitAutomation().containsAttackableEnemy(it,MapUnitCombatant(unit)) }) { .any { battleHelper.containsAttackableEnemy(it,MapUnitCombatant(unit)) }) {
unit.movement.moveToTile(city) unit.movement.moveToTile(city)
return return
} }
@ -220,7 +222,7 @@ class SpecificUnitAutomation{
} }
fun automateBomber(unit: MapUnit) { fun automateBomber(unit: MapUnit) {
if (UnitAutomation().tryAttackNearbyEnemy(unit)) return if (battleHelper.tryAttackNearbyEnemy(unit)) return
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange()) val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
@ -229,7 +231,7 @@ class SpecificUnitAutomation{
for (city in immediatelyReachableCities) { for (city in immediatelyReachableCities) {
if (city.getTilesInDistance(unit.getRange()) if (city.getTilesInDistance(unit.getRange())
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(unit)) }) { .any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
unit.movement.moveToTile(city) unit.movement.moveToTile(city)
return return
} }
@ -245,7 +247,7 @@ class SpecificUnitAutomation{
.filter { .filter {
it != airUnit.currentTile it != airUnit.currentTile
&& it.getTilesInDistance(airUnit.getRange()) && it.getTilesInDistance(airUnit.getRange())
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(airUnit)) } .any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(airUnit)) }
} }
if (citiesThatCanAttackFrom.isEmpty()) return if (citiesThatCanAttackFrom.isEmpty()) return
@ -258,7 +260,7 @@ class SpecificUnitAutomation{
// This really needs to be changed, to have better targetting for missiles // This really needs to be changed, to have better targetting for missiles
fun automateMissile(unit: MapUnit) { fun automateMissile(unit: MapUnit) {
if (UnitAutomation().tryAttackNearbyEnemy(unit)) return if (battleHelper.tryAttackNearbyEnemy(unit)) return
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange()) val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
@ -267,7 +269,7 @@ class SpecificUnitAutomation{
for (city in immediatelyReachableCities) { for (city in immediatelyReachableCities) {
if (city.getTilesInDistance(unit.getRange()) if (city.getTilesInDistance(unit.getRange())
.any { UnitAutomation().containsAttackableEnemy(it, MapUnitCombatant(unit)) }) { .any { battleHelper.containsAttackableEnemy(it, MapUnitCombatant(unit)) }) {
unit.movement.moveToTile(city) unit.movement.moveToTile(city)
return return
} }

View File

@ -3,7 +3,10 @@ package com.unciv.logic.automation
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.battle.* 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.CityInfo import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.GreatPersonManager import com.unciv.logic.civilization.GreatPersonManager
import com.unciv.logic.civilization.diplomacy.DiplomaticStatus import com.unciv.logic.civilization.diplomacy.DiplomaticStatus
@ -15,15 +18,19 @@ import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.ruleset.unit.UnitType
import com.unciv.ui.worldscreen.unit.UnitActions import com.unciv.ui.worldscreen.unit.UnitActions
class UnitAutomation {
class UnitAutomation{
companion object { companion object {
const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5 const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
} }
private val battleHelper = BattleHelper()
fun automateUnitMoves(unit: MapUnit) { fun automateUnitMoves(unit: MapUnit) {
if (unit.civInfo.isBarbarian()) {
throw IllegalStateException("Barbarians is not allowed here.")
}
if (unit.name == Constants.settler) { if (unit.name == Constants.settler) {
return SpecificUnitAutomation().automateSettlerActions(unit) return SpecificUnitAutomation().automateSettlerActions(unit)
@ -33,38 +40,32 @@ class UnitAutomation{
return WorkerAutomation(unit).automateWorkerAction() return WorkerAutomation(unit).automateWorkerAction()
} }
if(unit.name=="Work Boats"){ if (unit.name == "Work Boats") {
return SpecificUnitAutomation().automateWorkBoats(unit) return SpecificUnitAutomation().automateWorkBoats(unit)
} }
if (unit.name == "Great General") if (unit.name == "Great General")
return SpecificUnitAutomation().automateGreatGeneral(unit) return SpecificUnitAutomation().automateGreatGeneral(unit)
if(unit.type==UnitType.Fighter) if (unit.type == UnitType.Fighter)
return SpecificUnitAutomation().automateFighter(unit) return SpecificUnitAutomation().automateFighter(unit)
if(unit.type==UnitType.Bomber) if (unit.type == UnitType.Bomber)
return SpecificUnitAutomation().automateBomber(unit) return SpecificUnitAutomation().automateBomber(unit)
if(unit.type==UnitType.Missile) if (unit.type == UnitType.Missile)
return SpecificUnitAutomation().automateMissile(unit) return SpecificUnitAutomation().automateMissile(unit)
if(unit.name.startsWith("Great") if (unit.name.startsWith("Great")
&& unit.name in GreatPersonManager().statToGreatPersonMapping.values){ // So "Great War Infantry" isn't caught here && unit.name in GreatPersonManager().statToGreatPersonMapping.values) { // So "Great War Infantry" isn't caught here
return SpecificUnitAutomation().automateGreatPerson(unit) return SpecificUnitAutomation().automateGreatPerson(unit)
} }
val unitActions = UnitActions().getUnitActions(unit,UncivGame.Current.worldScreen) val unitActions = UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
var unitDistanceToTiles = unit.movement.getDistanceToTiles() var unitDistanceToTiles = unit.movement.getDistanceToTiles()
if(unit.civInfo.isBarbarian() && if (tryGoToRuin(unit, unitDistanceToTiles)) {
unit.currentTile.improvement==Constants.barbarianEncampment && unit.type.isLandUnit()) { if (unit.currentMovement == 0f) return
if(unit.canFortify()) unit.fortify()
return // stay in the encampment
}
if(tryGoToRuin(unit,unitDistanceToTiles)){
if(unit.currentMovement==0f) return
unitDistanceToTiles = unit.movement.getDistanceToTiles() unitDistanceToTiles = unit.movement.getDistanceToTiles()
} }
@ -73,18 +74,13 @@ class UnitAutomation{
// Accompany settlers // Accompany settlers
if (tryAccompanySettlerOrGreatPerson(unit)) return if (tryAccompanySettlerOrGreatPerson(unit)) return
if (unit.health < 50 && tryHealUnit(unit,unitDistanceToTiles)) return // do nothing but heal if (unit.health < 50 && tryHealUnit(unit, unitDistanceToTiles)) return // do nothing but heal
// if a embarked melee unit can land and attack next turn, do not attack from water. // if a embarked melee unit can land and attack next turn, do not attack from water.
if (unit.type.isLandUnit() && unit.type.isMelee() && unit.isEmbarked()) { if (battleHelper.tryDisembarkUnitToAttackPosition(unit, unitDistanceToTiles)) return
if (tryDisembarkUnitToAttackPosition(unit,unitDistanceToTiles)) return
}
// if there is an attackable unit in the vicinity, attack! // if there is an attackable unit in the vicinity, attack!
if (tryAttackNearbyEnemy(unit)) return if (battleHelper.tryAttackNearbyEnemy(unit)) return
// Barbarians try to pillage improvements if no targets reachable
if (unit.civInfo.isBarbarian() && tryPillageImprovement(unit, unitDistanceToTiles)) return
if (tryGarrisoningUnit(unit)) return if (tryGarrisoningUnit(unit)) return
@ -98,47 +94,42 @@ class 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(tryHeadTowardsEncampment(unit)) return if (tryHeadTowardsEncampment(unit)) return
// else, try to go o unreached tiles // else, try to go o unreached tiles
if(tryExplore(unit,unitDistanceToTiles)) return if (tryExplore(unit, unitDistanceToTiles)) return
// Barbarians just wander all over the place
if(unit.civInfo.isBarbarian())
wander(unit,unitDistanceToTiles)
} }
private fun tryHeadTowardsEncampment(unit: MapUnit): Boolean { private fun tryHeadTowardsEncampment(unit: MapUnit): Boolean {
if(unit.civInfo.isBarbarian()) return false if (unit.type == UnitType.Missile) return false // don't use missiles against barbarians...
if(unit.type==UnitType.Missile) return false // don't use missiles against barbarians...
val knownEncampments = unit.civInfo.gameInfo.tileMap.values.asSequence() val knownEncampments = unit.civInfo.gameInfo.tileMap.values.asSequence()
.filter { it.improvement==Constants.barbarianEncampment && unit.civInfo.exploredTiles.contains(it.position) } .filter { it.improvement == Constants.barbarianEncampment && unit.civInfo.exploredTiles.contains(it.position) }
val cities = unit.civInfo.cities val cities = unit.civInfo.cities
val encampmentsCloseToCities val encampmentsCloseToCities = knownEncampments.filter { cities.any { city -> city.getCenterTile().arialDistanceTo(it) < 6 } }
= knownEncampments.filter { cities.any { city -> city.getCenterTile().arialDistanceTo(it) < 6 } }
.sortedBy { it.arialDistanceTo(unit.currentTile) } .sortedBy { it.arialDistanceTo(unit.currentTile) }
val encampmentToHeadTowards = encampmentsCloseToCities.firstOrNull { unit.movement.canReach(it) } val encampmentToHeadTowards = encampmentsCloseToCities.firstOrNull { unit.movement.canReach(it) }
if(encampmentToHeadTowards==null) return false if (encampmentToHeadTowards == null) {
return false
}
unit.movement.headTowards(encampmentToHeadTowards) unit.movement.headTowards(encampmentToHeadTowards)
return true return true
} }
private fun tryHealUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
fun tryHealUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn):Boolean {
val tilesInDistance = unitDistanceToTiles.keys.filter { unit.movement.canMoveTo(it) } val tilesInDistance = unitDistanceToTiles.keys.filter { unit.movement.canMoveTo(it) }
if(unitDistanceToTiles.isEmpty()) return true // can't move, so... if (unitDistanceToTiles.isEmpty()) return true // can't move, so...
val currentUnitTile = unit.getTile() val currentUnitTile = unit.getTile()
if (tryPillageImprovement(unit, unitDistanceToTiles)) return true if (tryPillageImprovement(unit, unitDistanceToTiles)) return true
val tilesByHealingRate = tilesInDistance.groupBy { unit.rankTileForHealing(it) } val tilesByHealingRate = tilesInDistance.groupBy { unit.rankTileForHealing(it) }
if(tilesByHealingRate.keys.none { it!=0 }){// We can't heal here at all! We're probably embarked if (tilesByHealingRate.keys.none { it != 0 }) { // We can't heal here at all! We're probably embarked
val reachableCityTile = unit.civInfo.cities.map { it.getCenterTile() } val reachableCityTile = unit.civInfo.cities.map { it.getCenterTile() }
.sortedBy { it.arialDistanceTo(unit.currentTile) } .sortedBy { it.arialDistanceTo(unit.currentTile) }
.firstOrNull{unit.movement.canReach(it)} .firstOrNull { unit.movement.canReach(it) }
if(reachableCityTile!=null) unit.movement.headTowards(reachableCityTile) if (reachableCityTile != null) unit.movement.headTowards(reachableCityTile)
else wander(unit,unitDistanceToTiles) else wander(unit, unitDistanceToTiles)
return true return true
} }
@ -147,23 +138,23 @@ class UnitAutomation{
val bestTileForHealing = bestTilesForHealing.maxBy { it.getDefensiveBonus() }!! val bestTileForHealing = bestTilesForHealing.maxBy { it.getDefensiveBonus() }!!
val bestTileForHealingRank = unit.rankTileForHealing(bestTileForHealing) val bestTileForHealingRank = unit.rankTileForHealing(bestTileForHealing)
if(currentUnitTile!=bestTileForHealing if (currentUnitTile != bestTileForHealing
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile)) && bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile))
unit.movement.moveToTile(bestTileForHealing) unit.movement.moveToTile(bestTileForHealing)
if(unit.currentMovement>0 && unit.canFortify()) unit.fortify() if (unit.currentMovement > 0 && unit.canFortify()) unit.fortify()
return true return true
} }
fun tryPillageImprovement(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) : Boolean { private fun tryPillageImprovement(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if(unit.type.isCivilian()) return false if (unit.type.isCivilian()) return false
val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles
.filter {it.value.totalDistance < unit.currentMovement}.keys .filter { it.value.totalDistance < unit.currentMovement }.keys
.filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit,it) } .filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit, it) }
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxBy { it.getDefensiveBonus() }!! val tileToPillage = tilesThatCanWalkToAndThenPillage.maxBy { it.getDefensiveBonus() }!!
if (unit.getTile()!=tileToPillage) if (unit.getTile() != tileToPillage)
unit.movement.moveToTile(tileToPillage) unit.movement.moveToTile(tileToPillage)
UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen) UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
@ -171,86 +162,9 @@ class UnitAutomation{
return true return true
} }
fun containsAttackableEnemy(tile: TileInfo, combatant: ICombatant): Boolean {
if(combatant is MapUnitCombatant) {
if (combatant.unit.isEmbarked()) {
if (tile.isWater) return false // can't attack water units while embarked, only land
if (combatant.isRanged()) return false
}
if (combatant.unit.hasUnique("Can only attack water")) {
if (tile.isLand) return false
// trying to attack lake-to-coast or vice versa
if ((tile.baseTerrain == Constants.lakes) != (combatant.getTile().baseTerrain == Constants.lakes))
return false
}
}
val tileCombatant = Battle.getMapCombatantOfTile(tile)
if(tileCombatant==null) return false
if(tileCombatant.getCivInfo()==combatant.getCivInfo() ) return false
if(!combatant.getCivInfo().isAtWarWith(tileCombatant.getCivInfo())) return false
//only submarine and destroyer can attack submarine
//garrisoned submarine can be attacked by anyone, or the city will be in invincible
if (tileCombatant.isInvisible() && !tile.isCityCenter()) {
if (combatant is MapUnitCombatant
&& combatant.unit.hasUnique("Can attack submarines")
&& combatant.getCivInfo().viewableInvisibleUnitsTiles.map { it.position }.contains(tile.position)) {
return true
}
return false
}
return true
}
class AttackableTile(val tileToAttackFrom:TileInfo, val tileToAttack:TileInfo)
fun getAttackableEnemies(
unit: MapUnit,
unitDistanceToTiles: PathsToTilesWithinTurn,
tilesToCheck: List<TileInfo>? = null
): ArrayList<AttackableTile> {
val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles)
.filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) }
val rangeOfAttack = unit.getRange()
val attackableTiles = ArrayList<AttackableTile>()
// The >0.1 (instead of >0) solves a bug where you've moved 2/3 road tiles,
// you come to move a third (distance is less that remaining movements),
// and then later we round it off to a whole.
// So the poor unit thought it could attack from the tile, but when it comes to do so it has no movement points!
// Silly floats, basically
val unitMustBeSetUp = unit.hasUnique("Must set up to ranged attack")
val tilesToAttackFrom = if (unit.type.isAirUnit()) sequenceOf(unit.currentTile)
else
unitDistanceToTiles.asSequence()
.filter {
val movementPointsToExpendAfterMovement = if (unitMustBeSetUp) 1 else 0
val movementPointsToExpendHere = if (unitMustBeSetUp && unit.action != Constants.unitActionSetUp) 1 else 0
val movementPointsToExpendBeforeAttack = if (it.key == unit.currentTile) movementPointsToExpendHere else movementPointsToExpendAfterMovement
unit.currentMovement - it.value.totalDistance - movementPointsToExpendBeforeAttack > 0.1
} // still got leftover movement points after all that, to attack (0.1 is because of Float nonsense, see MapUnit.moveToTile(...)
.map { it.key }
.filter { unit.movement.canMoveTo(it) || it == unit.getTile() }
for (reachableTile in tilesToAttackFrom) { // tiles we'll still have energy after we reach there
val tilesInAttackRange =
if (unit.hasUnique("Ranged attacks may be performed over obstacles") || unit.type.isAirUnit())
reachableTile.getTilesInDistance(rangeOfAttack)
else reachableTile.getViewableTiles(rangeOfAttack, unit.type.isWaterUnit())
attackableTiles += tilesInAttackRange.asSequence().filter { it in tilesWithEnemies }
.map { AttackableTile(reachableTile, it) }
}
return attackableTiles
}
fun getBombardTargets(city: CityInfo): List<TileInfo> { fun getBombardTargets(city: CityInfo): List<TileInfo> {
return city.getCenterTile().getViewableTiles(city.range,true) return city.getCenterTile().getViewableTiles(city.range, true)
.filter { containsAttackableEnemy(it, CityCombatant(city)) } .filter { battleHelper.containsAttackableEnemy(it, CityCombatant(city)) }
} }
/** Move towards the closest attackable enemy of the [unit]. /** Move towards the closest attackable enemy of the [unit].
@ -260,20 +174,21 @@ class UnitAutomation{
private fun tryAdvanceTowardsCloseEnemy(unit: MapUnit): Boolean { private fun tryAdvanceTowardsCloseEnemy(unit: MapUnit): Boolean {
// this can be sped up if we check each layer separately // this can be sped up if we check each layer separately
val unitDistanceToTiles = unit.movement.getDistanceToTilesWithinTurn( val unitDistanceToTiles = unit.movement.getDistanceToTilesWithinTurn(
unit.getTile().position, unit.getTile().position,
unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT
) )
var closeEnemies = getAttackableEnemies( var closeEnemies = battleHelper.getAttackableEnemies(
unit, unit,
unitDistanceToTiles, unitDistanceToTiles,
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT) tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT)
).filter { // Ignore units that would 1-shot you if you attacked ).filter {
// Ignore units that would 1-shot you if you attacked
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit), BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
} }
if(unit.type.isRanged()) if (unit.type.isRanged())
closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health==1 } closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health == 1 }
val closestEnemy = closeEnemies.minBy { it.tileToAttack.arialDistanceTo(unit.getTile()) } val closestEnemy = closeEnemies.minBy { it.tileToAttack.arialDistanceTo(unit.getTile()) }
@ -286,10 +201,12 @@ class UnitAutomation{
private fun tryAccompanySettlerOrGreatPerson(unit: MapUnit): Boolean { private fun tryAccompanySettlerOrGreatPerson(unit: MapUnit): Boolean {
val settlerOrGreatPersonToAccompany = unit.civInfo.getCivUnits() val settlerOrGreatPersonToAccompany = unit.civInfo.getCivUnits()
.firstOrNull { val tile = it.currentTile .firstOrNull {
(it.name== Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values) val tile = it.currentTile
&& tile.militaryUnit==null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile) } (it.name == Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
if(settlerOrGreatPersonToAccompany==null) return false && tile.militaryUnit == null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile)
}
if (settlerOrGreatPersonToAccompany == null) return false
unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile) unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile)
return true return true
} }
@ -309,19 +226,20 @@ class UnitAutomation{
} }
private fun tryHeadTowardsEnemyCity(unit: MapUnit): Boolean { private fun tryHeadTowardsEnemyCity(unit: MapUnit): Boolean {
if(unit.civInfo.cities.isEmpty()) return false if (unit.civInfo.cities.isEmpty()) return false
var enemyCities = unit.civInfo.gameInfo.civilizations var enemyCities = unit.civInfo.gameInfo.civilizations
.filter { unit.civInfo.isAtWarWith(it) } .filter { unit.civInfo.isAtWarWith(it) }
.flatMap { it.cities }.asSequence() .flatMap { it.cities }.asSequence()
.filter { it.location in unit.civInfo.exploredTiles } .filter { it.location in unit.civInfo.exploredTiles }
if(unit.type.isRanged()) // ranged units don't harm capturable cities, waste of a turn if (unit.type.isRanged()) // ranged units don't harm capturable cities, waste of a turn
enemyCities = enemyCities.filterNot { it.health==1 } enemyCities = enemyCities.filterNot { it.health == 1 }
val closestReachableEnemyCity = enemyCities val closestReachableEnemyCity = enemyCities
.asSequence().map { it.getCenterTile() } .asSequence().map { it.getCenterTile() }
.sortedBy { cityCenterTile -> // sort enemy cities by closeness to our cities, and only then choose the first reachable - checking canReach is comparatively very time-intensive! .sortedBy { cityCenterTile ->
// sort enemy cities by closeness to our cities, and only then choose the first reachable - checking canReach is comparatively very time-intensive!
unit.civInfo.cities.asSequence().map { cityCenterTile.arialDistanceTo(it.getCenterTile()) }.min()!! unit.civInfo.cities.asSequence().map { cityCenterTile.arialDistanceTo(it.getCenterTile()) }.min()!!
} }
.firstOrNull { unit.movement.canReach(it) } .firstOrNull { unit.movement.canReach(it) }
@ -330,44 +248,38 @@ class UnitAutomation{
val unitDistanceToTiles = unit.movement.getDistanceToTiles() val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2) val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2)
val reachableTilesNotInBombardRange = unitDistanceToTiles.keys.filter { it !in tilesInBombardRange } val reachableTilesNotInBombardRange = unitDistanceToTiles.keys.filter { it !in tilesInBombardRange }
val canMoveIntoBombardRange = tilesInBombardRange.any { unitDistanceToTiles.containsKey(it)}
val suitableGatheringGroundTiles = closestReachableEnemyCity.getTilesAtDistance(4) val suitableGatheringGroundTiles = closestReachableEnemyCity.getTilesAtDistance(4)
.union(closestReachableEnemyCity.getTilesAtDistance(3)) .union(closestReachableEnemyCity.getTilesAtDistance(3))
.filter { it.isLand } .filter { it.isLand }
val closestReachableLandingGroundTile = suitableGatheringGroundTiles
.sortedBy { it.arialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it) }
// don't head straight to the city, try to head to landing grounds - // 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. // this is against tha AI's brilliant plan of having everyone embarked and attacking via sea when unnecessary.
val tileToHeadTo = if(closestReachableLandingGroundTile!=null) closestReachableLandingGroundTile val tileToHeadTo = suitableGatheringGroundTiles
else closestReachableEnemyCity .sortedBy { it.arialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it) } ?: closestReachableEnemyCity
if (tileToHeadTo !in tilesInBombardRange) // no need to worry, keep going as the movement alg. says
if(tileToHeadTo !in tilesInBombardRange) // no need to worry, keep going as the movement alg. says
unit.movement.headTowards(tileToHeadTo) unit.movement.headTowards(tileToHeadTo)
else {
else{ if (unit.getRange() > 2) { // should never be in a bombardable position
if(unit.getRange()>2){ // should never be in a bombardable position
val tilesCanAttackFromButNotInBombardRange = val tilesCanAttackFromButNotInBombardRange =
reachableTilesNotInBombardRange.filter{it.arialDistanceTo(closestReachableEnemyCity) <= unit.getRange()} reachableTilesNotInBombardRange.filter { it.arialDistanceTo(closestReachableEnemyCity) <= unit.getRange() }
// move into position far away enough that the bombard doesn't hurt // move into position far away enough that the bombard doesn't hurt
if(tilesCanAttackFromButNotInBombardRange.any()) if (tilesCanAttackFromButNotInBombardRange.any())
unit.movement.headTowards(tilesCanAttackFromButNotInBombardRange.minBy { unitDistanceToTiles[it]!!.totalDistance }!!) unit.movement.headTowards(tilesCanAttackFromButNotInBombardRange.minBy { unitDistanceToTiles[it]!!.totalDistance }!!)
} } else {
else {
// calculate total damage of units in surrounding 4-spaces from enemy city (so we can attack a city from 2 directions at once) // 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) val militaryUnitsAroundEnemyCity = closestReachableEnemyCity.getTilesInDistance(3)
.filter { it.militaryUnit!=null && it.militaryUnit!!.civInfo == unit.civInfo } .filter { it.militaryUnit != null && it.militaryUnit!!.civInfo == unit.civInfo }
.map { it.militaryUnit!! } .map { it.militaryUnit!! }
var totalAttackOnCityPerTurn = -20 // cities heal 20 per turn, so anything below that its useless var totalAttackOnCityPerTurn = -20 // cities heal 20 per turn, so anything below that its useless
val enemyCityCombatant = CityCombatant(closestReachableEnemyCity.getCity()!!) val enemyCityCombatant = CityCombatant(closestReachableEnemyCity.getCity()!!)
for(militaryUnit in militaryUnitsAroundEnemyCity){ for (militaryUnit in militaryUnitsAroundEnemyCity) {
totalAttackOnCityPerTurn += BattleDamage().calculateDamageToDefender(MapUnitCombatant(militaryUnit), enemyCityCombatant) totalAttackOnCityPerTurn += BattleDamage().calculateDamageToDefender(MapUnitCombatant(militaryUnit), enemyCityCombatant)
} }
if(totalAttackOnCityPerTurn * 3 > closestReachableEnemyCity.getCity()!!.health) // if we can defeat it in 3 turns with the current units, 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! unit.movement.headTowards(closestReachableEnemyCity) // go for it!
} }
} }
@ -377,42 +289,6 @@ class UnitAutomation{
return false return false
} }
private fun tryDisembarkUnitToAttackPosition(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if (!unit.type.isMelee() || !unit.type.isLandUnit() || !unit.isEmbarked()) return false
val attackableEnemiesNextTurn = getAttackableEnemies(unit, unitDistanceToTiles)
// Only take enemies we can fight without dying
.filter {
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
}
.filter {it.tileToAttackFrom.isLand}
val enemyTileToAttackNextTurn = chooseAttackTarget(unit, attackableEnemiesNextTurn)
if (enemyTileToAttackNextTurn != null) {
unit.movement.moveToTile(enemyTileToAttackNextTurn.tileToAttackFrom)
return true
}
return false
}
fun tryAttackNearbyEnemy(unit: MapUnit): Boolean {
val attackableEnemies = getAttackableEnemies(unit, unit.movement.getDistanceToTiles())
// Only take enemies we can fight without dying
.filter {
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
}
val enemyTileToAttack = chooseAttackTarget(unit, attackableEnemies)
if (enemyTileToAttack != null) {
Battle.moveAndAttack(MapUnitCombatant(unit), enemyTileToAttack)
return true
}
return false
}
fun tryBombardEnemy(city: CityInfo): Boolean { fun tryBombardEnemy(city: CityInfo): Boolean {
if (!city.attackedThisTurn) { if (!city.attackedThisTurn) {
val target = chooseBombardTarget(city) val target = chooseBombardTarget(city)
@ -424,48 +300,26 @@ class UnitAutomation{
return false return false
} }
private fun chooseAttackTarget(unit: MapUnit, attackableEnemies: List<AttackableTile>): AttackableTile? { private fun chooseBombardTarget(city: CityInfo): TileInfo? {
val cityTilesToAttack = attackableEnemies.filter { it.tileToAttack.isCityCenter() }
val nonCityTilesToAttack = attackableEnemies.filter { !it.tileToAttack.isCityCenter() }
// todo For air units, prefer to attack tiles with lower intercept chance
var enemyTileToAttack: AttackableTile? = null
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
.minBy { it.tileToAttack.getCity()!!.health }
if (unit.type.isMelee() && capturableCity!=null)
enemyTileToAttack = capturableCity // enter it quickly, top priority!
else if (nonCityTilesToAttack.isNotEmpty()) // second priority, units
enemyTileToAttack = nonCityTilesToAttack.minBy { Battle.getMapCombatantOfTile(it.tileToAttack)!!.getHealth() }
else if (cityWithHealthLeft!=null) enemyTileToAttack = cityWithHealthLeft// third priority, city
return enemyTileToAttack
}
private fun chooseBombardTarget(city: CityInfo) : TileInfo? {
var targets = getBombardTargets(city) var targets = getBombardTargets(city)
if (targets.isEmpty()) return null if (targets.isEmpty()) return null
val siegeUnits = targets val siegeUnits = targets
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType()==UnitType.Siege } .filter { Battle.getMapCombatantOfTile(it)!!.getUnitType() == UnitType.Siege }
if(siegeUnits.any()) targets = siegeUnits if (siegeUnits.any()) targets = siegeUnits
else{ else {
val rangedUnits = targets val rangedUnits = targets
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType().isRanged() } .filter { Battle.getMapCombatantOfTile(it)!!.getUnitType().isRanged() }
if(rangedUnits.any()) targets=rangedUnits if (rangedUnits.any()) targets = rangedUnits
} }
return targets.minBy { Battle.getMapCombatantOfTile(it)!!.getHealth() } return targets.minBy { Battle.getMapCombatantOfTile(it)!!.getHealth() }
} }
private fun tryGarrisoningUnit(unit: MapUnit): Boolean { private fun tryGarrisoningUnit(unit: MapUnit): Boolean {
if(unit.type.isMelee() || unit.type.isWaterUnit()) return false // don't garrison melee units, they're not that good at it if (unit.type.isMelee() || unit.type.isWaterUnit()) return false // don't garrison melee units, they're not that good at it
val citiesWithoutGarrison = unit.civInfo.cities.filter { val citiesWithoutGarrison = unit.civInfo.cities.filter {
val centerTile = it.getCenterTile() val centerTile = it.getCenterTile()
centerTile.militaryUnit==null centerTile.militaryUnit == null
&& unit.movement.canMoveTo(centerTile) && unit.movement.canMoveTo(centerTile)
} }
fun isCityThatNeedsDefendingInWartime(city: CityInfo): Boolean { fun isCityThatNeedsDefendingInWartime(city: CityInfo): Boolean {
@ -473,69 +327,70 @@ class UnitAutomation{
for (enemyCivCity in unit.civInfo.diplomacy.values for (enemyCivCity in unit.civInfo.diplomacy.values
.filter { it.diplomaticStatus == DiplomaticStatus.War } .filter { it.diplomaticStatus == DiplomaticStatus.War }
.map { it.otherCiv() }.flatMap { it.cities }) .map { it.otherCiv() }.flatMap { it.cities })
if (city.getCenterTile().arialDistanceTo(enemyCivCity.getCenterTile()) <= 5) return true// this is an edge city that needs defending if (city.getCenterTile().arialDistanceTo(enemyCivCity.getCenterTile()) <= 5) return true // this is an edge city that needs defending
return false return false
} }
val citiesToTry:Sequence<CityInfo> val citiesToTry: Sequence<CityInfo>
if (!unit.civInfo.isAtWar()) { if (!unit.civInfo.isAtWar()) {
if (unit.getTile().isCityCenter()) return true // It's always good to have a unit in the city center, so if you haven't found anyone around to attack, forget it. if (unit.getTile().isCityCenter()) return true // It's always good to have a unit in the city center, so if you haven't found anyone around to attack, forget it.
citiesToTry = citiesWithoutGarrison.asSequence() citiesToTry = citiesWithoutGarrison.asSequence()
} else { } else {
if (unit.getTile().isCityCenter() && if (unit.getTile().isCityCenter() &&
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
citiesToTry = citiesWithoutGarrison.asSequence() citiesToTry = citiesWithoutGarrison.asSequence()
.filter { isCityThatNeedsDefendingInWartime(it) } .filter { isCityThatNeedsDefendingInWartime(it) }
} }
val closestReachableCityNeedsDefending =citiesToTry val closestReachableCityNeedsDefending = citiesToTry
.sortedBy{ it.getCenterTile().arialDistanceTo(unit.currentTile) } .sortedBy { it.getCenterTile().arialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it.getCenterTile()) } .firstOrNull { unit.movement.canReach(it.getCenterTile()) }
if(closestReachableCityNeedsDefending==null) return false if (closestReachableCityNeedsDefending == null) return false
unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile()) unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile())
return true return true
} }
fun tryGoToRuin(unit:MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean { private fun tryGoToRuin(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if(!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins if (!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
val tileWithRuin = unitDistanceToTiles.keys val tileWithRuin = unitDistanceToTiles.keys
.firstOrNull{ it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) } .firstOrNull { it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) }
if(tileWithRuin==null) return false if (tileWithRuin == null) return false
unit.movement.moveToTile(tileWithRuin) unit.movement.moveToTile(tileWithRuin)
return true return true
} }
internal fun tryExplore(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean { internal fun tryExplore(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if(tryGoToRuin(unit,unitDistanceToTiles)) if (tryGoToRuin(unit, unitDistanceToTiles)) {
{ if (unit.currentMovement == 0f) return true
if(unit.currentMovement==0f) return true
} }
for(tile in unit.currentTile.getTilesInDistance(5)) for (tile in unit.currentTile.getTilesInDistance(5))
if(unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles if (unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
&& unit.movement.canReach(tile)){ && unit.movement.canReach(tile)) {
unit.movement.headTowards(tile) unit.movement.headTowards(tile)
return true return true
} }
return false return false
} }
fun automatedExplore(unit:MapUnit){ fun automatedExplore(unit: MapUnit) {
val unitDistanceToTiles = unit.movement.getDistanceToTiles() val unitDistanceToTiles = unit.movement.getDistanceToTiles()
if(tryGoToRuin(unit, unitDistanceToTiles) && unit.currentMovement==0f) return if (tryGoToRuin(unit, unitDistanceToTiles) && unit.currentMovement == 0f) return
if (unit.health < 80) { if (unit.health < 80) {
tryHealUnit(unit,unitDistanceToTiles) tryHealUnit(unit, unitDistanceToTiles)
return return
} }
for(i in 1..10){ for (i in 1..10) {
val unexploredTilesAtDistance = unit.getTile().getTilesAtDistance(i) val unexploredTilesAtDistance = unit.getTile().getTilesAtDistance(i)
.filter { unit.movement.canMoveTo(it) && it.position !in unit.civInfo.exploredTiles .filter {
&& unit.movement.canReach(it) } unit.movement.canMoveTo(it) && it.position !in unit.civInfo.exploredTiles
if(unexploredTilesAtDistance.isNotEmpty()){ && unit.movement.canReach(it)
}
if (unexploredTilesAtDistance.isNotEmpty()) {
unit.movement.headTowards(unexploredTilesAtDistance.random()) unit.movement.headTowards(unexploredTilesAtDistance.random())
return return
} }
@ -543,15 +398,12 @@ class UnitAutomation{
unit.civInfo.addNotification("[${unit.name}] finished exploring.", unit.currentTile.position, Color.GRAY) unit.civInfo.addNotification("[${unit.name}] finished exploring.", unit.currentTile.position, Color.GRAY)
} }
fun wander(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) { fun wander(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
val reachableTiles= unitDistanceToTiles val reachableTiles = unitDistanceToTiles
.filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) } .filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) }
val reachableTilesMaxWalkingDistance = reachableTiles.filter { it.value.totalDistance == unit.currentMovement } val reachableTilesMaxWalkingDistance = reachableTiles.filter { it.value.totalDistance == unit.currentMovement }
if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.toList().random().first) if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.toList().random().first)
else if (reachableTiles.any()) unit.movement.moveToTile(reachableTiles.toList().random().first) else if (reachableTiles.any()) unit.movement.moveToTile(reachableTiles.toList().random().first)
} }
} }

View File

@ -3,13 +3,13 @@ package com.unciv.logic.battle
import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.graphics.Color
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.automation.UnitAutomation
import com.unciv.logic.city.CityInfo import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.AlertType import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.PopupAlert import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.map.RoadStatus import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo import com.unciv.logic.map.TileInfo
import com.unciv.models.AttackableTile
import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.ruleset.unit.UnitType
import java.util.* import java.util.*
import kotlin.math.max import kotlin.math.max
@ -19,7 +19,7 @@ import kotlin.math.max
*/ */
object Battle { object Battle {
fun moveAndAttack(attacker: ICombatant, attackableTile: UnitAutomation.AttackableTile){ fun moveAndAttack(attacker: ICombatant, attackableTile: AttackableTile){
if (attacker is MapUnitCombatant) { if (attacker is MapUnitCombatant) {
attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom) attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom)
if (attacker.unit.hasUnique("Must set up to ranged attack") && attacker.unit.action != Constants.unitActionSetUp) { if (attacker.unit.hasUnique("Must set up to ranged attack") && attacker.unit.action != Constants.unitActionSetUp) {

View File

@ -262,7 +262,15 @@ class MapUnit {
return true return true
} }
fun fortify(){ action = "Fortify 0"} fun fortify() {
action = "Fortify 0"
}
fun fortifyIfCan() {
if (canFortify()) {
fortify()
}
}
fun adjacentHealingBonus():Int{ fun adjacentHealingBonus():Int{
var healingBonus = 0 var healingBonus = 0

View File

@ -7,7 +7,7 @@ import com.unciv.logic.civilization.CivilizationInfo
class UnitMovementAlgorithms(val unit:MapUnit) { class UnitMovementAlgorithms(val unit:MapUnit) {
// This function is called ALL THE TIME and should be as time-optimal as possible! // This function is called ALL THE TIME and should be as time-optimal as possible!
private fun getMovementCostBetweenAdjacentTiles(from: TileInfo, to: TileInfo, civInfo: CivilizationInfo): Float { fun getMovementCostBetweenAdjacentTiles(from: TileInfo, to: TileInfo, civInfo: CivilizationInfo): Float {
if ((from.isLand != to.isLand) && unit.type.isLandUnit()) if ((from.isLand != to.isLand) && unit.type.isLandUnit())
return 100f // this is embarkment or disembarkment, and will take the entire turn return 100f // this is embarkment or disembarkment, and will take the entire turn

View File

@ -0,0 +1,5 @@
package com.unciv.models
import com.unciv.logic.map.TileInfo
class AttackableTile(val tileToAttackFrom: TileInfo, val tileToAttack: TileInfo)

View File

@ -9,6 +9,7 @@ import com.badlogic.gdx.scenes.scene2d.actions.FloatAction
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.unciv.Constants import com.unciv.Constants
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.automation.BattleHelper
import com.unciv.logic.automation.UnitAutomation import com.unciv.logic.automation.UnitAutomation
import com.unciv.logic.city.CityInfo import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo import com.unciv.logic.civilization.CivilizationInfo
@ -244,7 +245,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
val unitType = unit.type val unitType = unit.type
val attackableTiles: List<TileInfo> = if (unitType.isCivilian()) listOf() val attackableTiles: List<TileInfo> = if (unitType.isCivilian()) listOf()
else { else {
val tiles = UnitAutomation().getAttackableEnemies(unit, unit.movement.getDistanceToTiles()).map { it.tileToAttack } val tiles = BattleHelper().getAttackableEnemies(unit, unit.movement.getDistanceToTiles()).map { it.tileToAttack }
tiles.filter { (UncivGame.Current.viewEntireMapForDebug || playerViewableTilePositions.contains(it.position)) } tiles.filter { (UncivGame.Current.viewEntireMapForDebug || playerViewableTilePositions.contains(it.position)) }
} }

View File

@ -9,8 +9,10 @@ import com.badlogic.gdx.scenes.scene2d.ui.Label
import com.badlogic.gdx.scenes.scene2d.ui.Table import com.badlogic.gdx.scenes.scene2d.ui.Table
import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.scenes.scene2d.ui.TextButton
import com.unciv.UncivGame import com.unciv.UncivGame
import com.unciv.logic.automation.BattleHelper
import com.unciv.logic.automation.UnitAutomation import com.unciv.logic.automation.UnitAutomation
import com.unciv.logic.battle.* import com.unciv.logic.battle.*
import com.unciv.models.AttackableTile
import com.unciv.models.translations.tr import com.unciv.models.translations.tr
import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.ruleset.unit.UnitType
import com.unciv.ui.utils.* import com.unciv.ui.utils.*
@ -20,7 +22,9 @@ import kotlin.math.max
class BattleTable(val worldScreen: WorldScreen): Table() { class BattleTable(val worldScreen: WorldScreen): Table() {
init{ private val battleHelper = BattleHelper()
init {
isVisible = false isVisible = false
skin = CameraStageBaseScreen.skin skin = CameraStageBaseScreen.skin
background = ImageGetter.getBackground(ImageGetter.getBlue()) background = ImageGetter.getBackground(ImageGetter.getBlue())
@ -170,11 +174,11 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
} }
val attackButton = TextButton(attackText.tr(), skin).apply { color= Color.RED } val attackButton = TextButton(attackText.tr(), skin).apply { color= Color.RED }
var attackableEnemy : UnitAutomation.AttackableTile? = null var attackableEnemy : AttackableTile? = null
if (attacker.canAttack()) { if (attacker.canAttack()) {
if (attacker is MapUnitCombatant) { if (attacker is MapUnitCombatant) {
attackableEnemy = UnitAutomation() attackableEnemy = battleHelper
.getAttackableEnemies(attacker.unit, attacker.unit.movement.getDistanceToTiles()) .getAttackableEnemies(attacker.unit, attacker.unit.movement.getDistanceToTiles())
.firstOrNull{ it.tileToAttack == defender.getTile()} .firstOrNull{ it.tileToAttack == defender.getTile()}
} }
@ -182,7 +186,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
{ {
val canBombard = UnitAutomation().getBombardTargets(attacker.city).contains(defender.getTile()) val canBombard = UnitAutomation().getBombardTargets(attacker.city).contains(defender.getTile())
if (canBombard) { if (canBombard) {
attackableEnemy = UnitAutomation.AttackableTile(attacker.getTile(), defender.getTile()) attackableEnemy = AttackableTile(attacker.getTile(), defender.getTile())
} }
} }
} }