Separated road automation to a separate file, but actually we have 2 completely different functions here, not sure if requires subdivision

This commit is contained in:
Yair Morgenstern 2024-02-02 00:14:39 +02:00
parent 88c7804490
commit 779fd51d9e
3 changed files with 341 additions and 321 deletions

View File

@ -28,8 +28,8 @@ object CivilianUnitAutomation {
if (unit.hasUnique(UniqueType.FoundCity))
return SpecificUnitAutomation.automateSettlerActions(unit, dangerousTiles)
if(unit.isAutomatingRoadConnection())
return unit.civ.getWorkerAutomation().automateConnectRoad(unit, dangerousTiles)
if (unit.isAutomatingRoadConnection())
return unit.civ.getWorkerAutomation().roadAutomation.automateConnectRoad(unit, dangerousTiles)
if (unit.cache.hasUniqueToBuildImprovements)
return unit.civ.getWorkerAutomation().automateWorkerAction(unit, dangerousTiles)

View File

@ -0,0 +1,324 @@
package com.unciv.logic.automation.unit
import com.badlogic.gdx.math.Vector2
import com.unciv.UncivGame
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.map.BFS
import com.unciv.logic.map.HexMath
import com.unciv.logic.map.MapPathing
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.RoadStatus
import com.unciv.logic.map.tile.Tile
import com.unciv.utils.Log
import com.unciv.utils.debug
private object WorkerAutomationConst {
/** BFS max size is determined by the aerial distance of two cities to connect, padded with this */
// two tiles longer than the distance to the nearest connected city should be enough as the 'reach' of a BFS is increased by blocked tiles
const val maxBfsReachPadding = 2
}
class RoadAutomation(val civInfo: Civilization, cachedForTurn:Int, cloningSource: RoadAutomation? = null) {
//region Cache
private val ruleSet = civInfo.gameInfo.ruleset
/** Caches BFS by city locations (cities needing connecting).
*
* key: The city to connect from as [hex position][Vector2].
*
* value: The [BFS] searching from that city, whether successful or not.
*/
//todo: If BFS were to deal in vectors instead of Tiles, we could copy this on cloning
private val bfsCache = HashMap<Vector2, BFS>()
/** Caches road to build for connecting cities unless option is off or ruleset removed all roads */
internal val bestRoadAvailable: RoadStatus =
cloningSource?.bestRoadAvailable ?:
//Player can choose not to auto-build roads & railroads.
if (civInfo.isHuman() && (!UncivGame.Current.settings.autoBuildingRoads
|| UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI()))
RoadStatus.None
else civInfo.tech.getBestRoadAvailable()
/** Same as above, but ignores the option */
private val actualBestRoadAvailable: RoadStatus = civInfo.tech.getBestRoadAvailable()
/** Civ-wide list of unconnected Cities, sorted by closest to capital first */
private val citiesThatNeedConnecting: List<City> by lazy {
val result = civInfo.cities.asSequence()
.filter {
civInfo.getCapital() != null
&& it.population.population > 3
&& !it.isCapital() && !it.isBeingRazed // Cities being razed should not be connected.
&& !it.cityStats.isConnectedToCapital(bestRoadAvailable)
}.sortedBy {
it.getCenterTile().aerialDistanceTo(civInfo.getCapital()!!.getCenterTile())
}.toList()
if (Log.shouldLog()) {
debug("WorkerAutomation citiesThatNeedConnecting for ${civInfo.civName} turn $cachedForTurn:")
if (result.isEmpty())
debug("\tempty")
else result.forEach {
debug("\t${it.name}")
}
}
result
}
/** Civ-wide list of _connected_ Cities, unsorted */
private val tilesOfConnectedCities: List<Tile> by lazy {
val result = civInfo.cities.asSequence()
.filter { it.isCapital() || it.cityStats.isConnectedToCapital(bestRoadAvailable) }
.map { it.getCenterTile() }
.toList()
if (Log.shouldLog()) {
debug("WorkerAutomation tilesOfConnectedCities for ${civInfo.civName} turn $cachedForTurn:")
if (result.isEmpty())
debug("\tempty")
else result.forEach {
debug("\t$it")
}
}
result
}
/** Cache of roads to connect cities each turn */
internal val roadsToConnectCitiesCache: HashMap<City, List<Tile>> = HashMap()
/** Hashmap of all cached tiles in each list in [roadsToConnectCitiesCache] */
internal val tilesOfRoadsToConnectCities: HashMap<Tile, City> = HashMap()
//endregion
//region Functions
/**
* Automate the process of connecting a road between two points.
* Current thoughts:
* Will be a special case of MapUnit.automated property
* Unit has new attributes startTile endTile
* - We will progress towards the end path sequentially, taking absolute least distance w/o regard for movement cost
* - Cancel upon risk of capture
* - Cancel upon blocked
* - End automation upon finish
*/
// TODO: Caching
// TODO: Hide the automate road button if road is not unlocked
fun automateConnectRoad(unit: MapUnit, tilesWhereWeWillBeCaptured: Set<Tile>){
if (actualBestRoadAvailable == RoadStatus.None) return
var currentTile = unit.getTile()
/** Reset side effects from automation, return worker to non-automated state*/
fun stopAndCleanAutomation(){
unit.automated = false
unit.action = null
unit.automatedRoadConnectionDestination = null
unit.automatedRoadConnectionPath = null
currentTile.stopWorkingOnImprovement()
}
if (unit.automatedRoadConnectionDestination == null){
stopAndCleanAutomation()
return
}
/** Conditions for whether it is acceptable to build a road on this tile */
fun shouldBuildRoadOnTile(tile: Tile): Boolean {
return !tile.isCityCenter() // Can't build road on city tiles
// Special case for civs that treat forest/jungles as roads (inside their territory). We shouldn't build if railroads aren't unlocked.
&& !(tile.hasConnection(unit.civ) && actualBestRoadAvailable == RoadStatus.Road)
// Build (upgrade) if possible
&& tile.roadStatus != actualBestRoadAvailable
// Build if the road is pillaged
|| tile.roadIsPillaged
}
val destinationTile = unit.civ.gameInfo.tileMap[unit.automatedRoadConnectionDestination!!]
var pathToDest: List<Vector2>? = unit.automatedRoadConnectionPath
// The path does not exist, create it
if (pathToDest == null) {
val foundPath: List<Tile>? = MapPathing.getRoadPath(unit, currentTile, destinationTile)
if (foundPath == null) {
Log.debug("WorkerAutomation: $unit -> connect road failed")
stopAndCleanAutomation()
unit.civ.addNotification("Connect road failed!", currentTile.position, NotificationCategory.Units, NotificationIcon.Construction)
return
}
pathToDest = foundPath // Convert to a list of positions for serialization
.map { it.position }
unit.automatedRoadConnectionPath = pathToDest
debug("WorkerAutomation: $unit -> found connect road path to destination tile: %s, %s", destinationTile, pathToDest)
}
val currTileIndex = pathToDest.indexOf(currentTile.position)
// The worker was somehow moved off its path, cancel the action
if (currTileIndex == -1) {
Log.debug("$unit -> was moved off its connect road path. Operation cancelled.")
stopAndCleanAutomation()
unit.civ.addNotification("Connect road cancelled!", currentTile.position, NotificationCategory.Units, unit.name)
return
}
/* Can not build a road on this tile, try to move on.
* The worker should search for the next furthest tile in the path that:
* - It can move to
* - Can be improved/upgraded
* */
if (unit.currentMovement > 0 && !shouldBuildRoadOnTile(currentTile)) {
if (currTileIndex == pathToDest.size - 1) { // The last tile in the path is unbuildable or has a road.
stopAndCleanAutomation()
unit.civ.addNotification("Connect road completed!", currentTile.position, NotificationCategory.Units, unit.name)
return
}
if (currTileIndex < pathToDest.size - 1) { // Try to move to the next tile in the path
val tileMap = unit.civ.gameInfo.tileMap
var nextTile: Tile = currentTile
// Create a new list with tiles where the index is greater than currTileIndex
val futureTiles = pathToDest.asSequence()
.dropWhile { it != unit.currentTile.position }
.drop(1)
.map { tileMap[it] }
for (futureTile in futureTiles) { // Find the furthest tile we can reach in this turn, move to, and does not have a road
if (unit.movement.canReachInCurrentTurn(futureTile) && unit.movement.canMoveTo(futureTile)) { // We can at least move to this tile
nextTile = futureTile
if (shouldBuildRoadOnTile(futureTile)) {
break // Stop on this tile
}
}
}
unit.movement.moveToTile(nextTile)
currentTile = unit.getTile()
}
}
// We need to check current movement again after we've (potentially) moved
if (unit.currentMovement > 0) {
// Repair pillaged roads first
if (currentTile.roadStatus != RoadStatus.None && currentTile.roadIsPillaged){
currentTile.setRepaired()
return
}
if (shouldBuildRoadOnTile(currentTile) && currentTile.improvementInProgress != actualBestRoadAvailable.name) {
val improvement = actualBestRoadAvailable.improvement(ruleSet)!!
currentTile.startWorkingOnImprovement(improvement, civInfo, unit)
return
}
}
}
/**
* 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]!!
}
/**
* 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
*/
internal 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 listOf() // do nothing.
// Search through ALL candidate cities to build the cache
for (toConnectCity in candidateCities) {
getRoadConnectionBetweenCities(unit, toConnectCity).filter { it.getUnpillagedRoad() < bestRoadAvailable }
}
return candidateCities
}
/**
* Looks for work connecting cities. Used to search for far away roads to build.
* @return whether we actually did anything
*/
internal 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
}
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
}
//endregion
}

