BFS cleanup (#4859)

* BFS cleanup
- Kotlin ArrayDeque is no longer experimental
- Client code sidestepped class API even where API existed -> closed it down
- nextStep when already ended now no-op instead of crash, suits usecases
- predicate test very likely to be more expensive than tilesReached lookup
- getPathTo only used asSequence() - why not a Sequence in the first place
- Compleat documentation. This is very low level, so better help future coders to understand.
- Lint: One annoying hardcoded conditional breakpoint, one unhelpful exception

* BFS cleanup - patch1
This commit is contained in:
SomeTroglodyte 2021-08-15 18:44:31 +02:00 committed by GitHub
parent 5d249ee673
commit 4daa22ddea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 50 additions and 33 deletions

View File

@ -347,7 +347,7 @@ object GameStarter {
while (landTiles.any()) { while (landTiles.any()) {
val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() } val bfs = BFS(landTiles.random()) { it.isLand && !it.isImpassible() }
bfs.stepToEnd() bfs.stepToEnd()
val tilesInGroup = bfs.tilesReached.keys val tilesInGroup = bfs.getReachedTiles()
landTiles = landTiles.filter { it !in tilesInGroup } landTiles = landTiles.filter { it !in tilesInGroup }
if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on if (tilesInGroup.size > 20) // is this a good number? I dunno, but it's easy enough to change later on
landTilesInBigEnoughGroup.addAll(tilesInGroup) landTilesInBigEnoughGroup.addAll(tilesInGroup)
@ -463,4 +463,4 @@ object GameStarter {
val distanceFromCenter = HexMath.getDistance(vector, Vector2.Zero) val distanceFromCenter = HexMath.getDistance(vector, Vector2.Zero)
return hexagonalRadius - distanceFromCenter >= n return hexagonalRadius - distanceFromCenter >= n
} }
} }

View File

