mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-09 23:39:40 +07:00
Get distance to nearest enemy rework (#10481)
* Initial refactor * Moved checking if a tile has an enemy to a new method * Rewrote getDistanceToEnemyUnit * changed the position of the logic of checking if the enemy is still there * Changed some of the other methods to use the ThreatManager getClosestEnemy() * Added a new getTilesWithEnemyUnitsInDistance method * Added a new getEnemyMilitaryUnitsInDistance method * Converted a few lines to use ThreatManager * Changed Air units to use threat manager * Fixed tileWithEnemy error * distanceToClosestEnemyTiles now clears at the start of every turn * Added blank lines to end of ThreatManager.kt * Renamed tilesInRange to tilesWithEnemyUnitsInRange * Changed ArrayList return to a MutableList * Removed ClosestEnemyTileData being a data class * Improved commenting * Improved commenting2 * getEnemyMilitaryUnitsInDistance now uses a flatMap and moved getDangerousTiles to threat manager * Created a new helper method getEnemyUnitsOnTiles * Renamed clearThreatData to clear * Added shortcut if maxDist is less than or equal to distanceSearched * Fixed distanceWithNoEnemies in getTilesWithEnemyUnitsInDistance * Fixed notFoundDistance being higher than maxDistance when takeLargerValues is false * Added some ThreatManager tests * Added some more ThreatManager tests * Removed visible map after use * getTilesWithEnemyUnitsInDistance doesn't search distances <= tileData.distanceSearched (previously was <) * Added 3 more tests --------- Co-authored-by: Yair Morgenstern <yairm210@hotmail.com>
This commit is contained in:
@ -342,7 +342,7 @@ object NextTurnAutomation {
|
|||||||
private fun getUnitPriority(unit: MapUnit, isAtWar: Boolean): Int {
|
private fun getUnitPriority(unit: MapUnit, isAtWar: Boolean): Int {
|
||||||
if (unit.isCivilian() && !unit.isGreatPersonOfType("War")) return 1 // Civilian
|
if (unit.isCivilian() && !unit.isGreatPersonOfType("War")) return 1 // Civilian
|
||||||
if (unit.baseUnit.isAirUnit()) return 2
|
if (unit.baseUnit.isAirUnit()) return 2
|
||||||
val distance = if (!isAtWar) 0 else unit.getDistanceToEnemyUnit(6)
|
val distance = if (!isAtWar) 0 else unit.civ.threatManager.getDistanceToClosestEnemyUnit(unit.getTile(),6)
|
||||||
// Lower health units should move earlier to swap with higher health units
|
// Lower health units should move earlier to swap with higher health units
|
||||||
return distance + (unit.health / 10) + when {
|
return distance + (unit.health / 10) + when {
|
||||||
unit.baseUnit.isRanged() -> 10
|
unit.baseUnit.isRanged() -> 10
|
||||||
|
@ -10,8 +10,8 @@ import com.unciv.logic.map.tile.Tile
|
|||||||
object AirUnitAutomation {
|
object AirUnitAutomation {
|
||||||
|
|
||||||
fun automateFighter(unit: MapUnit) {
|
fun automateFighter(unit: MapUnit) {
|
||||||
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
|
val tilesWithEnemyUnitsInRange = unit.civ.threatManager.getTilesWithEnemyUnitsInDistance(unit.getTile(), unit.getRange())
|
||||||
val enemyAirUnitsInRange = tilesInRange
|
val enemyAirUnitsInRange = tilesWithEnemyUnitsInRange
|
||||||
.flatMap { it.airUnits.asSequence() }.filter { it.civ.isAtWarWith(unit.civ) }
|
.flatMap { it.airUnits.asSequence() }.filter { it.civ.isAtWarWith(unit.civ) }
|
||||||
|
|
||||||
if (enemyAirUnitsInRange.any()) return // we need to be on standby in case they attack
|
if (enemyAirUnitsInRange.any()) return // we need to be on standby in case they attack
|
||||||
|
@ -26,9 +26,7 @@ object CivilianUnitAutomation {
|
|||||||
unit.movement.moveToTile(tilesCanMoveTo.minByOrNull { it.value.totalDistance }!!.key)
|
unit.movement.moveToTile(tilesCanMoveTo.minByOrNull { it.value.totalDistance }!!.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tilesWhereWeWillBeCaptured = unit.currentTile.getTilesInDistance(5)
|
val tilesWhereWeWillBeCaptured = unit.civ.threatManager.getEnemyMilitaryUnitsInDistance(unit.getTile(),5)
|
||||||
.mapNotNull { it.militaryUnit }
|
|
||||||
.filter { it.civ.isAtWarWith(unit.civ) }
|
|
||||||
.flatMap { it.movement.getReachableTilesInCurrentTurn() }
|
.flatMap { it.movement.getReachableTilesInCurrentTurn() }
|
||||||
.filter { it.militaryUnit?.civ != unit.civ }
|
.filter { it.militaryUnit?.civ != unit.civ }
|
||||||
.toSet()
|
.toSet()
|
||||||
@ -139,7 +137,7 @@ object CivilianUnitAutomation {
|
|||||||
// This is a little 'Bugblatter Beast of Traal': Run if we can attack an enemy
|
// This is a little 'Bugblatter Beast of Traal': Run if we can attack an enemy
|
||||||
// Cheaper than determining which enemies could attack us next turn
|
// Cheaper than determining which enemies could attack us next turn
|
||||||
val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys
|
val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys
|
||||||
.filter { UnitAutomation.containsEnemyMilitaryUnit(unit, it) }
|
.filter { unit.civ.threatManager.doesTileHaveMilitaryEnemy(it) }
|
||||||
|
|
||||||
if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary()
|
if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary()
|
||||||
&& unit.getTile().militaryUnit == null && !unit.getTile().isCityCenter()) {
|
&& unit.getTile().militaryUnit == null && !unit.getTile().isCityCenter()) {
|
||||||
@ -168,16 +166,9 @@ object CivilianUnitAutomation {
|
|||||||
}
|
}
|
||||||
val tileFurthestFromEnemy = reachableTiles.keys
|
val tileFurthestFromEnemy = reachableTiles.keys
|
||||||
.filter { unit.movement.canMoveTo(it) && unit.getDamageFromTerrain(it) < unit.health }
|
.filter { unit.movement.canMoveTo(it) && unit.getDamageFromTerrain(it) < unit.health }
|
||||||
.maxByOrNull { countDistanceToClosestEnemy(unit, it) }
|
.maxByOrNull { unit.civ.threatManager.getDistanceToClosestEnemyUnit(unit.getTile(), 4, false) }
|
||||||
?: return // can't move anywhere!
|
?: return // can't move anywhere!
|
||||||
unit.movement.moveToTile(tileFurthestFromEnemy)
|
unit.movement.moveToTile(tileFurthestFromEnemy)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun countDistanceToClosestEnemy(unit: MapUnit, tile: Tile): Int {
|
|
||||||
for (i in 1..3)
|
|
||||||
if (tile.getTilesAtDistance(i).any { UnitAutomation.containsEnemyMilitaryUnit(unit, it) })
|
|
||||||
return i
|
|
||||||
return 4
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ object UnitAutomation {
|
|||||||
&& tile.neighbors.any { !unit.civ.hasExplored(it) }
|
&& tile.neighbors.any { !unit.civ.hasExplored(it) }
|
||||||
&& (!unit.civ.isCityState() || tile.neighbors.any { it.getOwner() == unit.civ }) // Don't want city-states exploring far outside their borders
|
&& (!unit.civ.isCityState() || tile.neighbors.any { it.getOwner() == unit.civ }) // Don't want city-states exploring far outside their borders
|
||||||
&& unit.getDamageFromTerrain(tile) <= 0 // Don't take unnecessary damage
|
&& unit.getDamageFromTerrain(tile) <= 0 // Don't take unnecessary damage
|
||||||
&& tile.getTilesInDistance(3) .none { containsEnemyMilitaryUnit(unit, it) } // don't walk in range of enemy units
|
&& unit.civ.threatManager.getDistanceToClosestEnemyUnit(tile, 3) <= 3 // don't walk in range of enemy units
|
||||||
&& unit.movement.canMoveTo(tile) // expensive, evaluate last
|
&& unit.movement.canMoveTo(tile) // expensive, evaluate last
|
||||||
&& unit.movement.canReach(tile) // expensive, evaluate last
|
&& unit.movement.canReach(tile) // expensive, evaluate last
|
||||||
}
|
}
|
||||||
@ -262,8 +262,8 @@ object UnitAutomation {
|
|||||||
// Precondition: This must be a military unit
|
// Precondition: This must be a military unit
|
||||||
if (unit.isCivilian()) return false
|
if (unit.isCivilian()) return false
|
||||||
// Better to do a more healing oriented move then
|
// Better to do a more healing oriented move then
|
||||||
if (unit.getDistanceToEnemyUnit(6, true) > 4) return false
|
if (unit.civ.threatManager.getDistanceToClosestEnemyUnit(unit.getTile(),6, true) > 4) return false
|
||||||
|
|
||||||
if (unit.baseUnit.isAirUnit()) {
|
if (unit.baseUnit.isAirUnit()) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -272,12 +272,14 @@ object UnitAutomation {
|
|||||||
val swapableTiles = unitDistanceToTiles.keys.filter { it.militaryUnit != null && it.militaryUnit!!.owner == unit.owner}.reversed()
|
val swapableTiles = unitDistanceToTiles.keys.filter { it.militaryUnit != null && it.militaryUnit!!.owner == unit.owner}.reversed()
|
||||||
for (swapTile in swapableTiles) {
|
for (swapTile in swapableTiles) {
|
||||||
val otherUnit = swapTile.militaryUnit!!
|
val otherUnit = swapTile.militaryUnit!!
|
||||||
if (otherUnit.health > 80
|
val ourDistanceToClosestEnemy = unit.civ.threatManager.getDistanceToClosestEnemyUnit(unit.getTile(),6, false)
|
||||||
&& unit.getDistanceToEnemyUnit(6, false) < otherUnit.getDistanceToEnemyUnit(6,false)) {
|
if (otherUnit.health > 80
|
||||||
|
&& ourDistanceToClosestEnemy < otherUnit.civ.threatManager.getDistanceToClosestEnemyUnit(otherUnit.getTile(),6,false)) {
|
||||||
|
|
||||||
if (otherUnit.baseUnit.isRanged()) {
|
if (otherUnit.baseUnit.isRanged()) {
|
||||||
// Don't swap ranged units closer than they have to be
|
// Don't swap ranged units closer than they have to be
|
||||||
val range = otherUnit.baseUnit.range
|
val range = otherUnit.baseUnit.range
|
||||||
if (unit.getDistanceToEnemyUnit(6) < range)
|
if (ourDistanceToClosestEnemy < range)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (unit.movement.canUnitSwapTo(swapTile)) {
|
if (unit.movement.canUnitSwapTo(swapTile)) {
|
||||||
@ -299,7 +301,7 @@ object UnitAutomation {
|
|||||||
|
|
||||||
val currentUnitTile = unit.getTile()
|
val currentUnitTile = unit.getTile()
|
||||||
|
|
||||||
val dangerousTiles = getDangerousTiles(unit)
|
val dangerousTiles = unit.civ.threatManager.getDangerousTiles(unit)
|
||||||
|
|
||||||
val viableTilesForHealing = unitDistanceToTiles.keys
|
val viableTilesForHealing = unitDistanceToTiles.keys
|
||||||
.filter { it !in dangerousTiles && unit.movement.canMoveTo(it) }
|
.filter { it !in dangerousTiles && unit.movement.canMoveTo(it) }
|
||||||
@ -345,23 +347,6 @@ object UnitAutomation {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDangerousTiles(unit: MapUnit): HashSet<Tile> {
|
|
||||||
val nearbyRangedEnemyUnits = unit.currentTile.getTilesInDistance(3)
|
|
||||||
.flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } }
|
|
||||||
|
|
||||||
val tilesInRangeOfAttack = nearbyRangedEnemyUnits
|
|
||||||
.flatMap { it.getTile().getTilesInDistance(it.getRange()) }
|
|
||||||
|
|
||||||
val tilesWithinBombardmentRange = unit.currentTile.getTilesInDistance(3)
|
|
||||||
.filter { it.isCityCenter() && it.getCity()!!.civ.isAtWarWith(unit.civ) }
|
|
||||||
.flatMap { it.getTilesInDistance(it.getCity()!!.range) }
|
|
||||||
|
|
||||||
val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(3)
|
|
||||||
.filter { unit.getDamageFromTerrain(it) > 0 }
|
|
||||||
|
|
||||||
return (tilesInRangeOfAttack + tilesWithinBombardmentRange + tilesWithTerrainDamage).toHashSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun tryPillageImprovement(unit: MapUnit): Boolean {
|
fun tryPillageImprovement(unit: MapUnit): Boolean {
|
||||||
if (unit.isCivilian()) return false
|
if (unit.isCivilian()) return false
|
||||||
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
val unitDistanceToTiles = unit.movement.getDistanceToTiles()
|
||||||
@ -603,10 +588,6 @@ object UnitAutomation {
|
|||||||
unit.civ.addNotification("${unit.shortDisplayName()} finished exploring.", unit.currentTile.position, NotificationCategory.Units, unit.name, "OtherIcons/Sleep")
|
unit.civ.addNotification("${unit.shortDisplayName()} finished exploring.", unit.currentTile.position, NotificationCategory.Units, unit.name, "OtherIcons/Sleep")
|
||||||
unit.action = null
|
unit.action = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
internal fun containsEnemyMilitaryUnit(unit: MapUnit, tile: Tile) =
|
|
||||||
tile.militaryUnit != null
|
|
||||||
&& tile.militaryUnit!!.civ.isAtWarWith(unit.civ)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ import com.unciv.logic.civilization.managers.QuestManager
|
|||||||
import com.unciv.logic.civilization.managers.ReligionManager
|
import com.unciv.logic.civilization.managers.ReligionManager
|
||||||
import com.unciv.logic.civilization.managers.RuinsManager
|
import com.unciv.logic.civilization.managers.RuinsManager
|
||||||
import com.unciv.logic.civilization.managers.TechManager
|
import com.unciv.logic.civilization.managers.TechManager
|
||||||
|
import com.unciv.logic.civilization.managers.ThreatManager
|
||||||
import com.unciv.logic.civilization.managers.UnitManager
|
import com.unciv.logic.civilization.managers.UnitManager
|
||||||
import com.unciv.logic.civilization.managers.VictoryManager
|
import com.unciv.logic.civilization.managers.VictoryManager
|
||||||
import com.unciv.logic.civilization.transients.CivInfoStatsForNextTurn
|
import com.unciv.logic.civilization.transients.CivInfoStatsForNextTurn
|
||||||
@ -88,6 +89,9 @@ class Civilization : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
val units = UnitManager(this)
|
val units = UnitManager(this)
|
||||||
|
|
||||||
|
@Transient
|
||||||
|
var threatManager = ThreatManager(this)
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
var diplomacyFunctions = DiplomacyFunctions(this)
|
var diplomacyFunctions = DiplomacyFunctions(this)
|
||||||
|
149
core/src/com/unciv/logic/civilization/managers/ThreatManager.kt
Normal file
149
core/src/com/unciv/logic/civilization/managers/ThreatManager.kt
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
package com.unciv.logic.civilization.managers
|
||||||
|
|
||||||
|
import com.unciv.logic.civilization.Civilization
|
||||||
|
import com.unciv.logic.map.mapunit.MapUnit
|
||||||
|
import com.unciv.logic.map.tile.Tile
|
||||||
|
|
||||||
|
class ThreatManager(val civInfo: Civilization) {
|
||||||
|
|
||||||
|
class ClosestEnemyTileData(
|
||||||
|
/** The farthest radius in which we have checked all the tiles for enemies.
|
||||||
|
* A value of 2 means there are no enemies in a radius of 2. */
|
||||||
|
var distanceSearched: Int,
|
||||||
|
/** It is guaranteed that there is no enemy within a radius of D-1.
|
||||||
|
* The enemy that we saw might have been killed.
|
||||||
|
* so we have to check the tileWithEnemy to see if we need to search again. */
|
||||||
|
var distanceToClosestEnemy: Int? = null,
|
||||||
|
/** Stores the location of the enemy that we saw.
|
||||||
|
* This allows us to quickly check if they are still alive.
|
||||||
|
* and if we should search farther. */
|
||||||
|
var tileWithEnemy: Tile? = null
|
||||||
|
)
|
||||||
|
|
||||||
|
private val distanceToClosestEnemyTiles = HashMap<Tile, ClosestEnemyTileData>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the distance to the closest visible enemy unit or city.
|
||||||
|
* The result value is cached and since it is called each turn in NextTurnAutomation.getUnitPriority
|
||||||
|
* each subsequent calls are likely to be free.
|
||||||
|
*/
|
||||||
|
fun getDistanceToClosestEnemyUnit(tile: Tile, maxDist: Int, takeLargerValues: Boolean = true): Int {
|
||||||
|
val tileData = distanceToClosestEnemyTiles[tile]
|
||||||
|
// Needs to be a high value, but not the max value so we can still add to it. Example: nextTurnAutomation sorting
|
||||||
|
val notFoundDistance = if (takeLargerValues) 500000 else maxDist
|
||||||
|
var minDistanceToSearch = 1
|
||||||
|
// Look if we can return the cache or if we can reduce our search
|
||||||
|
if (tileData != null) {
|
||||||
|
if (tileData.distanceToClosestEnemy == null) {
|
||||||
|
if (tileData.distanceSearched >= maxDist)
|
||||||
|
return notFoundDistance
|
||||||
|
// else: we need to search more we didn't search as far as we are looking for now
|
||||||
|
} else if (doesTileHaveMilitaryEnemy(tileData.tileWithEnemy!!)) {
|
||||||
|
// The enemy is still there
|
||||||
|
return if (tileData.distanceToClosestEnemy!! <= maxDist || takeLargerValues)
|
||||||
|
tileData.distanceToClosestEnemy!!
|
||||||
|
else notFoundDistance
|
||||||
|
}
|
||||||
|
// Only search the tiles that we haven't searched yet
|
||||||
|
minDistanceToSearch = (tileData.distanceSearched + 1).coerceAtLeast(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search for nearby enemies and store the results
|
||||||
|
for (i in minDistanceToSearch..maxDist) {
|
||||||
|
for (searchTile in tile.getTilesAtDistance(i)) {
|
||||||
|
if (doesTileHaveMilitaryEnemy(searchTile)) {
|
||||||
|
// We have only completely searched a radius of i - 1
|
||||||
|
distanceToClosestEnemyTiles[tile] = ClosestEnemyTileData(i - 1, i, searchTile)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
distanceToClosestEnemyTiles[tile] = ClosestEnemyTileData(maxDist, null, null)
|
||||||
|
return notFoundDistance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all tiles with enemy units on them in distance.
|
||||||
|
* May be quicker than a manual search because of caching.
|
||||||
|
* Also ends up calculating and caching [getDistanceToClosestEnemyUnit].
|
||||||
|
*/
|
||||||
|
fun getTilesWithEnemyUnitsInDistance(tile: Tile, maxDist: Int): MutableList<Tile> {
|
||||||
|
val tileData = distanceToClosestEnemyTiles[tile]
|
||||||
|
|
||||||
|
// Shortcut, we don't need to search for anything
|
||||||
|
if (tileData != null && maxDist <= tileData.distanceSearched)
|
||||||
|
return ArrayList<Tile>()
|
||||||
|
|
||||||
|
val minDistanceToSearch = (tileData?.distanceSearched?.coerceAtLeast(0) ?: 0) + 1
|
||||||
|
var distanceWithNoEnemies = tileData?.distanceSearched ?: 0
|
||||||
|
var closestEnemyDistance = tileData?.distanceToClosestEnemy
|
||||||
|
var tileWithEnemy = tileData?.tileWithEnemy
|
||||||
|
val tilesWithEnemies = ArrayList<Tile>()
|
||||||
|
|
||||||
|
for (i in minDistanceToSearch..maxDist) {
|
||||||
|
for (searchTile in tile.getTilesAtDistance(i)) {
|
||||||
|
if (doesTileHaveMilitaryEnemy(searchTile)) {
|
||||||
|
tilesWithEnemies.add(searchTile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tilesWithEnemies.isEmpty() && distanceWithNoEnemies < i) {
|
||||||
|
distanceWithNoEnemies = i
|
||||||
|
}
|
||||||
|
if (tilesWithEnemies.isNotEmpty() && (closestEnemyDistance == null || closestEnemyDistance < i)) {
|
||||||
|
closestEnemyDistance = i
|
||||||
|
tileWithEnemy = tilesWithEnemies.first()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Cache our results for later
|
||||||
|
// tilesWithEnemies must return the enemy at a distance of closestEnemyDistance
|
||||||
|
distanceToClosestEnemyTiles[tile] = ClosestEnemyTileData(distanceWithNoEnemies, closestEnemyDistance, tileWithEnemy)
|
||||||
|
return tilesWithEnemies
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all enemy military units within maxDistance of the tile.
|
||||||
|
*/
|
||||||
|
fun getEnemyMilitaryUnitsInDistance(tile: Tile, maxDist: Int): List<MapUnit> =
|
||||||
|
getEnemyUnitsOnTiles(getTilesWithEnemyUnitsInDistance(tile, maxDist))
|
||||||
|
|
||||||
|
fun getEnemyUnitsOnTiles(tilesWithEnemyUnitsInDistance:List<Tile>): List<MapUnit> =
|
||||||
|
tilesWithEnemyUnitsInDistance.flatMap { enemyTile -> enemyTile.getUnits()
|
||||||
|
.filter { it.isMilitary() && civInfo.isAtWarWith(it.civ) } }
|
||||||
|
|
||||||
|
|
||||||
|
fun getDangerousTiles(unit: MapUnit, distance: Int = 3): HashSet<Tile> {
|
||||||
|
val tilesWithEnemyUnits = getTilesWithEnemyUnitsInDistance(unit.getTile(), distance)
|
||||||
|
val nearbyRangedEnemyUnits = getEnemyUnitsOnTiles(tilesWithEnemyUnits)
|
||||||
|
|
||||||
|
val tilesInRangeOfAttack = nearbyRangedEnemyUnits
|
||||||
|
.flatMap { it.getTile().getTilesInDistance(it.getRange()) }
|
||||||
|
|
||||||
|
val tilesWithinBombardmentRange = tilesWithEnemyUnits
|
||||||
|
.filter { it.isCityCenter() && it.getCity()!!.civ.isAtWarWith(unit.civ) }
|
||||||
|
.flatMap { it.getTilesInDistance(it.getCity()!!.range) }
|
||||||
|
|
||||||
|
val tilesWithTerrainDamage = unit.currentTile.getTilesInDistance(distance)
|
||||||
|
.filter { unit.getDamageFromTerrain(it) > 0 }
|
||||||
|
|
||||||
|
return (tilesInRangeOfAttack + tilesWithinBombardmentRange + tilesWithTerrainDamage).toHashSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the tile has a visible enemy, otherwise returns false.
|
||||||
|
*/
|
||||||
|
fun doesTileHaveMilitaryEnemy(tile: Tile): Boolean {
|
||||||
|
if (!tile.isExplored(civInfo)) return false
|
||||||
|
if (tile.isCityCenter() && tile.getCity()!!.civ.isAtWarWith(civInfo)) return true
|
||||||
|
if (!tile.isVisible(civInfo)) return false
|
||||||
|
if (tile.militaryUnit != null
|
||||||
|
&& tile.militaryUnit!!.civ.isAtWarWith(civInfo)
|
||||||
|
&& !tile.militaryUnit!!.isInvisible(civInfo))
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
distanceToClosestEnemyTiles.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,8 @@ class TurnManager(val civInfo: Civilization) {
|
|||||||
|
|
||||||
fun startTurn(progressBar: NextTurnProgress? = null) {
|
fun startTurn(progressBar: NextTurnProgress? = null) {
|
||||||
if (civInfo.isSpectator()) return
|
if (civInfo.isSpectator()) return
|
||||||
|
|
||||||
|
civInfo.threatManager.clear()
|
||||||
if (civInfo.isMajorCiv() && civInfo.isAlive()) {
|
if (civInfo.isMajorCiv() && civInfo.isAlive()) {
|
||||||
civInfo.statsHistory.recordRankingStats(civInfo)
|
civInfo.statsHistory.recordRankingStats(civInfo)
|
||||||
}
|
}
|
||||||
|
@ -487,40 +487,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
fun isGreatPerson() = baseUnit.isGreatPerson()
|
fun isGreatPerson() = baseUnit.isGreatPerson()
|
||||||
fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type)
|
fun isGreatPersonOfType(type: String) = baseUnit.isGreatPersonOfType(type)
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the distance to the closest visible enemy unit or city.
|
|
||||||
* The result value is cached
|
|
||||||
* Since it is called each turn each subsequent call is essentially free
|
|
||||||
*/
|
|
||||||
fun getDistanceToEnemyUnit(maxDist: Int, takeLargerValues: Boolean = true): Int {
|
|
||||||
if (cache.distanceToClosestEnemyUnit != null) {
|
|
||||||
return if ((takeLargerValues || cache.distanceToClosestEnemyUnit!! < maxDist))
|
|
||||||
cache.distanceToClosestEnemyUnit!!
|
|
||||||
// In some cases we might rely on every distance farther than maxDist being the same
|
|
||||||
else Int.MAX_VALUE
|
|
||||||
}
|
|
||||||
|
|
||||||
fun tileHasEnemyCity(tile: Tile): Boolean = tile.isExplored(civ)
|
|
||||||
&& tile.isCityCenter()
|
|
||||||
&& tile.getCity()!!.civ.isAtWarWith(civ)
|
|
||||||
|
|
||||||
fun tileHasEnemyMilitaryUnit(tile: Tile): Boolean = tile.isVisible(civ)
|
|
||||||
&& tile.militaryUnit != null
|
|
||||||
&& tile.militaryUnit!!.civ.isAtWarWith(civ)
|
|
||||||
&& !tile.militaryUnit!!.isInvisible(civ)
|
|
||||||
|
|
||||||
// Needs to be a high value, but not the max value so we can still add to it
|
|
||||||
cache.distanceToClosestEnemyUnit = 500000
|
|
||||||
for (i in 1..maxDist) {
|
|
||||||
if (currentTile.getTilesAtDistance(i).any {
|
|
||||||
tileHasEnemyCity(it) || tileHasEnemyMilitaryUnit(it) }) {
|
|
||||||
cache.distanceToClosestEnemyUnit = i
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return cache.distanceToClosestEnemyUnit!!
|
|
||||||
}
|
|
||||||
//endregion
|
//endregion
|
||||||
|
|
||||||
//region state-changing functions
|
//region state-changing functions
|
||||||
@ -565,10 +531,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
|
|
||||||
val currentTile = getTile()
|
val currentTile = getTile()
|
||||||
if (isMoving()) {
|
if (isMoving()) {
|
||||||
// We have moved so invalidate the previous calculation
|
|
||||||
cache.distanceToClosestEnemyUnit = null
|
|
||||||
cache.distanceToClosestEnemyUnitSearched = null
|
|
||||||
|
|
||||||
val destinationTile = getMovementDestination()
|
val destinationTile = getMovementDestination()
|
||||||
if (!movement.canReach(destinationTile)) { // That tile that we were moving towards is now unreachable -
|
if (!movement.canReach(destinationTile)) { // That tile that we were moving towards is now unreachable -
|
||||||
// for instance we headed towards an unknown tile and it's apparently unreachable
|
// for instance we headed towards an unknown tile and it's apparently unreachable
|
||||||
@ -923,7 +885,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
|
|||||||
"Non-City" -> true
|
"Non-City" -> true
|
||||||
else -> {
|
else -> {
|
||||||
if (baseUnit.matchesFilter(filter)) return true
|
if (baseUnit.matchesFilter(filter)) return true
|
||||||
if (civ.matchesFilter(filter)) return true
|
if (civ.nation.matchesFilter(filter)) return true
|
||||||
if (tempUniquesMap.containsKey(filter)) return true
|
if (tempUniquesMap.containsKey(filter)) return true
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,6 @@ class MapUnitCache(private val mapUnit: MapUnit) {
|
|||||||
|
|
||||||
var hasCitadelPlacementUnique = false
|
var hasCitadelPlacementUnique = false
|
||||||
|
|
||||||
var distanceToClosestEnemyUnit: Int? = null
|
|
||||||
var distanceToClosestEnemyUnitSearched: Int? = null
|
|
||||||
|
|
||||||
fun updateUniques() {
|
fun updateUniques() {
|
||||||
|
|
||||||
allTilesCosts1 = mapUnit.hasUnique(UniqueType.AllTilesCost1Move)
|
allTilesCosts1 = mapUnit.hasUnique(UniqueType.AllTilesCost1Move)
|
||||||
|
@ -364,8 +364,6 @@ class UnitMovement(val unit: MapUnit) {
|
|||||||
fun moveToTile(destination: Tile, considerZoneOfControl: Boolean = true) {
|
fun moveToTile(destination: Tile, considerZoneOfControl: Boolean = true) {
|
||||||
if (destination == unit.getTile() || unit.isDestroyed) return // already here (or dead)!
|
if (destination == unit.getTile() || unit.isDestroyed) return // already here (or dead)!
|
||||||
// Reset closestEnemy chache
|
// Reset closestEnemy chache
|
||||||
unit.cache.distanceToClosestEnemyUnit = null
|
|
||||||
unit.cache.distanceToClosestEnemyUnitSearched = null
|
|
||||||
|
|
||||||
if (unit.baseUnit.movesLikeAirUnits()) { // air units move differently from all other units
|
if (unit.baseUnit.movesLikeAirUnits()) { // air units move differently from all other units
|
||||||
if (unit.action != UnitActionType.Automate.value) unit.action = null
|
if (unit.action != UnitActionType.Automate.value) unit.action = null
|
||||||
|
@ -0,0 +1,173 @@
|
|||||||
|
package com.unciv.logic.civilization.managers
|
||||||
|
|
||||||
|
import com.badlogic.gdx.math.Vector2
|
||||||
|
import com.unciv.testing.GdxTestRunner
|
||||||
|
import com.unciv.testing.TestGame
|
||||||
|
import com.unciv.utils.DebugUtils
|
||||||
|
import junit.framework.TestCase.assertEquals
|
||||||
|
import org.junit.After
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(GdxTestRunner::class)
|
||||||
|
class ThreatManangerTests {
|
||||||
|
|
||||||
|
val testGame = TestGame()
|
||||||
|
val civ = testGame.addCiv()
|
||||||
|
val neutralCiv = testGame.addCiv()
|
||||||
|
val enemyCiv = testGame.addCiv()
|
||||||
|
val threatManager = civ.threatManager
|
||||||
|
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
DebugUtils.VISIBLE_MAP = true // Needed to be able to see the enemy units
|
||||||
|
testGame.makeHexagonalMap(10)
|
||||||
|
civ.diplomacyFunctions.makeCivilizationsMeet(enemyCiv)
|
||||||
|
civ.diplomacyFunctions.makeCivilizationsMeet(neutralCiv)
|
||||||
|
civ.getDiplomacyManager(enemyCiv).declareWar()
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
fun wrapUp() {
|
||||||
|
DebugUtils.VISIBLE_MAP = false
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Distance to closest enemy with no enemies`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
assertEquals(5, threatManager.getDistanceToClosestEnemyUnit(centerTile,5, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find tiles with enemies with no enemies`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
assertEquals(0, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 5).count())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find enemies on tiles with no enemies`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
assertEquals(0, threatManager.getEnemyUnitsOnTiles(threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 5)).count())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find distance to enemy`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", neutralCiv, testGame.getTile(Vector2(1f, 1f)))
|
||||||
|
assertEquals(3, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find distance to closer enemy`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(4f, 0f)))
|
||||||
|
assertEquals(3, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find distance to farther enemy`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
assertEquals(2, threatManager.getDistanceToClosestEnemyUnit(centerTile, 2, false))
|
||||||
|
// Cache results should say there is not a unit within a distance of 2
|
||||||
|
// Therefore the warrior at distance 2 should not be checked
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(2f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(4f, 0f)))
|
||||||
|
assertEquals(4, threatManager.getDistanceToClosestEnemyUnit(centerTile, 4, false))
|
||||||
|
assertEquals(4, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find distance to enemy wrong cache`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
assertEquals(3, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5))
|
||||||
|
testGame.getTile(Vector2(3f, 0f)).militaryUnit!!.removeFromTile()
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(4f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(4f, 1f)))
|
||||||
|
assertEquals(4, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5))
|
||||||
|
testGame.getTile(Vector2(4f, 0f)).militaryUnit!!.removeFromTile()
|
||||||
|
assertEquals(4, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5, false))
|
||||||
|
testGame.getTile(Vector2(4f, 1f)).militaryUnit!!.removeFromTile()
|
||||||
|
assertEquals(5, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5, false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find distance to enemy cache`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
assertEquals(3, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5))
|
||||||
|
// An enemy unit should never be spawned closer than we previously searched
|
||||||
|
// Therefore our cache results should return 3 instead of the closer unit at a distance of 2
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(2f, 0f)))
|
||||||
|
assertEquals(3, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find tiles with enemy units`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(2f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(4f, 0f)))
|
||||||
|
assertEquals(3, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 5).count())
|
||||||
|
assertEquals(2, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 3).count())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find tiles with enemy units cache`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
assertEquals(5, threatManager.getDistanceToClosestEnemyUnit(centerTile, 5, false))
|
||||||
|
// We have stored in the cach that there is no enemy unit within a distance of 5
|
||||||
|
// Therefore adding these units is illegal and should not be returned
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(2f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(4f, 0f)))
|
||||||
|
assertEquals(0, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 5).count())
|
||||||
|
assertEquals(0, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 3).count())
|
||||||
|
|
||||||
|
// Now it might be another turn, so it is allowed
|
||||||
|
threatManager.clear()
|
||||||
|
assertEquals(3, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 5).count())
|
||||||
|
assertEquals(2, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 3).count())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find distance to enemy after find tiles with enemy units`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
assertEquals(1, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 5).count())
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(2f, 0f)))
|
||||||
|
assertEquals(1, threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 5).count())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Find enemy units on tiles`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
testGame.addCity(enemyCiv,testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
testGame.addUnit("Bomber", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(2f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(4f, 0f)))
|
||||||
|
testGame.addUnit("Warrior", neutralCiv, testGame.getTile(Vector2(-3f, -3f)))
|
||||||
|
assertEquals(4, threatManager.getEnemyUnitsOnTiles(threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 5)).count())
|
||||||
|
assertEquals(3, threatManager.getEnemyUnitsOnTiles(threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 3)).count())
|
||||||
|
assertEquals(0, threatManager.getEnemyUnitsOnTiles(threatManager.getTilesWithEnemyUnitsInDistance(centerTile, 1)).count())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Dangerous tiles`() {
|
||||||
|
val centerTile = testGame.getTile(Vector2(0f, 0f))
|
||||||
|
testGame.addUnit("Warrior", civ, centerTile)
|
||||||
|
testGame.addUnit("Warrior", enemyCiv, testGame.getTile(Vector2(3f, 0f)))
|
||||||
|
testGame.addUnit("Archer", enemyCiv, testGame.getTile(Vector2(-3f, 0f)))
|
||||||
|
val dangerousTiles = threatManager.getDangerousTiles(centerTile.militaryUnit!!,3)
|
||||||
|
assertEquals(null, testGame.getTile(Vector2(3f, 0f)).getTilesInDistance(1).firstOrNull {tile -> !dangerousTiles.contains(tile)})
|
||||||
|
assertEquals(null, testGame.getTile(Vector2(-3f, 0f)).getTilesInDistance(2).firstOrNull {tile -> !dangerousTiles.contains(tile)})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user