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:
Oskar Niesen 2023-11-25 12:11:10 -06:00 committed by GitHub
parent dc7f1f703a
commit f1ceaa216a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 346 additions and 90 deletions

View File

@ -342,7 +342,7 @@ object NextTurnAutomation {
private fun getUnitPriority(unit: MapUnit, isAtWar: Boolean): Int {
if (unit.isCivilian() && !unit.isGreatPersonOfType("War")) return 1 // Civilian
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
return distance + (unit.health / 10) + when {
unit.baseUnit.isRanged() -> 10

View File

@ -10,8 +10,8 @@ import com.unciv.logic.map.tile.Tile
object AirUnitAutomation {
fun automateFighter(unit: MapUnit) {
val tilesInRange = unit.currentTile.getTilesInDistance(unit.getRange())
val enemyAirUnitsInRange = tilesInRange
val tilesWithEnemyUnitsInRange = unit.civ.threatManager.getTilesWithEnemyUnitsInDistance(unit.getTile(), unit.getRange())
val enemyAirUnitsInRange = tilesWithEnemyUnitsInRange
.flatMap { it.airUnits.asSequence() }.filter { it.civ.isAtWarWith(unit.civ) }
if (enemyAirUnitsInRange.any()) return // we need to be on standby in case they attack

View File

@ -26,9 +26,7 @@ object CivilianUnitAutomation {
unit.movement.moveToTile(tilesCanMoveTo.minByOrNull { it.value.totalDistance }!!.key)
}
val tilesWhereWeWillBeCaptured = unit.currentTile.getTilesInDistance(5)
.mapNotNull { it.militaryUnit }
.filter { it.civ.isAtWarWith(unit.civ) }
val tilesWhereWeWillBeCaptured = unit.civ.threatManager.getEnemyMilitaryUnitsInDistance(unit.getTile(),5)
.flatMap { it.movement.getReachableTilesInCurrentTurn() }
.filter { it.militaryUnit?.civ != unit.civ }
.toSet()
@ -139,7 +137,7 @@ object CivilianUnitAutomation {
// 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
val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys
.filter { UnitAutomation.containsEnemyMilitaryUnit(unit, it) }
.filter { unit.civ.threatManager.doesTileHaveMilitaryEnemy(it) }
if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary()
&& unit.getTile().militaryUnit == null && !unit.getTile().isCityCenter()) {
@ -168,16 +166,9 @@ object CivilianUnitAutomation {
}
val tileFurthestFromEnemy = reachableTiles.keys
.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!
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
}
}

View File

@ -29,7 +29,7 @@ object UnitAutomation {
&& 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.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.canReach(tile) // expensive, evaluate last
}
@ -262,8 +262,8 @@ object UnitAutomation {
// Precondition: This must be a military unit
if (unit.isCivilian()) return false
// 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()) {
return false
}
@ -272,12 +272,14 @@ object UnitAutomation {
val swapableTiles = unitDistanceToTiles.keys.filter { it.militaryUnit != null && it.militaryUnit!!.owner == unit.owner}.reversed()
for (swapTile in swapableTiles) {
val otherUnit = swapTile.militaryUnit!!
if (otherUnit.health > 80
&& unit.getDistanceToEnemyUnit(6, false) < otherUnit.getDistanceToEnemyUnit(6,false)) {
val ourDistanceToClosestEnemy = unit.civ.threatManager.getDistanceToClosestEnemyUnit(unit.getTile(),6, false)
if (otherUnit.health > 80
&& ourDistanceToClosestEnemy < otherUnit.civ.threatManager.getDistanceToClosestEnemyUnit(otherUnit.getTile(),6,false)) {
if (otherUnit.baseUnit.isRanged()) {
// Don't swap ranged units closer than they have to be
val range = otherUnit.baseUnit.range
if (unit.getDistanceToEnemyUnit(6) < range)
if (ourDistanceToClosestEnemy < range)
continue
}
if (unit.movement.canUnitSwapTo(swapTile)) {
@ -299,7 +301,7 @@ object UnitAutomation {
val currentUnitTile = unit.getTile()
val dangerousTiles = getDangerousTiles(unit)
val dangerousTiles = unit.civ.threatManager.getDangerousTiles(unit)
val viableTilesForHealing = unitDistanceToTiles.keys
.filter { it !in dangerousTiles && unit.movement.canMoveTo(it) }
@ -345,23 +347,6 @@ object UnitAutomation {
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 {
if (unit.isCivilian()) return false
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.action = null
}
internal fun containsEnemyMilitaryUnit(unit: MapUnit, tile: Tile) =
tile.militaryUnit != null
&& tile.militaryUnit!!.civ.isAtWarWith(unit.civ)
}

View File

@ -24,6 +24,7 @@ import com.unciv.logic.civilization.managers.QuestManager
import com.unciv.logic.civilization.managers.ReligionManager
import com.unciv.logic.civilization.managers.RuinsManager
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.VictoryManager
import com.unciv.logic.civilization.transients.CivInfoStatsForNextTurn
@ -88,6 +89,9 @@ class Civilization : IsPartOfGameInfoSerialization {
@Transient
val units = UnitManager(this)
@Transient
var threatManager = ThreatManager(this)
@Transient
var diplomacyFunctions = DiplomacyFunctions(this)

View 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()
}
}

View File

@ -31,7 +31,8 @@ class TurnManager(val civInfo: Civilization) {
fun startTurn(progressBar: NextTurnProgress? = null) {
if (civInfo.isSpectator()) return
civInfo.threatManager.clear()
if (civInfo.isMajorCiv() && civInfo.isAlive()) {
civInfo.statsHistory.recordRankingStats(civInfo)
}

View File

@ -487,40 +487,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
fun isGreatPerson() = baseUnit.isGreatPerson()
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
//region state-changing functions
@ -565,10 +531,6 @@ class MapUnit : IsPartOfGameInfoSerialization {
val currentTile = getTile()
if (isMoving()) {
// We have moved so invalidate the previous calculation
cache.distanceToClosestEnemyUnit = null
cache.distanceToClosestEnemyUnitSearched = null
val destinationTile = getMovementDestination()
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
@ -923,7 +885,7 @@ class MapUnit : IsPartOfGameInfoSerialization {
"Non-City" -> true
else -> {
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
return false
}

View File

@ -71,9 +71,6 @@ class MapUnitCache(private val mapUnit: MapUnit) {
var hasCitadelPlacementUnique = false
var distanceToClosestEnemyUnit: Int? = null
var distanceToClosestEnemyUnitSearched: Int? = null
fun updateUniques() {
allTilesCosts1 = mapUnit.hasUnique(UniqueType.AllTilesCost1Move)

View File

@ -364,8 +364,6 @@ class UnitMovement(val unit: MapUnit) {
fun moveToTile(destination: Tile, considerZoneOfControl: Boolean = true) {
if (destination == unit.getTile() || unit.isDestroyed) return // already here (or dead)!
// 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.action != UnitActionType.Automate.value) unit.action = null

View File

@ -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)})
}
}