View File

@ -1,22 +1,15 @@
package com.unciv.logic.automation.unit
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.automation.Automation
import com.unciv.logic.automation.ThreatLevel
import com.unciv.logic.automation.civilization.NextTurnAutomation
import com.unciv.logic.automation.unit.UnitAutomation.wander
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.logic.civilization.NotificationCategory
import com.unciv.logic.civilization.NotificationIcon
import com.unciv.logic.civilization.diplomacy.RelationshipLevel
import com.unciv.logic.map.BFS
import com.unciv.logic.map.HexMath
import com.unciv.logic.map.MapPathing
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.RoadStatus
import com.unciv.logic.map.tile.Tile
import com.unciv.logic.map.tile.TileStatFunctions
import com.unciv.logic.map.tile.toStats
@ -28,16 +21,9 @@ import com.unciv.models.ruleset.unique.LocalUniqueCache
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsFromUniques
import com.unciv.utils.Log
import com.unciv.utils.debug
import kotlin.math.abs
private object WorkerAutomationConst {
/** BFS max size is determined by the aerial distance of two cities to connect, padded with this */
// two tiles longer than the distance to the nearest connected city should be enough as the 'reach' of a BFS is increased by blocked tiles
const val maxBfsReachPadding = 2
}
/**
* Contains the logic for worker automation.
*
@ -53,84 +39,16 @@ class WorkerAutomation(
) {
///////////////////////////////////////// Cached data /////////////////////////////////////////
val roadAutomation:RoadAutomation = RoadAutomation(civInfo, cachedForTurn, cloningSource?.roadAutomation)
private val ruleSet = civInfo.gameInfo.ruleset
/** Caches road to build for connecting cities unless option is off or ruleset removed all roads */
private val bestRoadAvailable: RoadStatus =
cloningSource?.bestRoadAvailable ?:
//Player can choose not to auto-build roads & railroads.
if (civInfo.isHuman() && (!UncivGame.Current.settings.autoBuildingRoads
|| UncivGame.Current.settings.autoPlay.isAutoPlayingAndFullAI()))
RoadStatus.None
else civInfo.tech.getBestRoadAvailable()
/** Same as above, but ignores the option */
private val actualBestRoadAvailable: RoadStatus = civInfo.tech.getBestRoadAvailable()
/** Civ-wide list of unconnected Cities, sorted by closest to capital first */
private val citiesThatNeedConnecting: List<City> by lazy {
val result = civInfo.cities.asSequence()
.filter {
civInfo.getCapital() != null
&& it.population.population > 3
&& !it.isCapital() && !it.isBeingRazed // Cities being razed should not be connected.
&& !it.cityStats.isConnectedToCapital(bestRoadAvailable)
}.sortedBy {
it.getCenterTile().aerialDistanceTo(civInfo.getCapital()!!.getCenterTile())
}.toList()
if (Log.shouldLog()) {
debug("WorkerAutomation citiesThatNeedConnecting for ${civInfo.civName} turn $cachedForTurn:")
if (result.isEmpty())
debug("\tempty")
else result.forEach {
debug("\t${it.name}")
}
}
result
}
/** Civ-wide list of _connected_ Cities, unsorted */
private val tilesOfConnectedCities: List<Tile> by lazy {
val result = civInfo.cities.asSequence()
.filter { it.isCapital() || it.cityStats.isConnectedToCapital(bestRoadAvailable) }
.map { it.getCenterTile() }
.toList()
if (Log.shouldLog()) {
debug("WorkerAutomation tilesOfConnectedCities for ${civInfo.civName} turn $cachedForTurn:")
if (result.isEmpty())
debug("\tempty")
else result.forEach {
debug("\t$it")
}
}
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].
*
* value: The [BFS] searching from that city, whether successful or not.
*/
//todo: If BFS were to deal in vectors instead of Tiles, we could copy this on cloning
private val bfsCache = HashMap<Vector2, BFS>()
//todo: UnitMovement.canReach still very expensive and could benefit from caching, it's not using BFS
///////////////////////////////////////// Helpers /////////////////////////////////////////
companion object {
/** For console logging only */
private fun MapUnit.label() = toString() + " " + getTile().position.toString()
}
/**
* Each object has two stages, this first one is checking the basic priority without any improvements.
* If tilePriority is -1 then it must be a dangerous tile.
@ -146,178 +64,13 @@ class WorkerAutomation(
///////////////////////////////////////// Methods /////////////////////////////////////////
/**
* Automate the process of connecting a road between two points.
* Current thoughts:
* Will be a special case of MapUnit.automated property
* Unit has new attributes startTile endTile
* - We will progress towards the end path sequentially, taking absolute least distance w/o regard for movement cost
* - Cancel upon risk of capture
* - Cancel upon blocked
* - End automation upon finish
*/
// TODO: Caching
// TODO: Hide the automate road button if road is not unlocked
fun automateConnectRoad(unit: MapUnit, tilesWhereWeWillBeCaptured: Set<Tile>){
if (actualBestRoadAvailable == RoadStatus.None) return
var currentTile = unit.getTile()
/** Reset side effects from automation, return worker to non-automated state*/
fun stopAndCleanAutomation(){
unit.automated = false
unit.action = null
unit.automatedRoadConnectionDestination = null
unit.automatedRoadConnectionPath = null
currentTile.stopWorkingOnImprovement()
}
if (unit.automatedRoadConnectionDestination == null){
stopAndCleanAutomation()
return
}
/** Conditions for whether it is acceptable to build a road on this tile */
fun shouldBuildRoadOnTile(tile: Tile): Boolean {
return !tile.isCityCenter() // Can't build road on city tiles
// Special case for civs that treat forest/jungles as roads (inside their territory). We shouldn't build if railroads aren't unlocked.
&& !(tile.hasConnection(unit.civ) && actualBestRoadAvailable == RoadStatus.Road)
// Build (upgrade) if possible
&& tile.roadStatus != actualBestRoadAvailable
// Build if the road is pillaged
|| tile.roadIsPillaged
}
val destinationTile = unit.civ.gameInfo.tileMap[unit.automatedRoadConnectionDestination!!]
var pathToDest: List<Vector2>? = unit.automatedRoadConnectionPath
// The path does not exist, create it
if (pathToDest == null) {
val foundPath: List<Tile>? = MapPathing.getRoadPath(unit, currentTile, destinationTile)
if (foundPath == null) {
Log.debug("WorkerAutomation: ${unit.label()} -> connect road failed")
stopAndCleanAutomation()
unit.civ.addNotification("Connect road failed!", currentTile.position, NotificationCategory.Units, NotificationIcon.Construction)
return
}
pathToDest = foundPath // Convert to a list of positions for serialization
.map { it.position }
unit.automatedRoadConnectionPath = pathToDest
debug("WorkerAutomation: ${unit.label()} -> found connect road path to destination tile: %s, %s", destinationTile, pathToDest)
}
val currTileIndex = pathToDest.indexOf(currentTile.position)
// The worker was somehow moved off its path, cancel the action
if (currTileIndex == -1) {
Log.debug("${unit.label()} -> was moved off its connect road path. Operation cancelled.")
stopAndCleanAutomation()
unit.civ.addNotification("Connect road cancelled!", currentTile.position, NotificationCategory.Units, unit.name)
return
}
/* Can not build a road on this tile, try to move on.
* The worker should search for the next furthest tile in the path that:
* - It can move to
* - Can be improved/upgraded
* */
if (unit.currentMovement > 0 && !shouldBuildRoadOnTile(currentTile)) {
if (currTileIndex == pathToDest.size - 1) { // The last tile in the path is unbuildable or has a road.
stopAndCleanAutomation()
unit.civ.addNotification("Connect road completed!", currentTile.position, NotificationCategory.Units, unit.name)
return
}
if (currTileIndex < pathToDest.size - 1) { // Try to move to the next tile in the path
val tileMap = unit.civ.gameInfo.tileMap
var nextTile: Tile = currentTile
// Create a new list with tiles where the index is greater than currTileIndex
val futureTiles = pathToDest.asSequence()
.dropWhile { it != unit.currentTile.position }
.drop(1)
.map { tileMap[it] }
for (futureTile in futureTiles) { // Find the furthest tile we can reach in this turn, move to, and does not have a road
if (unit.movement.canReachInCurrentTurn(futureTile) && unit.movement.canMoveTo(futureTile)) { // We can at least move to this tile
nextTile = futureTile
if (shouldBuildRoadOnTile(futureTile)) {
break // Stop on this tile
}
}
}
unit.movement.moveToTile(nextTile)
currentTile = unit.getTile()
}
}
// We need to check current movement again after we've (potentially) moved
if (unit.currentMovement > 0) {
// Repair pillaged roads first
if (currentTile.roadStatus != RoadStatus.None && currentTile.roadIsPillaged){
currentTile.setRepaired()
return
}
if (shouldBuildRoadOnTile(currentTile) && currentTile.improvementInProgress != actualBestRoadAvailable.name) {
val improvement = actualBestRoadAvailable.improvement(ruleSet)!!
currentTile.startWorkingOnImprovement(improvement, civInfo, unit)
return
}
}
}
/**
* 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)
val citiesToConnect = roadAutomation.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) {
@ -326,7 +79,7 @@ class WorkerAutomation(
val tileToWork = findTileToWork(unit, dangerousTiles)
if (tileToWork != currentTile) {
debug("WorkerAutomation: %s -> head towards %s", unit.label(), tileToWork)
debug("WorkerAutomation: %s -> head towards %s", unit.toString(), tileToWork)
val reachedTile = unit.movement.headTowards(tileToWork)
if (reachedTile != currentTile) unit.doAction() // otherwise, we get a situation where the worker is automated, so it tries to move but doesn't, then tries to automate, then move, etc, forever. Stack overflow exception!
@ -341,14 +94,14 @@ class WorkerAutomation(
// Unit may stop due to Enemy Unit within walking range during doAction() call
if (unit.currentMovement > 0 && reachedTile == tileToWork) {
if (reachedTile.isPillaged()) {
debug("WorkerAutomation: ${unit.label()} -> repairs $reachedTile")
debug("WorkerAutomation: $unit -> repairs $reachedTile")
UnitActionsFromUniques.getRepairAction(unit)?.action?.invoke()
return
}
if (reachedTile.improvementInProgress == null && reachedTile.isLand
&& tileHasWorkToDo(reachedTile, unit)
) {
debug("WorkerAutomation: ${unit.label()} -> start improving $reachedTile")
debug("WorkerAutomation: $unit -> start improving $reachedTile")
return reachedTile.startWorkingOnImprovement(tileRankings[reachedTile]!!.bestImprovement!!, civInfo, unit)
}
}
@ -360,12 +113,12 @@ class WorkerAutomation(
if (tileHasWorkToDo(currentTile, unit)) {
val tileRankings = tileRankings[currentTile]!!
if (tileRankings.repairImprovment!!) {
debug("WorkerAutomation: ${unit.label()} -> repairs $currentTile")
debug("WorkerAutomation: $unit -> repairs $currentTile")
UnitActionsFromUniques.getRepairAction(unit)?.action?.invoke()
return
}
if (tileRankings.bestImprovement != null) {
debug("WorkerAutomation: ${unit.label()} -> start improving $currentTile")
debug("WorkerAutomation: $unit} -> start improving $currentTile")
return currentTile.startWorkingOnImprovement(tileRankings.bestImprovement!!, civInfo, unit)
} else {
throw IllegalStateException("We didn't find anything to improve on this tile even though there was supposed to be something to improve!")
@ -389,17 +142,17 @@ class WorkerAutomation(
.firstOrNull { unit.movement.canReach(it.getCenterTile()) } //goto most undeveloped city
if (closestUndevelopedCity != null && closestUndevelopedCity != currentTile.owningCity) {
debug("WorkerAutomation: %s -> head towards undeveloped city %s", unit.label(), closestUndevelopedCity.name)
debug("WorkerAutomation: %s -> head towards undeveloped city %s", unit, closestUndevelopedCity.name)
val reachedTile = unit.movement.headTowards(closestUndevelopedCity.getCenterTile())
if (reachedTile != currentTile) unit.doAction() // since we've moved, maybe we can do something here - automate
return
}
// Nothing to do, try again to connect cities
if (civInfo.stats.statsForNextTurn.gold > 10 && tryConnectingCities(unit, citiesToConnect)) return
if (civInfo.stats.statsForNextTurn.gold > 10 && roadAutomation.tryConnectingCities(unit, citiesToConnect)) return
debug("WorkerAutomation: %s -> nothing to do", unit.label())
debug("WorkerAutomation: %s -> nothing to do", unit.toString())
unit.civ.addNotification("${unit.shortDisplayName()} has no work to do.", currentTile.position, NotificationCategory.Units, unit.name, "OtherIcons/Sleep")
// Idle CS units should wander so they don't obstruct players so much
@ -407,62 +160,6 @@ class WorkerAutomation(
wander(unit, stayInTerritory = true, tilesToAvoid = dangerousTiles)
}
/**
* 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 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 listOf() // do nothing.
// Search through ALL candidate cities to build the cache
for (toConnectCity in candidateCities) {
getRoadConnectionBetweenCities(unit, toConnectCity).filter { it.getUnpillagedRoad() < bestRoadAvailable }
}
return candidateCities
}
/**
* 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
}
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
}
/**
* Looks for a worthwhile tile to improve
* @return The current tile if no tile to work was found
@ -533,7 +230,7 @@ class WorkerAutomation(
&& !civInfo.hasResource(tile.resource!!))
priority += 2
}
if (tile in tilesOfRoadsToConnectCities) priority += when {
if (tile in roadAutomation.tilesOfRoadsToConnectCities) priority += when {
civInfo.stats.statsForNextTurn.gold <= 5 -> 0
civInfo.stats.statsForNextTurn.gold <= 10 -> 1
civInfo.stats.statsForNextTurn.gold <= 30 -> 2
@ -672,15 +369,15 @@ class WorkerAutomation(
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) {
if (improvement.isRoad() && roadAutomation.bestRoadAvailable.improvement(ruleSet) == improvement
&& tile in roadAutomation.tilesOfRoadsToConnectCities) {
var value = 1f
val city = tilesOfRoadsToConnectCities[tile]!!
val city = roadAutomation.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)
value += (5 - roadAutomation.roadsToConnectCitiesCache[city]!!.size).coerceAtLeast(0)
return value
}
@ -835,7 +532,6 @@ class WorkerAutomation(
val distanceToEnemyCity = tile.aerialDistanceTo(closestEnemyCity)
// Find our closest city to defend from this enemy city
val closestCity = civInfo.cities.minByOrNull { it.getCenterTile().aerialDistanceTo(tile) }!!.getCenterTile()
val distanceToCity = tile.aerialDistanceTo(closestCity)
val distanceBetweenCities = closestEnemyCity.aerialDistanceTo(closestCity)
// Find the distance between the target enemy city to our closest city
val distanceOfEnemyCityToClosestCityOfUs = civInfo.cities.map { it.getCenterTile().aerialDistanceTo(closestEnemyCity) }.minBy { it }