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 */
fun automateCivMoves(civInfo: CivilizationInfo) {
respondToDemands(civInfo)
respondToTradeRequests(civInfo)
if(civInfo.isMajorCiv()) {
offerPeaceTreaty(civInfo)
exchangeTechs(civInfo)
exchangeLuxuries(civInfo)
issueRequests(civInfo)
adoptPolicy(civInfo)
if (civInfo.isBarbarian()) {
BarbarianAutomation(civInfo).automate()
} 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.
}

View File

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

View File

@ -3,7 +3,10 @@ package com.unciv.logic.automation
import com.badlogic.gdx.graphics.Color
import com.unciv.Constants
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.civilization.GreatPersonManager
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.ui.worldscreen.unit.UnitActions
class UnitAutomation{
class UnitAutomation {
companion object {
const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
}
private val battleHelper = BattleHelper()
fun automateUnitMoves(unit: MapUnit) {
if (unit.civInfo.isBarbarian()) {
throw IllegalStateException("Barbarians is not allowed here.")
}
if (unit.name == Constants.settler) {
return SpecificUnitAutomation().automateSettlerActions(unit)
@ -33,38 +40,32 @@ class UnitAutomation{
return WorkerAutomation(unit).automateWorkerAction()
}
if(unit.name=="Work Boats"){
if (unit.name == "Work Boats") {
return SpecificUnitAutomation().automateWorkBoats(unit)
}
if (unit.name == "Great General")
return SpecificUnitAutomation().automateGreatGeneral(unit)
if(unit.type==UnitType.Fighter)
if (unit.type == UnitType.Fighter)
return SpecificUnitAutomation().automateFighter(unit)
if(unit.type==UnitType.Bomber)
if (unit.type == UnitType.Bomber)
return SpecificUnitAutomation().automateBomber(unit)
if(unit.type==UnitType.Missile)
if (unit.type == UnitType.Missile)
return SpecificUnitAutomation().automateMissile(unit)
if(unit.name.startsWith("Great")
&& unit.name in GreatPersonManager().statToGreatPersonMapping.values){ // So "Great War Infantry" isn't caught here
if (unit.name.startsWith("Great")
&& unit.name in GreatPersonManager().statToGreatPersonMapping.values) { // So "Great War Infantry" isn't caught here
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()
if(unit.civInfo.isBarbarian() &&
unit.currentTile.improvement==Constants.barbarianEncampment && unit.type.isLandUnit()) {
if(unit.canFortify()) unit.fortify()
return // stay in the encampment
}
if(tryGoToRuin(unit,unitDistanceToTiles)){
if(unit.currentMovement==0f) return
if (tryGoToRuin(unit, unitDistanceToTiles)) {
if (unit.currentMovement == 0f) return
unitDistanceToTiles = unit.movement.getDistanceToTiles()
}
@ -73,18 +74,13 @@ class UnitAutomation{
// Accompany settlers
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 (unit.type.isLandUnit() && unit.type.isMelee() && unit.isEmbarked()) {
if (tryDisembarkUnitToAttackPosition(unit,unitDistanceToTiles)) return
}
if (battleHelper.tryDisembarkUnitToAttackPosition(unit, unitDistanceToTiles)) return
// if there is an attackable unit in the vicinity, attack!
if (tryAttackNearbyEnemy(unit)) return
// Barbarians try to pillage improvements if no targets reachable
if (unit.civInfo.isBarbarian() && tryPillageImprovement(unit, unitDistanceToTiles)) return
if (battleHelper.tryAttackNearbyEnemy(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
if (tryHeadTowardsEnemyCity(unit)) return
if(tryHeadTowardsEncampment(unit)) return
if (tryHeadTowardsEncampment(unit)) return
// else, try to go o unreached tiles
if(tryExplore(unit,unitDistanceToTiles)) return
// Barbarians just wander all over the place
if(unit.civInfo.isBarbarian())
wander(unit,unitDistanceToTiles)
if (tryExplore(unit, unitDistanceToTiles)) return
}
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()
.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 encampmentsCloseToCities
= knownEncampments.filter { cities.any { city -> city.getCenterTile().arialDistanceTo(it) < 6 } }
val encampmentsCloseToCities = knownEncampments.filter { cities.any { city -> city.getCenterTile().arialDistanceTo(it) < 6 } }
.sortedBy { it.arialDistanceTo(unit.currentTile) }
val encampmentToHeadTowards = encampmentsCloseToCities.firstOrNull { unit.movement.canReach(it) }
if(encampmentToHeadTowards==null) return false
if (encampmentToHeadTowards == null) {
return false
}
unit.movement.headTowards(encampmentToHeadTowards)
return true
}
fun tryHealUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn):Boolean {
private fun tryHealUnit(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
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()
if (tryPillageImprovement(unit, unitDistanceToTiles)) return true
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() }
.sortedBy { it.arialDistanceTo(unit.currentTile) }
.firstOrNull{unit.movement.canReach(it)}
if(reachableCityTile!=null) unit.movement.headTowards(reachableCityTile)
else wander(unit,unitDistanceToTiles)
.firstOrNull { unit.movement.canReach(it) }
if (reachableCityTile != null) unit.movement.headTowards(reachableCityTile)
else wander(unit, unitDistanceToTiles)
return true
}
@ -147,23 +138,23 @@ class UnitAutomation{
val bestTileForHealing = bestTilesForHealing.maxBy { it.getDefensiveBonus() }!!
val bestTileForHealingRank = unit.rankTileForHealing(bestTileForHealing)
if(currentUnitTile!=bestTileForHealing
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile))
if (currentUnitTile != bestTileForHealing
&& bestTileForHealingRank > unit.rankTileForHealing(currentUnitTile))
unit.movement.moveToTile(bestTileForHealing)
if(unit.currentMovement>0 && unit.canFortify()) unit.fortify()
if (unit.currentMovement > 0 && unit.canFortify()) unit.fortify()
return true
}
fun tryPillageImprovement(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) : Boolean {
if(unit.type.isCivilian()) return false
private fun tryPillageImprovement(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if (unit.type.isCivilian()) return false
val tilesThatCanWalkToAndThenPillage = unitDistanceToTiles
.filter {it.value.totalDistance < unit.currentMovement}.keys
.filter { unit.movement.canMoveTo(it) && UnitActions().canPillage(unit,it) }
.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)
if (unit.getTile() != tileToPillage)
unit.movement.moveToTile(tileToPillage)
UnitActions().getUnitActions(unit, UncivGame.Current.worldScreen)
@ -171,86 +162,9 @@ class UnitAutomation{
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> {
return city.getCenterTile().getViewableTiles(city.range,true)
.filter { containsAttackableEnemy(it, CityCombatant(city)) }
return city.getCenterTile().getViewableTiles(city.range, true)
.filter { battleHelper.containsAttackableEnemy(it, CityCombatant(city)) }
}
/** Move towards the closest attackable enemy of the [unit].
@ -260,20 +174,21 @@ class UnitAutomation{
private fun tryAdvanceTowardsCloseEnemy(unit: MapUnit): Boolean {
// this can be sped up if we check each layer separately
val unitDistanceToTiles = unit.movement.getDistanceToTilesWithinTurn(
unit.getTile().position,
unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT
unit.getTile().position,
unit.getMaxMovement() * CLOSE_ENEMY_TURNS_AWAY_LIMIT
)
var closeEnemies = getAttackableEnemies(
unit,
unitDistanceToTiles,
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT)
).filter { // Ignore units that would 1-shot you if you attacked
var closeEnemies = battleHelper.getAttackableEnemies(
unit,
unitDistanceToTiles,
tilesToCheck = unit.getTile().getTilesInDistance(CLOSE_ENEMY_TILES_AWAY_LIMIT)
).filter {
// Ignore units that would 1-shot you if you attacked
BattleDamage().calculateDamageToAttacker(MapUnitCombatant(unit),
Battle.getMapCombatantOfTile(it.tileToAttack)!!) < unit.health
}
if(unit.type.isRanged())
closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health==1 }
if (unit.type.isRanged())
closeEnemies = closeEnemies.filterNot { it.tileToAttack.isCityCenter() && it.tileToAttack.getCity()!!.health == 1 }
val closestEnemy = closeEnemies.minBy { it.tileToAttack.arialDistanceTo(unit.getTile()) }
@ -286,10 +201,12 @@ class UnitAutomation{
private fun tryAccompanySettlerOrGreatPerson(unit: MapUnit): Boolean {
val settlerOrGreatPersonToAccompany = unit.civInfo.getCivUnits()
.firstOrNull { val tile = it.currentTile
(it.name== Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
&& tile.militaryUnit==null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile) }
if(settlerOrGreatPersonToAccompany==null) return false
.firstOrNull {
val tile = it.currentTile
(it.name == Constants.settler || it.name in GreatPersonManager().statToGreatPersonMapping.values)
&& tile.militaryUnit == null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile)
}
if (settlerOrGreatPersonToAccompany == null) return false
unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile)
return true
}
@ -309,19 +226,20 @@ class UnitAutomation{
}
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
.filter { unit.civInfo.isAtWarWith(it) }
.flatMap { it.cities }.asSequence()
.filter { it.location in unit.civInfo.exploredTiles }
if(unit.type.isRanged()) // ranged units don't harm capturable cities, waste of a turn
enemyCities = enemyCities.filterNot { it.health==1 }
if (unit.type.isRanged()) // ranged units don't harm capturable cities, waste of a turn
enemyCities = enemyCities.filterNot { it.health == 1 }
val closestReachableEnemyCity = enemyCities
.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()!!
}
.firstOrNull { unit.movement.canReach(it) }
@ -330,44 +248,38 @@ class UnitAutomation{
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
val tilesInBombardRange = closestReachableEnemyCity.getTilesInDistance(2)
val reachableTilesNotInBombardRange = unitDistanceToTiles.keys.filter { it !in tilesInBombardRange }
val canMoveIntoBombardRange = tilesInBombardRange.any { unitDistanceToTiles.containsKey(it)}
val suitableGatheringGroundTiles = closestReachableEnemyCity.getTilesAtDistance(4)
.union(closestReachableEnemyCity.getTilesAtDistance(3))
.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 -
// this is against tha AI's brilliant plan of having everyone embarked and attacking via sea when unnecessary.
val tileToHeadTo = if(closestReachableLandingGroundTile!=null) closestReachableLandingGroundTile
else closestReachableEnemyCity
val tileToHeadTo = suitableGatheringGroundTiles
.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)
else{
if(unit.getRange()>2){ // should never be in a bombardable position
else {
if (unit.getRange() > 2) { // should never be in a bombardable position
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
if(tilesCanAttackFromButNotInBombardRange.any())
if (tilesCanAttackFromButNotInBombardRange.any())
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)
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!! }
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)
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,
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!
}
}
@ -377,42 +289,6 @@ class UnitAutomation{
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 {
if (!city.attackedThisTurn) {
val target = chooseBombardTarget(city)
@ -424,48 +300,26 @@ class UnitAutomation{
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
}
private fun chooseBombardTarget(city: CityInfo) : TileInfo? {
private fun chooseBombardTarget(city: CityInfo): TileInfo? {
var targets = getBombardTargets(city)
if (targets.isEmpty()) return null
val siegeUnits = targets
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType()==UnitType.Siege }
if(siegeUnits.any()) targets = siegeUnits
else{
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType() == UnitType.Siege }
if (siegeUnits.any()) targets = siegeUnits
else {
val rangedUnits = targets
.filter { Battle.getMapCombatantOfTile(it)!!.getUnitType().isRanged() }
if(rangedUnits.any()) targets=rangedUnits
if (rangedUnits.any()) targets = rangedUnits
}
return targets.minBy { Battle.getMapCombatantOfTile(it)!!.getHealth() }
}
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 centerTile = it.getCenterTile()
centerTile.militaryUnit==null
&& unit.movement.canMoveTo(centerTile)
centerTile.militaryUnit == null
&& unit.movement.canMoveTo(centerTile)
}
fun isCityThatNeedsDefendingInWartime(city: CityInfo): Boolean {
@ -473,69 +327,70 @@ class UnitAutomation{
for (enemyCivCity in unit.civInfo.diplomacy.values
.filter { it.diplomaticStatus == DiplomaticStatus.War }
.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
}
val citiesToTry:Sequence<CityInfo>
val citiesToTry: Sequence<CityInfo>
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.
citiesToTry = citiesWithoutGarrison.asSequence()
} else {
if (unit.getTile().isCityCenter() &&
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
citiesToTry = citiesWithoutGarrison.asSequence()
.filter { isCityThatNeedsDefendingInWartime(it) }
}
val closestReachableCityNeedsDefending =citiesToTry
.sortedBy{ it.getCenterTile().arialDistanceTo(unit.currentTile) }
val closestReachableCityNeedsDefending = citiesToTry
.sortedBy { it.getCenterTile().arialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it.getCenterTile()) }
if(closestReachableCityNeedsDefending==null) return false
if (closestReachableCityNeedsDefending == null) return false
unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile())
return true
}
fun tryGoToRuin(unit:MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if(!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
private fun tryGoToRuin(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if (!unit.civInfo.isMajorCiv()) return false // barbs don't have anything to do in ruins
val tileWithRuin = unitDistanceToTiles.keys
.firstOrNull{ it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) }
if(tileWithRuin==null) return false
.firstOrNull { it.improvement == Constants.ancientRuins && unit.movement.canMoveTo(it) }
if (tileWithRuin == null) return false
unit.movement.moveToTile(tileWithRuin)
return true
}
internal fun tryExplore(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn): Boolean {
if(tryGoToRuin(unit,unitDistanceToTiles))
{
if(unit.currentMovement==0f) return true
if (tryGoToRuin(unit, unitDistanceToTiles)) {
if (unit.currentMovement == 0f) return true
}
for(tile in unit.currentTile.getTilesInDistance(5))
if(unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
&& unit.movement.canReach(tile)){
for (tile in unit.currentTile.getTilesInDistance(5))
if (unit.movement.canMoveTo(tile) && tile.position !in unit.civInfo.exploredTiles
&& unit.movement.canReach(tile)) {
unit.movement.headTowards(tile)
return true
}
return false
}
fun automatedExplore(unit:MapUnit){
fun automatedExplore(unit: MapUnit) {
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) {
tryHealUnit(unit,unitDistanceToTiles)
tryHealUnit(unit, unitDistanceToTiles)
return
}
for(i in 1..10){
for (i in 1..10) {
val unexploredTilesAtDistance = unit.getTile().getTilesAtDistance(i)
.filter { unit.movement.canMoveTo(it) && it.position !in unit.civInfo.exploredTiles
&& unit.movement.canReach(it) }
if(unexploredTilesAtDistance.isNotEmpty()){
.filter {
unit.movement.canMoveTo(it) && it.position !in unit.civInfo.exploredTiles
&& unit.movement.canReach(it)
}
if (unexploredTilesAtDistance.isNotEmpty()) {
unit.movement.headTowards(unexploredTilesAtDistance.random())
return
}
@ -543,15 +398,12 @@ class UnitAutomation{
unit.civInfo.addNotification("[${unit.name}] finished exploring.", unit.currentTile.position, Color.GRAY)
}
fun wander(unit: MapUnit, unitDistanceToTiles: PathsToTilesWithinTurn) {
val reachableTiles= unitDistanceToTiles
val reachableTiles = unitDistanceToTiles
.filter { unit.movement.canMoveTo(it.key) && unit.movement.canReach(it.key) }
val reachableTilesMaxWalkingDistance = reachableTiles.filter { it.value.totalDistance == unit.currentMovement }
if (reachableTilesMaxWalkingDistance.any()) unit.movement.moveToTile(reachableTilesMaxWalkingDistance.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.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.automation.UnitAutomation
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.AlertType
import com.unciv.logic.civilization.PopupAlert
import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.models.AttackableTile
import com.unciv.models.ruleset.unit.UnitType
import java.util.*
import kotlin.math.max
@ -19,7 +19,7 @@ import kotlin.math.max
*/
object Battle {
fun moveAndAttack(attacker: ICombatant, attackableTile: UnitAutomation.AttackableTile){
fun moveAndAttack(attacker: ICombatant, attackableTile: AttackableTile){
if (attacker is MapUnitCombatant) {
attacker.unit.movement.moveToTile(attackableTile.tileToAttackFrom)
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
}
fun fortify(){ action = "Fortify 0"}
fun fortify() {
action = "Fortify 0"
}
fun fortifyIfCan() {
if (canFortify()) {
fortify()
}
}
fun adjacentHealingBonus():Int{
var healingBonus = 0

View File

@ -7,7 +7,7 @@ import com.unciv.logic.civilization.CivilizationInfo
class UnitMovementAlgorithms(val unit:MapUnit) {
// 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())
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.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.automation.BattleHelper
import com.unciv.logic.automation.UnitAutomation
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
@ -244,7 +245,7 @@ class WorldMapHolder(internal val worldScreen: WorldScreen, internal val tileMap
val unitType = unit.type
val attackableTiles: List<TileInfo> = if (unitType.isCivilian()) listOf()
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)) }
}

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.TextButton
import com.unciv.UncivGame
import com.unciv.logic.automation.BattleHelper
import com.unciv.logic.automation.UnitAutomation
import com.unciv.logic.battle.*
import com.unciv.models.AttackableTile
import com.unciv.models.translations.tr
import com.unciv.models.ruleset.unit.UnitType
import com.unciv.ui.utils.*
@ -20,7 +22,9 @@ import kotlin.math.max
class BattleTable(val worldScreen: WorldScreen): Table() {
init{
private val battleHelper = BattleHelper()
init {
isVisible = false
skin = CameraStageBaseScreen.skin
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 }
var attackableEnemy : UnitAutomation.AttackableTile? = null
var attackableEnemy : AttackableTile? = null
if (attacker.canAttack()) {
if (attacker is MapUnitCombatant) {
attackableEnemy = UnitAutomation()
attackableEnemy = battleHelper
.getAttackableEnemies(attacker.unit, attacker.unit.movement.getDistanceToTiles())
.firstOrNull{ it.tileToAttack == defender.getTile()}
}
@ -182,7 +186,7 @@ class BattleTable(val worldScreen: WorldScreen): Table() {
{
val canBombard = UnitAutomation().getBombardTargets(attacker.city).contains(defender.getTile())
if (canBombard) {
attackableEnemy = UnitAutomation.AttackableTile(attacker.getTile(), defender.getTile())
attackableEnemy = AttackableTile(attacker.getTile(), defender.getTile())
}
}
}