mirror of
https://github.com/yairm210/Unciv.git
synced 2025-07-04 07:17:50 +07:00
AI worker road priority rework (#10918)
* WorkerAutomation now caches the roads to use * Workers now build roads differently * Fixed error if unit movement is zero * Fixed civ researching an unwanted tech in a test * Fixed spelling * Increased road building priority * getRoadConnectionBetweenCities no longer does unnecessary sorting * roadsToConnectCitiesCache no longer stores roads that are already built * ChooseImprovement now builds roads on resource tiles! * Fixed tryConnectingCities error related to using minByOrNull instead of firstOrNull * Roads can't have a negative value if they are bigger
This commit is contained in:
@ -105,6 +105,12 @@ class WorkerAutomation(
|
||||
result
|
||||
}
|
||||
|
||||
/** Cache of roads to connect cities each turn */
|
||||
private val roadsToConnectCitiesCache: HashMap<City, List<Tile>> = HashMap()
|
||||
|
||||
/** Hashmap of all cached tiles in each list in [roadsToConnectCitiesCache] */
|
||||
private val tilesOfRoadsToConnectCities: HashMap<Tile, City> = HashMap()
|
||||
|
||||
/** Caches BFS by city locations (cities needing connecting).
|
||||
*
|
||||
* key: The city to connect from as [hex position][Vector2].
|
||||
@ -267,14 +273,53 @@ class WorkerAutomation(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a cache to find and return the connection to make that is associated with a city.
|
||||
* May not work if the unit that originally created this cache is different from the next.
|
||||
* (Due to the difference in [UnitMovement.canPassThrough()])
|
||||
*/
|
||||
private fun getRoadConnectionBetweenCities(unit: MapUnit, city: City): List<Tile> {
|
||||
if (city in roadsToConnectCitiesCache) return roadsToConnectCitiesCache[city]!!
|
||||
|
||||
val isCandidateTilePredicate: (Tile) -> Boolean = { it.isLand && unit.movement.canPassThrough(it) }
|
||||
val toConnectTile = city.getCenterTile()
|
||||
val bfs: BFS = bfsCache[toConnectTile.position] ?:
|
||||
BFS(toConnectTile, isCandidateTilePredicate).apply {
|
||||
maxSize = HexMath.getNumberOfTilesInHexagon(
|
||||
WorkerAutomationConst.maxBfsReachPadding +
|
||||
tilesOfConnectedCities.minOf { it.aerialDistanceTo(toConnectTile) }
|
||||
)
|
||||
bfsCache[toConnectTile.position] = this@apply
|
||||
}
|
||||
val cityTilesToSeek = HashSet(tilesOfConnectedCities)
|
||||
|
||||
var nextTile = bfs.nextStep()
|
||||
while (nextTile != null) {
|
||||
if (nextTile in cityTilesToSeek) {
|
||||
// We have a winner!
|
||||
val cityTile = nextTile
|
||||
val pathToCity = bfs.getPathTo(cityTile)
|
||||
roadsToConnectCitiesCache[city] = pathToCity.toList().filter { it.roadStatus != bestRoadAvailable }
|
||||
for (tile in pathToCity) {
|
||||
if (tile !in tilesOfRoadsToConnectCities)
|
||||
tilesOfRoadsToConnectCities[tile] = city
|
||||
}
|
||||
return roadsToConnectCitiesCache[city]!!
|
||||
}
|
||||
nextTile = bfs.nextStep()
|
||||
}
|
||||
|
||||
roadsToConnectCitiesCache[city] = listOf()
|
||||
return roadsToConnectCitiesCache[city]!!
|
||||
}
|
||||
|
||||
/**
|
||||
* Automate one Worker - decide what to do and where, move, start or continue work.
|
||||
*/
|
||||
fun automateWorkerAction(unit: MapUnit, dangerousTiles: HashSet<Tile>) {
|
||||
val currentTile = unit.getTile()
|
||||
|
||||
// Must be called before any getPriority checks to guarantee the local road cache is processed
|
||||
val citiesToConnect = getNearbyCitiesToConnect(unit)
|
||||
// Shortcut, we are working a good tile (like resource) and don't need to check for other tiles to work
|
||||
if (!dangerousTiles.contains(currentTile) && getFullPriority(unit.getTile(), unit) >= 10
|
||||
&& currentTile.improvementInProgress != null) {
|
||||
@ -282,10 +327,6 @@ class WorkerAutomation(
|
||||
}
|
||||
val tileToWork = findTileToWork(unit, dangerousTiles)
|
||||
|
||||
// If we have < 20 GPT lets not spend time connecting roads
|
||||
if (civInfo.stats.statsForNextTurn.gold >= 20
|
||||
&& tryConnectingCities(unit, getImprovementPriority(tileToWork, unit))) return
|
||||
|
||||
if (tileToWork != currentTile) {
|
||||
debug("WorkerAutomation: %s -> head towards %s", unit.label(), tileToWork)
|
||||
val reachedTile = unit.movement.headTowards(tileToWork)
|
||||
@ -340,9 +381,6 @@ class WorkerAutomation(
|
||||
if (automateWorkBoats(unit)) return
|
||||
}
|
||||
|
||||
//Lets check again if we want to build roads because we don't have a tile nearby to improve
|
||||
if (civInfo.stats.statsForNextTurn.gold > 15 && tryConnectingCities(unit, 0f)) return
|
||||
|
||||
val citiesToNumberOfUnimprovedTiles = HashMap<String, Int>()
|
||||
for (city in unit.civ.cities) {
|
||||
citiesToNumberOfUnimprovedTiles[city.id] = city.getTiles()
|
||||
@ -362,7 +400,7 @@ class WorkerAutomation(
|
||||
}
|
||||
|
||||
// Nothing to do, try again to connect cities
|
||||
if (civInfo.stats.statsForNextTurn.gold > 10 && tryConnectingCities(unit, 0f)) return
|
||||
if (civInfo.stats.statsForNextTurn.gold > 10 && tryConnectingCities(unit, citiesToConnect)) return
|
||||
|
||||
|
||||
debug("WorkerAutomation: %s -> nothing to do", unit.label())
|
||||
@ -374,84 +412,59 @@ class WorkerAutomation(
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for work connecting cities
|
||||
* @return whether we actually did anything
|
||||
* Most importantly builds the cache so that [chooseImprovement] knows later what tiles a road should be built on
|
||||
* Returns a list of all the cities close by that this worker may want to connect
|
||||
*/
|
||||
private fun tryConnectingCities(unit: MapUnit, minPriority: Float): Boolean {
|
||||
if (bestRoadAvailable == RoadStatus.None || citiesThatNeedConnecting.isEmpty()) return false
|
||||
val maxDistanceWanted = when {
|
||||
minPriority > 4 -> -1
|
||||
minPriority > 3 -> 0
|
||||
minPriority > 2 -> 1
|
||||
minPriority > 1 -> 2
|
||||
minPriority > 0 -> 10
|
||||
else -> 20
|
||||
}
|
||||
if (maxDistanceWanted < 0) return false
|
||||
|
||||
// Since further away cities take longer to get to and - most importantly - the canReach() to them is very long,
|
||||
// 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.
|
||||
val candidateCities = citiesThatNeedConnecting.asSequence().filter {
|
||||
private fun getNearbyCitiesToConnect(unit: MapUnit): List<City> {
|
||||
if (bestRoadAvailable == RoadStatus.None || citiesThatNeedConnecting.isEmpty()) return listOf()
|
||||
val candidateCities = citiesThatNeedConnecting.filter {
|
||||
// Cities that are too far away make the canReach() calculations devastatingly long
|
||||
it.getCenterTile().aerialDistanceTo(unit.getTile()) < 20
|
||||
}
|
||||
if (candidateCities.none()) return false // do nothing.
|
||||
|
||||
val isCandidateTilePredicate: (Tile) -> Boolean = { it.isLand && unit.movement.canPassThrough(it) }
|
||||
val currentTile = unit.getTile()
|
||||
val cityTilesToSeek = ArrayList(tilesOfConnectedCities.sortedBy { it.aerialDistanceTo(currentTile) })
|
||||
if (candidateCities.none()) return listOf() // do nothing.
|
||||
|
||||
// Search through ALL candidate cities to build the cache
|
||||
for (toConnectCity in candidateCities) {
|
||||
val toConnectTile = toConnectCity.getCenterTile()
|
||||
val bfs: BFS = bfsCache[toConnectTile.position] ?:
|
||||
BFS(toConnectTile, isCandidateTilePredicate).apply {
|
||||
maxSize = HexMath.getNumberOfTilesInHexagon(
|
||||
WorkerAutomationConst.maxBfsReachPadding +
|
||||
tilesOfConnectedCities.minOf { it.aerialDistanceTo(toConnectTile) }
|
||||
)
|
||||
bfsCache[toConnectTile.position] = this@apply
|
||||
}
|
||||
getRoadConnectionBetweenCities(unit, toConnectCity).filter { it.getUnpillagedRoad() < bestRoadAvailable }
|
||||
}
|
||||
return candidateCities
|
||||
}
|
||||
|
||||
while (true) {
|
||||
for (cityTile in cityTilesToSeek.toList()) { // copy since we change while running
|
||||
if (!bfs.hasReachedTile(cityTile)) continue
|
||||
// we have a winner!
|
||||
val pathToCity = bfs.getPathTo(cityTile)
|
||||
val roadableTiles = pathToCity.filter { it.getUnpillagedRoad() < bestRoadAvailable }
|
||||
val tileToConstructRoadOn: Tile
|
||||
if (currentTile in roadableTiles) tileToConstructRoadOn =
|
||||
currentTile
|
||||
else {
|
||||
val reachableTile = roadableTiles
|
||||
.filter { it.aerialDistanceTo(unit.getTile()) <= maxDistanceWanted }
|
||||
.sortedBy { it.aerialDistanceTo(unit.getTile()) }
|
||||
.firstOrNull {
|
||||
unit.movement.canMoveTo(it) && unit.movement.canReach(it)
|
||||
}
|
||||
if (reachableTile == null) {
|
||||
cityTilesToSeek.remove(cityTile) // Apparently we can't reach any of these tiles at all
|
||||
continue
|
||||
}
|
||||
tileToConstructRoadOn = reachableTile
|
||||
unit.movement.headTowards(tileToConstructRoadOn)
|
||||
}
|
||||
if (unit.currentMovement > 0 && currentTile == tileToConstructRoadOn
|
||||
&& currentTile.improvementInProgress != bestRoadAvailable.name) {
|
||||
val improvement = bestRoadAvailable.improvement(ruleSet)!!
|
||||
tileToConstructRoadOn.startWorkingOnImprovement(improvement, civInfo, unit)
|
||||
}
|
||||
debug("WorkerAutomation: %s -> connect city %s to %s on %s",
|
||||
unit.label(), bfs.startingPoint.getCity()?.name, cityTile.getCity()!!.name, tileToConstructRoadOn)
|
||||
return true
|
||||
}
|
||||
if (bfs.hasEnded()) break // We've found another city that this one can connect to
|
||||
bfs.nextStep()
|
||||
}
|
||||
debug("WorkerAutomation: ${unit.label()} -> connect city ${bfs.startingPoint.getCity()?.name} failed at BFS size ${bfs.size()}")
|
||||
/**
|
||||
* Looks for work connecting cities. Used to search for far away roads to build.
|
||||
* @return whether we actually did anything
|
||||
*/
|
||||
private fun tryConnectingCities(unit: MapUnit, candidateCities: List<City>): Boolean {
|
||||
if (bestRoadAvailable == RoadStatus.None || citiesThatNeedConnecting.isEmpty()) return false
|
||||
|
||||
if (candidateCities.none()) return false // do nothing.
|
||||
val currentTile = unit.getTile()
|
||||
var bestTileToConstructRoadOn: Tile? = null
|
||||
var bestTileToConstructRoadOnDist: Int = Int.MAX_VALUE
|
||||
|
||||
// Search through ALL candidate cities for the closest tile to build a road on
|
||||
for (toConnectCity in candidateCities) {
|
||||
val roadableTiles = getRoadConnectionBetweenCities(unit, toConnectCity).filter { it.getUnpillagedRoad() < bestRoadAvailable }
|
||||
val reachableTile = roadableTiles.map { Pair(it, it.aerialDistanceTo(unit.getTile())) }
|
||||
.filter { it.second < bestTileToConstructRoadOnDist }
|
||||
.sortedBy { it.second }
|
||||
.firstOrNull {
|
||||
unit.movement.canMoveTo(it.first) && unit.movement.canReach(it.first)
|
||||
} ?: continue // Apparently we can't reach any of these tiles at all
|
||||
bestTileToConstructRoadOn = reachableTile.first
|
||||
bestTileToConstructRoadOnDist = reachableTile.second
|
||||
}
|
||||
|
||||
return false
|
||||
if (bestTileToConstructRoadOn == null) return false
|
||||
|
||||
if (bestTileToConstructRoadOn != currentTile && unit.currentMovement > 0)
|
||||
unit.movement.headTowards(bestTileToConstructRoadOn)
|
||||
if (unit.currentMovement > 0 && bestTileToConstructRoadOn == currentTile
|
||||
&& currentTile.improvementInProgress != bestRoadAvailable.name) {
|
||||
val improvement = bestRoadAvailable.improvement(ruleSet)!!
|
||||
bestTileToConstructRoadOn.startWorkingOnImprovement(improvement, civInfo, unit)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
@ -525,6 +538,12 @@ class WorkerAutomation(
|
||||
&& !civInfo.hasResource(tile.resource!!))
|
||||
priority += 2
|
||||
}
|
||||
if (tile in tilesOfRoadsToConnectCities) priority += when {
|
||||
civInfo.stats.statsForNextTurn.gold <= 5 -> 0
|
||||
civInfo.stats.statsForNextTurn.gold <= 10 -> 1
|
||||
civInfo.stats.statsForNextTurn.gold <= 30 -> 2
|
||||
else -> 3
|
||||
}
|
||||
tileRankings[tile] = TileImprovementRank(priority)
|
||||
return priority + unitSpecificPriority
|
||||
}
|
||||
@ -626,6 +645,7 @@ class WorkerAutomation(
|
||||
|
||||
// After gathering all the data, we conduct the hierarchy in one place
|
||||
val improvementString = when {
|
||||
bestBuildableImprovement != null && bestBuildableImprovement.isRoad() -> bestBuildableImprovement.name
|
||||
improvementStringForResource != null -> if (improvementStringForResource==tile.improvement) null else improvementStringForResource
|
||||
// If this is a resource that HAS an improvement that we can see, but this unit can't build it, don't waste your time
|
||||
tile.resource != null && tile.hasViewableResource(civInfo) && tile.tileResource.getImprovements().any() -> return null
|
||||
@ -648,7 +668,20 @@ class WorkerAutomation(
|
||||
|
||||
private fun getImprovementRanking(tile: Tile, unit: MapUnit, improvementName: String, localUniqueCache: LocalUniqueCache): Float {
|
||||
val improvement = ruleSet.tileImprovements[improvementName]!!
|
||||
|
||||
|
||||
// Add the value of roads if we want to build it here
|
||||
if (improvement.isRoad() && bestRoadAvailable.improvement(ruleSet) == improvement
|
||||
&& tile in tilesOfRoadsToConnectCities) {
|
||||
var value = 1f
|
||||
val city = tilesOfRoadsToConnectCities[tile]!!
|
||||
if (civInfo.stats.statsForNextTurn.gold >= 20)
|
||||
// Bigger cities have a higher priority to connect
|
||||
value += (city.population.population - 3) * .3f
|
||||
// Higher priority if we are closer to connecting the city
|
||||
value += (5 - roadsToConnectCitiesCache[city]!!.size).coerceAtLeast(0)
|
||||
return value
|
||||
}
|
||||
|
||||
// If this tile is not in our territory or neighboring it, it has no value
|
||||
if (tile.getOwner() != unit.civ
|
||||
// Check if it is not an unowned neighboring tile that can be in city range
|
||||
|
@ -47,15 +47,16 @@ class BFS(
|
||||
*
|
||||
* Will do nothing when [hasEnded] returns `true`
|
||||
*/
|
||||
fun nextStep() {
|
||||
if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return }
|
||||
val current = tilesToCheck.removeFirstOrNull() ?: return
|
||||
fun nextStep(): Tile? {
|
||||
if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return null }
|
||||
val current = tilesToCheck.removeFirstOrNull() ?: return null
|
||||
for (neighbor in current.neighbors) {
|
||||
if (neighbor !in tilesReached && predicate(neighbor)) {
|
||||
tilesReached[neighbor] = current
|
||||
tilesToCheck.add(neighbor)
|
||||
}
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -292,12 +292,13 @@ internal class WorkerAutomationTest {
|
||||
// Prevent any sort of worker spawning
|
||||
civInfo.addGold(-civInfo.gold)
|
||||
civInfo.policies.freePolicies = 0
|
||||
civInfo.addStat(Stat.Science, - 100000)
|
||||
|
||||
NextTurnAutomation.automateCivMoves(civInfo)
|
||||
TurnManager(civInfo).endTurn()
|
||||
// Invalidate WorkerAutomationCache
|
||||
testGame.gameInfo.turns++
|
||||
// Because the civ will annoyingly try to research it again
|
||||
civInfo.tech.techsResearched.remove(testGame.ruleset.tileImprovements["Farm"]!!.techRequired!!)
|
||||
}
|
||||
|
||||
var finishedCount = 0
|
||||
|
Reference in New Issue
Block a user