From 10f3781e6f36d97e5b7525e06eea28fa313ef694 Mon Sep 17 00:00:00 2001 From: Oskar Niesen Date: Mon, 5 Feb 2024 15:53:34 -0600 Subject: [PATCH] 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 --- .../civilization/managers/ThreatManager.kt | 117 +++++++++++------- 1 file changed, 72 insertions(+), 45 deletions(-) diff --git a/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt b/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt index e90501a5b9..4c3813e2ed 100644 --- a/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/ThreatManager.kt @@ -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> ) private val distanceToClosestEnemyTiles = HashMap() @@ -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> = 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 { val tileData = distanceToClosestEnemyTiles[tile] - - // Shortcut, we don't need to search for anything + + // The list of tiles that we will return + val tilesWithEnemies: MutableList = mutableListOf() + // The list of tiles with distance that will be stored in distanceToClosestEnemyTiles + val tileDataTilesWithEnemies: MutableList> = 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 ArrayList() - + 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() 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 } @@ -105,7 +132,7 @@ class ThreatManager(val civInfo: Civilization) { */ fun getEnemyMilitaryUnitsInDistance(tile: Tile, maxDist: Int): List = getEnemyUnitsOnTiles(getTilesWithEnemyUnitsInDistance(tile, maxDist)) - + fun getEnemyUnitsOnTiles(tilesWithEnemyUnitsInDistance:List): List = tilesWithEnemyUnitsInDistance.flatMap { enemyTile -> enemyTile.getUnits() .filter { it.isMilitary() && civInfo.isAtWarWith(it.civ) } } @@ -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 }