@ -71,7 +71,7 @@ object Automation {
val findWaterConnectedCitiesAndEnemies = val findWaterConnectedCitiesAndEnemies =
BFS(city.getCenterTile()) { it.isWater || it.isCityCenter() } BFS(city.getCenterTile()) { it.isWater || it.isCityCenter() }
findWaterConnectedCitiesAndEnemies.stepToEnd() findWaterConnectedCitiesAndEnemies.stepToEnd()
if (findWaterConnectedCitiesAndEnemies.tilesReached.keys.none { if (findWaterConnectedCitiesAndEnemies.getReachedTiles().none {
(it.isCityCenter() && it.getOwner() != city.civInfo) (it.isCityCenter() && it.getOwner() != city.civInfo)
|| (it.militaryUnit != null && it.militaryUnit!!.civInfo != city.civInfo) || (it.militaryUnit != null && it.militaryUnit!!.civInfo != city.civInfo)
}) // there is absolutely no reason for you to make water units on this body of water. }) // there is absolutely no reason for you to make water units on this body of water.
@ -218,4 +218,4 @@ enum class ThreatLevel{
Medium, Medium,
High, High,
VeryHigh VeryHigh
} }

View File

@ -85,10 +85,10 @@ class WorkerAutomation(val unit: MapUnit) {
// we order cities by their closeness to the worker first, and then check for each one whether there's a viable path // we order cities by their closeness to the worker first, and then check for each one whether there's a viable path
// it can take to an existing connected city. // it can take to an existing connected city.
for (bfs in citiesThatNeedConnectingBfs) { for (bfs in citiesThatNeedConnectingBfs) {
while (bfs.tilesToCheck.isNotEmpty()) { while (!bfs.hasEnded()) {
bfs.nextStep() bfs.nextStep()
for (city in connectedCities) for (city in connectedCities)
if (bfs.tilesToCheck.contains(city)) { // we have a winner! if (bfs.hasReachedTile(city)) { // we have a winner!
val pathToCity = bfs.getPathTo(city).asSequence() val pathToCity = bfs.getPathTo(city).asSequence()
val roadableTiles = pathToCity.filter { it.roadStatus < targetRoad } val roadableTiles = pathToCity.filter { it.roadStatus < targetRoad }
val tileToConstructRoadOn: TileInfo val tileToConstructRoadOn: TileInfo

View File

@ -1,59 +1,80 @@
package com.unciv.logic.map package com.unciv.logic.map
// Kotlin's ArrayDeque is experimental import kotlin.collections.ArrayDeque
import java.util.ArrayDeque
/** /**
* Defines intermediate steps of a breadth-first search, for use in either get shortest path or get onnected tiles. * Defines intermediate steps of a breadth-first search, for use in either get shortest path or get connected tiles.
*/ */
class BFS(val startingPoint: TileInfo, val predicate : (TileInfo) -> Boolean) { class BFS(
var tilesToCheck = ArrayDeque<TileInfo>() val startingPoint: TileInfo,
private val predicate : (TileInfo) -> Boolean
) {
/** remaining tiles to check */
private val tilesToCheck = ArrayDeque<TileInfo>(37) // needs resize at distance 4
/** each tile reached points to its parent tile, where we got to it from */ /** each tile reached points to its parent tile, where we got to it from */
val tilesReached = HashMap<TileInfo, TileInfo>() private val tilesReached = HashMap<TileInfo, TileInfo>()
init { init {
tilesToCheck.add(startingPoint) tilesToCheck.add(startingPoint)
tilesReached[startingPoint] = startingPoint tilesReached[startingPoint] = startingPoint
} }
/** Process fully until there's nowhere left to check */
fun stepToEnd() { fun stepToEnd() {
while (!hasEnded()) while (!hasEnded())
nextStep() nextStep()
} }
/**
* Process until either [destination] is reached or there's nowhere left to check
* @return `this` instance for chaining
*/
fun stepUntilDestination(destination: TileInfo): BFS { fun stepUntilDestination(destination: TileInfo): BFS {
while (!tilesReached.containsKey(destination) && !hasEnded()) while (!tilesReached.containsKey(destination) && !hasEnded())
nextStep() nextStep()
return this return this
} }
/**
* Process one tile-to-search, fetching all neighbors not yet touched
* and adding those that fulfill the [predicate] to the reached set
* and to the yet-to-be-processed set.
*
* Will do nothing when [hasEnded] returns `true`
*/
fun nextStep() { fun nextStep() {
val current = tilesToCheck.remove() val current = tilesToCheck.removeFirstOrNull() ?: return
for (neighbor in current.neighbors) { for (neighbor in current.neighbors) {
if (predicate(neighbor) && !tilesReached.containsKey(neighbor)) { if (neighbor !in tilesReached && predicate(neighbor)) {
tilesReached[neighbor] = current tilesReached[neighbor] = current
tilesToCheck.add(neighbor) tilesToCheck.add(neighbor)
} }
} }
} }
fun getPathTo(destination: TileInfo): ArrayList<TileInfo> { /**
val path = ArrayList<TileInfo>() * @return a Sequence from the [destination] back to the [startingPoint], including both, or empty if [destination] has not been reached
path.add(destination) */
fun getPathTo(destination: TileInfo): Sequence<TileInfo> = sequence {
var currentNode = destination var currentNode = destination
while (currentNode != startingPoint) { while (true) {
val parent = tilesReached[currentNode] val parent = tilesReached[currentNode] ?: break // destination is not in our path
if (parent == null) return ArrayList()// destination is not in our path yield(currentNode)
if (currentNode == startingPoint) break
currentNode = parent currentNode = parent
path.add(currentNode)
} }
return path
} }
/** @return true if there are no more tiles to check */
fun hasEnded() = tilesToCheck.isEmpty() fun hasEnded() = tilesToCheck.isEmpty()
fun hasReachedTile(tile: TileInfo) = /** @return true if the [tile] has been reached */
tilesReached.containsKey(tile) fun hasReachedTile(tile: TileInfo) = tilesReached.containsKey(tile)
}
/** @return all tiles reached so far */
fun getReachedTiles(): MutableSet<TileInfo> = tilesReached.keys
/** @return number of tiles reached so far */
fun size() = tilesReached.size
}

View File

@ -254,10 +254,6 @@ class TileMap {
val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any { val containsViewableNeighborThatCanSeeOver = cTile.neighbors.any {
bNeighbor: TileInfo -> bNeighbor: TileInfo ->
val bNeighborHeight = bNeighbor.getHeight() val bNeighborHeight = bNeighbor.getHeight()
if(cTile.resource=="Marble"
&& bNeighbor.terrainFeatures.contains("Forest")
)
println()
viewableTiles.contains(bNeighbor) && ( viewableTiles.contains(bNeighbor) && (
currentTileHeight > bNeighborHeight // a>b currentTileHeight > bNeighborHeight // a>b
|| cTileHeight > bNeighborHeight // c>b || cTileHeight > bNeighborHeight // c>b

View File

@ -183,12 +183,12 @@ class UnitMovementAlgorithms(val unit:MapUnit) {
val distanceToTiles = getDistanceToTiles() val distanceToTiles = getDistanceToTiles()
class UnreachableDestinationException : Exception() class UnreachableDestinationException(msg: String) : Exception(msg)
// If the tile is far away, we need to build a path how to get there, and then take the first step // If the tile is far away, we need to build a path how to get there, and then take the first step
if (!distanceToTiles.containsKey(finalDestination)) if (!distanceToTiles.containsKey(finalDestination))
return getShortestPath(finalDestination).firstOrNull() return getShortestPath(finalDestination).firstOrNull()
?: throw UnreachableDestinationException() ?: throw UnreachableDestinationException("$unit ${unit.currentTile.position} cannot reach $finalDestination")
// we should be able to get there this turn // we should be able to get there this turn
if (canMoveTo(finalDestination)) if (canMoveTo(finalDestination))
@ -565,4 +565,4 @@ class PathsToTilesWithinTurn : LinkedHashMap<TileInfo, UnitMovementAlgorithms.Pa
} }
return reversePathList.reversed() return reversePathList.reversed()
} }
} }