ThreatManager improvement (#11030)

* Reworked ThreatManager to be optimised for getTilesWithEnemyUnitsInDistance

* Refactored and added an exception

* doesTileHaveMilitaryEnemy now searches all military units on the tile and not just the unit in the military slot

* Fixed some errors

* Refactored getTilesWithEnemyUnitsInDistance to use a mutableIterator

* Added some more comments
This commit is contained in:
Oskar Niesen 2024-02-05 15:53:34 -06:00 committed by GitHub
parent cfc26e03fd
commit 10f3781e6f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -4,20 +4,20 @@ import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile
/**
* Handles optimised operations related to finding threats or allies in an area.
*/
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. */
/** The farthest radius in which we have checked tiles for enemies.
* A value of 2 means all enemies at a radius of 2 are in tilesWithEnemies. */
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
/** Stores the location of the enemy tiles that we saw with the distance at which we saw them.
* Tiles are sorted by distance in increasing order.
* This allows us to quickly check if they are still alive and if we should search farther.
* It is not guaranteed that each tile in this list has an enemy (since they may have died).*/
var tilesWithEnemies: MutableList<Pair<Tile,Int>>
)
private val distanceToClosestEnemyTiles = HashMap<Tile, ClosestEnemyTileData>()
@ -34,69 +34,96 @@ class ThreatManager(val civInfo: Civilization) {
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
val tilesWithEnemies = tileData.tilesWithEnemies
// Check the tiles where we have previously found an enemy, if so it must be the closest
while (tilesWithEnemies.isNotEmpty()) {
val enemyTile = tilesWithEnemies.first()
if (doesTileHaveMilitaryEnemy(enemyTile.first)) {
return if (takeLargerValues) enemyTile.second
else enemyTile.second.coerceAtMost(maxDist)
} else {
// This tile is no longer valid
tilesWithEnemies.removeFirst()
}
}
if (tileData.distanceSearched > maxDist) {
// We have already searched past the range we want to search and haven't found any enemies
return if (takeLargerValues) notFoundDistance else maxDist
}
// Only search the tiles that we haven't searched yet
minDistanceToSearch = (tileData.distanceSearched + 1).coerceAtLeast(1)
}
if (tileData != null && tileData.tilesWithEnemies.isNotEmpty()) throw IllegalStateException("There must be no elements in tile.data.tilesWithEnemies at this point")
val tilesWithEnemyAtDistance: MutableList<Pair<Tile,Int>> = mutableListOf()
// 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
tilesWithEnemyAtDistance.add(Pair(searchTile, i))
}
}
if (tilesWithEnemyAtDistance.isNotEmpty()) {
distanceToClosestEnemyTiles[tile] = ClosestEnemyTileData(i, tilesWithEnemyAtDistance)
return i
}
}
distanceToClosestEnemyTiles[tile] = ClosestEnemyTileData(maxDist, null, null)
distanceToClosestEnemyTiles[tile] = ClosestEnemyTileData(maxDist, mutableListOf())
return notFoundDistance
}
/**
* Returns all tiles with enemy units on them in distance.
* Every tile is guaranteed to have an enemy.
* 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>()
// The list of tiles that we will return
val tilesWithEnemies: MutableList<Tile> = mutableListOf()
// The list of tiles with distance that will be stored in distanceToClosestEnemyTiles
val tileDataTilesWithEnemies: MutableList<Pair<Tile,Int>> = if (tileData?.tilesWithEnemies != null) tileData.tilesWithEnemies else mutableListOf()
if (tileData != null && tileData.distanceSearched >= maxDist) {
// Add all tiles that we have previously found
val tilesWithEnemiesIterator = tileDataTilesWithEnemies.listIterator()
for (tileWithDistance in tilesWithEnemiesIterator) {
// Check if the next tile is out of our search range, if so lets stop here
if (tileWithDistance.second > maxDist) return tilesWithEnemies
// Check if the threat on the tile is still present
if (doesTileHaveMilitaryEnemy(tileWithDistance.first))
tilesWithEnemies.add(tileWithDistance.first)
else tilesWithEnemiesIterator.remove()
}
}
// We don't need to search for anything more if we have previously searched past maxDist
if (tileData != null && maxDist <= tileData.distanceSearched)
return tilesWithEnemies
// Search all tiles that haven't been searched yet up until madDist
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)
tileDataTilesWithEnemies.add(Pair(searchTile, i))
}
}
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)
if (tileData != null) {
tileData.distanceSearched = maxOf(tileData.distanceSearched, maxDist)
} else {
// Cache our results for later
distanceToClosestEnemyTiles[tile] = ClosestEnemyTileData(maxDist, tileDataTilesWithEnemies)
}
return tilesWithEnemies
}
@ -135,9 +162,9 @@ class ThreatManager(val civInfo: Civilization) {
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))
if (tile.getUnits().any { it.isMilitary()
&& it.civ.isAtWarWith(civInfo)
&& !it.isInvisible(civInfo) })
return true
return false
}