From 725edc2a31faf7cc2004814e87be4b8b6ff1b81b Mon Sep 17 00:00:00 2001 From: Vladimir Tanakov Date: Sun, 12 Jan 2020 21:48:34 +0300 Subject: [PATCH] Better barbarian automation (#1560) --- .../logic/automation/BarbarianAutomation.kt | 200 ++++++++++ .../unciv/logic/automation/BattleHelper.kt | 146 +++++++ .../logic/automation/NextTurnAutomation.kt | 41 +- .../automation/SpecificUnitAutomation.kt | 16 +- .../unciv/logic/automation/UnitAutomation.kt | 374 ++++++------------ core/src/com/unciv/logic/battle/Battle.kt | 4 +- core/src/com/unciv/logic/map/MapUnit.kt | 10 +- .../unciv/logic/map/UnitMovementAlgorithms.kt | 2 +- core/src/com/unciv/models/AttackableTile.kt | 5 + .../unciv/ui/worldscreen/WorldMapHolder.kt | 3 +- .../ui/worldscreen/bottombar/BattleTable.kt | 12 +- 11 files changed, 518 insertions(+), 295 deletions(-) create mode 100644 core/src/com/unciv/logic/automation/BarbarianAutomation.kt create mode 100644 core/src/com/unciv/logic/automation/BattleHelper.kt create mode 100644 core/src/com/unciv/models/AttackableTile.kt diff --git a/core/src/com/unciv/logic/automation/BarbarianAutomation.kt b/core/src/com/unciv/logic/automation/BarbarianAutomation.kt new file mode 100644 index 0000000000..0605f7c6a7 --- /dev/null +++ b/core/src/com/unciv/logic/automation/BarbarianAutomation.kt @@ -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 + ): TileInfo { + val possibleTiles = unitDistanceToTiles.keys.filter { unit.movement.canMoveTo(it) } + val enemies = nearEnemyTiles.mapNotNull { it.tileToAttack.militaryUnit } + var furthestTile: Pair = 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): 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 + ): 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 + } +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/automation/BattleHelper.kt b/core/src/com/unciv/logic/automation/BattleHelper.kt new file mode 100644 index 0000000000..c7637bef50 --- /dev/null +++ b/core/src/com/unciv/logic/automation/BattleHelper.kt @@ -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? = null + ): ArrayList { + val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles) + .filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) } + + val rangeOfAttack = unit.getRange() + + val attackableTiles = ArrayList() + // 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? { + 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 + } +} \ No newline at end of file diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt index 8bffb97f40..2a3b07a76a 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt @@ -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. } diff --git a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt index a2c944966d..2ba6a238ba 100644 --- a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt @@ -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 } diff --git a/core/src/com/unciv/logic/automation/UnitAutomation.kt b/core/src/com/unciv/logic/automation/UnitAutomation.kt index 37653a4ee7..e9d228e157 100644 --- a/core/src/com/unciv/logic/automation/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/UnitAutomation.kt @@ -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? = null - ): ArrayList { - val tilesWithEnemies = (tilesToCheck ?: unit.civInfo.viewableTiles) - .filter { containsAttackableEnemy(it, MapUnitCombatant(unit)) } - - val rangeOfAttack = unit.getRange() - - val attackableTiles = ArrayList() - // 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 { - 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? { - 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 + val citiesToTry: Sequence 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) - } - } \ No newline at end of file diff --git a/core/src/com/unciv/logic/battle/Battle.kt b/core/src/com/unciv/logic/battle/Battle.kt index ca7889d462..6f5d779253 100644 --- a/core/src/com/unciv/logic/battle/Battle.kt +++ b/core/src/com/unciv/logic/battle/Battle.kt @@ -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) { diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index 2651b7301d..97d57c4601 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -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 diff --git a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt index b5ae7bfe4a..1d86918d64 100644 --- a/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt +++ b/core/src/com/unciv/logic/map/UnitMovementAlgorithms.kt @@ -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 diff --git a/core/src/com/unciv/models/AttackableTile.kt b/core/src/com/unciv/models/AttackableTile.kt new file mode 100644 index 0000000000..0fb612bb92 --- /dev/null +++ b/core/src/com/unciv/models/AttackableTile.kt @@ -0,0 +1,5 @@ +package com.unciv.models + +import com.unciv.logic.map.TileInfo + +class AttackableTile(val tileToAttackFrom: TileInfo, val tileToAttack: TileInfo) \ No newline at end of file diff --git a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt index e2fd31b5d9..ee5e77a83e 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldMapHolder.kt @@ -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 = 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)) } } diff --git a/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt b/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt index 6c9dd4c27c..3f2855f7d5 100644 --- a/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt +++ b/core/src/com/unciv/ui/worldscreen/bottombar/BattleTable.kt @@ -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()) } } }