WorkerAutomation cached per Civ - BFS cached (#4868)

* WorkerAutomation cached per Civ - BFS cached

* WorkerAutomation cached per Civ - more linting
This commit is contained in:
SomeTroglodyte 2021-08-20 00:25:41 +02:00 committed by GitHub
parent d856efac06
commit 970259a0ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 394 additions and 191 deletions

View File

@ -9,7 +9,6 @@ import com.unciv.logic.civilization.*
import com.unciv.logic.map.TileInfo
import com.unciv.logic.map.TileMap
import com.unciv.models.Religion
import com.unciv.models.metadata.BaseRuleset
import com.unciv.models.metadata.GameParameters
import com.unciv.models.ruleset.Difficulty
import com.unciv.models.ruleset.Ruleset
@ -462,4 +461,4 @@ class GameInfoPreview {
var gameId = ""
var currentPlayer = ""
fun getCivilization(civName: String) = civilizations.first { it.civName == civName }
}
}

View File

@ -23,7 +23,7 @@ import kotlin.math.min
object NextTurnAutomation {
/** Top-level AI turn tasklist */
/** Top-level AI turn task list */
fun automateCivMoves(civInfo: CivilizationInfo) {
if (civInfo.isBarbarian()) return BarbarianAutomation(civInfo).automate()
@ -39,7 +39,7 @@ object NextTurnAutomation {
offerResearchAgreement(civInfo)
exchangeLuxuries(civInfo)
issueRequests(civInfo)
adoptPolicy(civInfo)
adoptPolicy(civInfo) //todo can take a second - why?
choosePantheon(civInfo)
} else {
getFreeTechForCityStates(civInfo)
@ -50,8 +50,8 @@ object NextTurnAutomation {
automateCityBombardment(civInfo)
useGold(civInfo)
protectCityStates(civInfo)
automateUnits(civInfo)
reassignWorkedTiles(civInfo)
automateUnits(civInfo) // this is the most expensive part
reassignWorkedTiles(civInfo) // second most expensive
trainSettler(civInfo)
tryVoteForDiplomaticVictory(civInfo)
@ -183,20 +183,30 @@ object NextTurnAutomation {
if (researchableTechs.isEmpty()) return
val cheapestTechs = techsGroups[costs[0]]!!
//Do not consider advanced techs if only one tech left in cheapest groupe
val techToResearch: Technology
if (cheapestTechs.size == 1 || costs.size == 1) {
techToResearch = cheapestTechs.random()
} else {
//Choose randomly between cheapest and second cheapest groupe
val techsAdvanced = techsGroups[costs[1]]!!
techToResearch = (cheapestTechs + techsAdvanced).random()
}
//Do not consider advanced techs if only one tech left in cheapest group
val techToResearch: Technology =
if (cheapestTechs.size == 1 || costs.size == 1) {
cheapestTechs.random()
} else {
//Choose randomly between cheapest and second cheapest group
val techsAdvanced = techsGroups[costs[1]]!!
(cheapestTechs + techsAdvanced).random()
}
civInfo.tech.techsToResearch.add(techToResearch.name)
}
}
private object PolicyPriorityMap {
//todo This should be moddable, and needs an update to include new G&K Policies
/** Maps [VictoryType] to an ordered List of PolicyBranch names - the AI will prefer them in that order */
val priorities = mapOf(
VictoryType.Cultural to listOf("Piety", "Freedom", "Tradition", "Commerce", "Patronage"),
VictoryType.Scientific to listOf("Rationalism", "Commerce", "Liberty", "Order", "Patronage"),
VictoryType.Domination to listOf("Autocracy", "Honor", "Liberty", "Rationalism", "Commerce"),
VictoryType.Diplomatic to listOf("Patronage", "Commerce", "Rationalism", "Freedom", "Tradition")
)
}
private fun adoptPolicy(civInfo: CivilizationInfo) {
while (civInfo.policies.canAdoptPolicy()) {
@ -206,20 +216,11 @@ object NextTurnAutomation {
// This can happen if the player is crazy enough to have the game continue forever and he disabled cultural victory
if (adoptablePolicies.isEmpty()) return
val preferredVictoryType = civInfo.victoryType()
val policyBranchPriority =
when (preferredVictoryType) {
VictoryType.Cultural -> listOf("Piety", "Freedom", "Tradition", "Commerce", "Patronage")
VictoryType.Scientific -> listOf("Rationalism", "Commerce", "Liberty", "Order", "Patronage")
VictoryType.Domination -> listOf("Autocracy", "Honor", "Liberty", "Rationalism", "Commerce")
VictoryType.Diplomatic -> listOf("Patronage", "Commerce", "Rationalism", "Freedom", "Tradition")
VictoryType.Neutral -> listOf()
}
val policyBranchPriority = PolicyPriorityMap.priorities[civInfo.victoryType()]
?: emptyList()
val policiesByPreference = adoptablePolicies
.groupBy {
if (it.branch.name in policyBranchPriority)
policyBranchPriority.indexOf(it.branch.name) else 10
.groupBy { policy ->
policyBranchPriority.indexOf(policy.branch.name).let { if (it == -1) 99 else it }
}
val preferredPolicies = policiesByPreference.minByOrNull { it.key }!!.value
@ -235,7 +236,7 @@ object NextTurnAutomation {
// the functions for choosing beliefs total in at around 400 lines.
// https://github.com/Gedemon/Civ5-DLL/blob/aa29e80751f541ae04858b6d2a2c7dcca454201e/CvGameCoreDLL_Expansion1/CvReligionClasses.cpp
// line 4426 through 4870.
// This is way to much work for now, so I'll just choose a random pantheon instead.
// This is way too much work for now, so I'll just choose a random pantheon instead.
// Should probably be changed later, but it works for now.
// If this is omitted, the AI will never choose a religion,
// instead automatically choosing the same one as the player,
@ -300,6 +301,7 @@ object NextTurnAutomation {
}
}
@Suppress("unused") //todo: Work in Progress?
private fun offerDeclarationOfFriendship(civInfo: CivilizationInfo) {
val civsThatWeCanDeclareFriendshipWith = civInfo.getKnownCivs()
.asSequence()
@ -369,7 +371,7 @@ object NextTurnAutomation {
val enemyCivs = civInfo.getKnownCivs()
.filterNot {
it == civInfo || it.cities.isEmpty() || !civInfo.getDiplomacyManager(it).canDeclareWar()
|| it.cities.none { civInfo.exploredTiles.contains(it.location) }
|| it.cities.none { city -> civInfo.exploredTiles.contains(city.location) }
}
// If the AI declares war on a civ without knowing the location of any cities, it'll just keep amassing an army and not sending it anywhere,
// and end up at a massive disadvantage
@ -452,7 +454,7 @@ object NextTurnAutomation {
if (diplomacyManager.resourcesFromTrade().any { it.amount > 0 })
modifierMap["Receiving trade resources"] = -5
if (theirCity.getTiles().none { it.neighbors.any { it.getOwner() == theirCity.civInfo && it.getCity() != theirCity } })
if (theirCity.getTiles().none { tile -> tile.neighbors.any { it.getOwner() == theirCity.civInfo && it.getCity() != theirCity } })
modifierMap["Isolated city"] = 15
//Maybe not needed if city-state has potential protectors?
@ -566,7 +568,7 @@ object NextTurnAutomation {
}
// Technically, this function should also check for civs that have liberated one or more cities
// Hoewever, that can be added in another update, this PR is large enough as it is.
// However, that can be added in another update, this PR is large enough as it is.
private fun tryVoteForDiplomaticVictory(civInfo: CivilizationInfo) {
if (!civInfo.mayVoteForDiplomaticVictory()) return
val chosenCiv: String? = if (civInfo.isMajorCiv()) {

View File

@ -68,7 +68,7 @@ object SpecificUnitAutomation {
.flatMap { it.cities }.asSequence()
// find the suitable tiles (or their neighbours)
val tileToSteal = enemyCities.flatMap { it.getTiles() } // City tiles
.filter { it.neighbors.any { it.getOwner() != unit.civInfo } } // Edge city tiles
.filter { it.neighbors.any { tile -> tile.getOwner() != unit.civInfo } } // Edge city tiles
.flatMap { it.neighbors.asSequence() } // Neighbors of edge city tiles
.filter {
it in unit.civInfo.viewableTiles // we can see them
@ -80,7 +80,7 @@ object SpecificUnitAutomation {
// ...also get priorities to steal the most valuable for them
val owner = it.getOwner()
if (owner != null)
distance - WorkerAutomation(unit).getPriority(it, owner)
distance - WorkerAutomation.getPriority(it, owner)
else distance
}
.firstOrNull { unit.movement.canReach(it) } // canReach is performance-heavy and always a last resort
@ -93,7 +93,7 @@ object SpecificUnitAutomation {
}
// try to build a citadel for defensive purposes
if (WorkerAutomation(unit).evaluateFortPlacement(unit.currentTile, unit.civInfo, true)) {
if (WorkerAutomation.evaluateFortPlacement(unit.currentTile, unit.civInfo, true)) {
UnitActions.getImprovementConstructionActions(unit, unit.currentTile).firstOrNull()?.action?.invoke()
return
}
@ -113,7 +113,7 @@ object SpecificUnitAutomation {
val potentialTilesNearCity = cityToGarrison.getTilesInDistanceRange(3..4)
val tileForCitadel = potentialTilesNearCity.firstOrNull {
reachableTest(it) &&
WorkerAutomation(unit).evaluateFortPlacement(it, unit.civInfo, true)
WorkerAutomation.evaluateFortPlacement(it, unit.civInfo, true)
}
if (tileForCitadel != null) {
unit.movement.headTowards(tileForCitadel)
@ -246,8 +246,8 @@ object SpecificUnitAutomation {
// if we got here, we're pretty close, start looking!
val chosenTile = applicableTiles.sortedByDescending { Automation.rankTile(it, unit.civInfo) }
.firstOrNull { unit.movement.canReach(it) }
if (chosenTile == null) continue // to another city
.firstOrNull { unit.movement.canReach(it) }
?: continue // to another city
unit.movement.headTowards(chosenTile)
if (unit.currentTile == chosenTile)
@ -280,7 +280,7 @@ object SpecificUnitAutomation {
if (citiesByNearbyAirUnits.keys.any { it != 0 }) {
val citiesWithMostNeedOfAirUnits = citiesByNearbyAirUnits.maxByOrNull { it.key }!!.value
//todo: maybe groupby size and choose highest priority within the same size turns
//todo: maybe group by size and choose highest priority within the same size turns
val chosenCity = citiesWithMostNeedOfAirUnits.minByOrNull { pathsToCities.getValue(it).size }!! // city with min path = least turns to get there
val firstStepInPath = pathsToCities.getValue(chosenCity).first()
unit.movement.moveToTile(firstStepInPath)
@ -312,7 +312,7 @@ object SpecificUnitAutomation {
if (citiesThatCanAttackFrom.isEmpty()) return
//todo: this logic looks similar to some parts of automateFighter, maybe pull out common code
//todo: maybe groupby size and choose highest priority within the same size turns
//todo: maybe group by size and choose highest priority within the same size turns
val closestCityThatCanAttackFrom = citiesThatCanAttackFrom.minByOrNull { pathsToCities[it]!!.size }!!
val firstStepInPath = pathsToCities[closestCityThatCanAttackFrom]!!.first()
airUnit.movement.moveToTile(firstStepInPath)
@ -330,7 +330,7 @@ object SpecificUnitAutomation {
tryRelocateToNearbyAttackableCities(unit)
}
// This really needs to be changed, to have better targetting for missiles
// This really needs to be changed, to have better targeting for missiles
fun automateMissile(unit: MapUnit) {
if (BattleHelper.tryAttackNearbyEnemy(unit)) return
tryRelocateToNearbyAttackableCities(unit)
@ -367,4 +367,4 @@ object SpecificUnitAutomation {
}
return false
}
}
}

View File

@ -10,8 +10,8 @@ import com.unciv.ui.worldscreen.unit.UnitActions
object UnitAutomation {
const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
private const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
private const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
private fun isGoodTileToExplore(unit: MapUnit, tile: TileInfo): Boolean {
return unit.movement.canMoveTo(tile)
@ -29,7 +29,7 @@ object UnitAutomation {
if (explorableTilesThisTurn.any()) {
val bestTile = explorableTilesThisTurn
.sortedByDescending { it.getHeight() } // secondary sort is by 'how far can you see'
.maxByOrNull { it: TileInfo -> it.aerialDistanceTo(unit.currentTile) }!! // primary sort is by 'how far can you go'
.maxByOrNull { it.aerialDistanceTo(unit.currentTile) }!! // primary sort is by 'how far can you go'
unit.movement.headTowards(bestTile)
return true
}
@ -53,9 +53,7 @@ object UnitAutomation {
|| it.improvement == Constants.barbarianEncampment
)
&& unit.movement.canMoveTo(it)
}
if (tileWithRuinOrEncampment == null)
return false
} ?: return false
unit.movement.moveToTile(tileWithRuinOrEncampment)
return true
}
@ -78,7 +76,7 @@ object UnitAutomation {
if (!upgradedUnit.isBuildable(unit.civInfo)) return false // for resource reasons, usually
val upgradeAction = UnitActions.getUpgradeAction(unit)
if (upgradeAction == null) return false
?: return false
upgradeAction.action?.invoke()
return true
@ -93,7 +91,7 @@ object UnitAutomation {
return SpecificUnitAutomation.automateSettlerActions(unit)
if (unit.hasUniqueToBuildImprovements)
return WorkerAutomation(unit).automateWorkerAction()
return WorkerAutomation.automateWorkerAction(unit)
if (unit.hasUnique(Constants.workBoatsUnique))
return SpecificUnitAutomation.automateWorkBoats(unit)
@ -119,7 +117,6 @@ object UnitAutomation {
if (unit.hasUnique("Self-destructs when attacking"))
return SpecificUnitAutomation.automateMissile(unit)
if (tryGoToRuinAndEncampment(unit)) {
if (unit.currentMovement == 0f) return
}
@ -167,14 +164,12 @@ object UnitAutomation {
val encampmentsCloseToCities = knownEncampments.filter { cities.any { city -> city.getCenterTile().aerialDistanceTo(it) < 6 } }
.sortedBy { it.aerialDistanceTo(unit.currentTile) }
val encampmentToHeadTowards = encampmentsCloseToCities.firstOrNull { unit.movement.canReach(it) }
if (encampmentToHeadTowards == null) {
return false
}
?: return false
unit.movement.headTowards(encampmentToHeadTowards)
return true
}
fun tryHealUnit(unit: MapUnit): Boolean {
private fun tryHealUnit(unit: MapUnit): Boolean {
if (unit.baseUnit.isRanged() && unit.hasUnique("Unit will heal every turn, even if it performs an action"))
return false // will heal anyway, and attacks don't hurt
@ -187,17 +182,17 @@ object UnitAutomation {
if (tryPillageImprovement(unit)) return true
val tilesWithRangedEnemyUnits = unit.currentTile.getTilesInDistance(3)
.flatMap { it.getUnits().filter { unit.civInfo.isAtWarWith(it.civInfo) } }
val nearbyRangedEnemyUnits = unit.currentTile.getTilesInDistance(3)
.flatMap { tile -> tile.getUnits().filter { unit.civInfo.isAtWarWith(it.civInfo) } }
val tilesInRangeOfAttack = tilesWithRangedEnemyUnits
val tilesInRangeOfAttack = nearbyRangedEnemyUnits
.flatMap { it.getTile().getTilesInDistance(it.getRange()) }
val tilesWithinBomardmentRange = unit.currentTile.getTilesInDistance(3)
val tilesWithinBombardmentRange = unit.currentTile.getTilesInDistance(3)
.filter { it.isCityCenter() && it.getCity()!!.civInfo.isAtWarWith(unit.civInfo) }
.flatMap { it.getTilesInDistance(it.getCity()!!.range) }
val dangerousTiles = (tilesInRangeOfAttack + tilesWithinBomardmentRange).toHashSet()
val dangerousTiles = (tilesInRangeOfAttack + tilesWithinBombardmentRange).toHashSet()
val viableTilesForHealing = unitDistanceToTiles.keys
@ -233,7 +228,7 @@ object UnitAutomation {
.filter { unit.movement.canMoveTo(it) && UnitActions.canPillage(unit, it) }
if (tilesThatCanWalkToAndThenPillage.isEmpty()) return false
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxByOrNull { it: TileInfo -> it.getDefensiveBonus() }!!
val tileToPillage = tilesThatCanWalkToAndThenPillage.maxByOrNull { it.getDefensiveBonus() }!!
if (unit.getTile() != tileToPillage)
unit.movement.moveToTile(tileToPillage)
@ -285,8 +280,7 @@ object UnitAutomation {
it.isCivilian() &&
(it.hasUnique(Constants.settlerUnique) || unit.isGreatPerson())
&& tile.militaryUnit == null && unit.movement.canMoveTo(tile) && unit.movement.canReach(tile)
}
if (settlerOrGreatPersonToAccompany == null) return false
} ?: return false
unit.movement.headTowards(settlerOrGreatPersonToAccompany.currentTile)
return true
}
@ -385,7 +379,7 @@ object UnitAutomation {
fun tryBombardEnemy(city: CityInfo): Boolean {
if (!city.canBombard()) return false
val enemy = chooseBombardTarget(city)
if (enemy == null) return false
?: return false
Battle.attack(CityCombatant(city), enemy)
return true
}
@ -402,7 +396,7 @@ object UnitAutomation {
.filter { it.isRanged() }
if (rangedUnits.any()) targets = rangedUnits
}
return targets.minByOrNull { it: ICombatant -> it.getHealth() }
return targets.minByOrNull { it.getHealth() }
}
private fun tryTakeBackCapturedCity(unit: MapUnit): Boolean {
@ -448,23 +442,20 @@ object UnitAutomation {
return false
}
val citiesToTry: Sequence<CityInfo>
if (!unit.civInfo.isAtWar()) {
val citiesToTry = if (!unit.civInfo.isAtWar()) {
if (unit.getTile().isCityCenter()) return true // It's always good to have a unit in the city center, so if you haven't found anyone around to attack, forget it.
citiesToTry = citiesWithoutGarrison.asSequence()
citiesWithoutGarrison.asSequence()
} else {
if (unit.getTile().isCityCenter() &&
isCityThatNeedsDefendingInWartime(unit.getTile().getCity()!!)) return true
citiesToTry = citiesWithoutGarrison.asSequence()
citiesWithoutGarrison.asSequence()
.filter { isCityThatNeedsDefendingInWartime(it) }
}
val closestReachableCityNeedsDefending = citiesToTry
.sortedBy { it.getCenterTile().aerialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it.getCenterTile()) }
if (closestReachableCityNeedsDefending == null) return false
.sortedBy { it.getCenterTile().aerialDistanceTo(unit.currentTile) }
.firstOrNull { unit.movement.canReach(it.getCenterTile()) }
?: return false
unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile())
return true
}
@ -489,12 +480,12 @@ object UnitAutomation {
}
val tileFurthestFromEnemy = reachableTiles.keys.filter { unit.movement.canMoveTo(it) }
.maxByOrNull { countDistanceToClosestEnemy(unit, it) }
if (tileFurthestFromEnemy == null) return // can't move anywhere!
?: return // can't move anywhere!
unit.movement.moveToTile(tileFurthestFromEnemy)
}
fun countDistanceToClosestEnemy(unit: MapUnit, tile: TileInfo): Int {
private fun countDistanceToClosestEnemy(unit: MapUnit, tile: TileInfo): Int {
for (i in 1..3)
if (tile.getTilesAtDistance(i).any { containsEnemyMilitaryUnit(unit, it) })
return i
@ -504,4 +495,4 @@ object UnitAutomation {
fun containsEnemyMilitaryUnit(unit: MapUnit, tileInfo: TileInfo) = tileInfo.militaryUnit != null
&& tileInfo.militaryUnit!!.civInfo.isAtWarWith(unit.civInfo)
}
}

View File

@ -1,38 +1,176 @@
package com.unciv.logic.automation
import com.badlogic.gdx.math.Vector2
import com.unciv.Constants
import com.unciv.UncivGame
import com.unciv.logic.HexMath
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.CivilizationInfo
import com.unciv.logic.map.BFS
import com.unciv.logic.map.MapUnit
import com.unciv.logic.map.RoadStatus
import com.unciv.logic.map.TileInfo
import com.unciv.models.ruleset.tile.TileImprovement
class WorkerAutomation(val unit: MapUnit) {
private object WorkerAutomationConst {
/** Controls detailed logging of decisions to the console -Turn off for release builds! */
const val consoleOutput = false
/** 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.
*
* This is instantiated from [CivilizationInfo.getWorkerAutomation] and cached there.
*
* @param civInfo The Civilization - data common to all automated workers is cached once per Civ
* @param cachedForTurn The turn number this was created for - a recreation of the instance is forced on different turn numbers
*/
class WorkerAutomation(
val civInfo: CivilizationInfo,
val cachedForTurn: Int,
cloningSource: WorkerAutomation? = null
) {
///////////////////////////////////////// Cached data /////////////////////////////////////////
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.isPlayerCivilization() && !UncivGame.Current.settings.autoBuildingRoads)
RoadStatus.None
else civInfo.tech.getBestRoadAvailable()
/** Civ-wide list of unconnected Cities, sorted by closest to capital first */
private val citiesThatNeedConnecting: List<CityInfo> by lazy {
val result = civInfo.cities.asSequence()
.filter {
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 (WorkerAutomationConst.consoleOutput) {
println("WorkerAutomation citiesThatNeedConnecting for ${civInfo.civName} turn $cachedForTurn:")
if (result.isEmpty())
println("\tempty")
else result.forEach {
println("\t${it.name}")
}
}
result
}
/** Civ-wide list of _connected_ Cities, unsorted */
private val tilesOfConnectedCities: List<TileInfo> by lazy {
val result = civInfo.cities.asSequence()
.filter { it.isCapital() || it.cityStats.isConnectedToCapital(bestRoadAvailable) }
.map { it.getCenterTile() }
.toList()
if (WorkerAutomationConst.consoleOutput) {
println("WorkerAutomation tilesOfConnectedCities for ${civInfo.civName} turn $cachedForTurn:")
if (result.isEmpty())
println("\tempty")
else result.forEach {
println("\t$it") // ${it.getCity()?.name} included in TileInfo toString()
}
}
result
}
/** 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 TileInfos, we could copy this on cloning
private val bfsCache = HashMap<Vector2, BFS?>()
//todo: UnitMovementAlgorithms.canReach still very expensive and could benefit from caching, it's not using BFS
fun automateWorkerAction() {
///////////////////////////////////////// Helpers /////////////////////////////////////////
companion object {
/** Maps to instance [WorkerAutomation.automateWorkerAction] knowing only the MapUnit */
fun automateWorkerAction(unit: MapUnit) {
unit.civInfo.getWorkerAutomation().automateWorkerAction(unit)
}
/** Convenience shortcut supports old calling syntax for [WorkerAutomation.getPriority] */
fun getPriority(tileInfo: TileInfo, civInfo: CivilizationInfo): Int {
return civInfo.getWorkerAutomation().getPriority(tileInfo)
}
/** Convenience shortcut supports old calling syntax for [WorkerAutomation.evaluateFortPlacement] */
fun evaluateFortPlacement(tile: TileInfo, civInfo: CivilizationInfo, isCitadel: Boolean): Boolean {
return civInfo.getWorkerAutomation().evaluateFortPlacement(tile, isCitadel)
}
/** For console logging only */
private fun MapUnit.label() = toString() + " " + getTile().position.toString()
}
/**
* @return A complete, or partial clone, or null - meaning any missing information should be regenerated
*/
fun clone(): WorkerAutomation {
// This is a tricky one - we'd like to continue using the cached knowledge stored while the turn was
// interactive at the moment nextTurn clones the GameInfo - but would a shallow copy work? No.
// Also, AutoSave pulls another clone of GameInfo where any extra work would be wasted.
//
// Is a deep clone by looking up cloned objects by their primary keys worthwhile?
// Current answer: NO. But we still allow this method to carry the decision.
//
// The following is cheap and does not convert the lazies, forcing a rebuild:
return WorkerAutomation(civInfo, cachedForTurn, this)
}
///////////////////////////////////////// Methods /////////////////////////////////////////
/**
* Automate one Worker - decide what to do and where, move, start or continue work.
*/
fun automateWorkerAction(unit: MapUnit) {
// 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
//todo - stay when we're stacked with a good military unit???
val enemyUnitsInWalkingDistance = unit.movement.getDistanceToTiles().keys
.filter { UnitAutomation.containsEnemyMilitaryUnit(unit, it) }
.filter { UnitAutomation.containsEnemyMilitaryUnit(unit, it) }
if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary()) return UnitAutomation.runAway(unit)
if (enemyUnitsInWalkingDistance.isNotEmpty() && !unit.baseUnit.isMilitary()) {
if (WorkerAutomationConst.consoleOutput)
println("WorkerAutomation: ${unit.label()} -> run away")
return UnitAutomation.runAway(unit)
}
val currentTile = unit.getTile()
val tileToWork = findTileToWork()
if (getPriority(tileToWork, unit.civInfo) < 3) { // building roads is more important
val tileToWork = findTileToWork(unit)
if (getPriority(tileToWork, civInfo) < 3) { // building roads is more important
if (tryConnectingCities(unit)) return
}
if (tileToWork != currentTile) {
if (WorkerAutomationConst.consoleOutput)
println("WorkerAutomation: ${unit.label()} -> head towards $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!
return
}
if (currentTile.improvementInProgress == null && currentTile.isLand
&& tileCanBeImproved(currentTile, unit.civInfo)) {
return currentTile.startWorkingOnImprovement(chooseImprovement(currentTile, unit.civInfo)!!, unit.civInfo)
&& tileCanBeImproved(unit, currentTile)) {
if (WorkerAutomationConst.consoleOutput)
println("WorkerAutomation: ${unit.label()} -> start improving $currentTile")
return currentTile.startWorkingOnImprovement(chooseImprovement(unit, currentTile)!!, civInfo)
}
if (currentTile.improvementInProgress != null) return // we're working!
@ -41,92 +179,111 @@ class WorkerAutomation(val unit: MapUnit) {
val citiesToNumberOfUnimprovedTiles = HashMap<String, Int>()
for (city in unit.civInfo.cities) {
citiesToNumberOfUnimprovedTiles[city.id] = city.getTiles()
.count { it.isLand && it.civilianUnit == null && tileCanBeImproved(it, unit.civInfo) }
.count { it.isLand && it.civilianUnit == null && tileCanBeImproved(unit, it) }
}
val mostUndevelopedCity = unit.civInfo.cities.asSequence()
.filter { citiesToNumberOfUnimprovedTiles[it.id]!! > 0 }
.sortedByDescending { citiesToNumberOfUnimprovedTiles[it.id] }
.firstOrNull { unit.movement.canReach(it.getCenterTile()) } //goto most undeveloped city
.filter { citiesToNumberOfUnimprovedTiles[it.id]!! > 0 }
.sortedByDescending { citiesToNumberOfUnimprovedTiles[it.id] }
.firstOrNull { unit.movement.canReach(it.getCenterTile()) } //goto most undeveloped city
if (mostUndevelopedCity != null && mostUndevelopedCity != unit.currentTile.owningCity) {
if (mostUndevelopedCity != null && mostUndevelopedCity != currentTile.owningCity) {
if (WorkerAutomationConst.consoleOutput)
println("WorkerAutomation: ${unit.label()} -> head towards undeveloped city ${mostUndevelopedCity.name}")
val reachedTile = unit.movement.headTowards(mostUndevelopedCity.getCenterTile())
if (reachedTile != currentTile) unit.doAction() // since we've moved, maybe we can do something here - automate
return
}
unit.civInfo.addNotification("[${unit.displayName()}] has no work to do.", unit.currentTile.position, unit.name, "OtherIcons/Sleep")
if (WorkerAutomationConst.consoleOutput)
println("WorkerAutomation: ${unit.label()} -> nothing to do")
unit.civInfo.addNotification("[${unit.displayName()}] has no work to do.", currentTile.position, unit.name, "OtherIcons/Sleep")
}
private fun tryConnectingCities(unit: MapUnit): Boolean { // returns whether we actually did anything
//Player can choose not to auto-build roads & railroads.
if (unit.civInfo.isPlayerCivilization() && !UncivGame.Current.settings.autoBuildingRoads)
return false
val targetRoad = unit.civInfo.tech.getBestRoadAvailable()
val citiesThatNeedConnecting = unit.civInfo.cities.asSequence()
.filter {
it.population.population > 3 && !it.isCapital() && !it.isBeingRazed //City being razed should not be connected.
&& !it.cityStats.isConnectedToCapital(targetRoad)
// Cities that are too far away make the caReach() calculations devastatingly long
&& it.getCenterTile().aerialDistanceTo(unit.getTile()) < 20
}
if (citiesThatNeedConnecting.none()) return false // do nothing.
val citiesThatNeedConnectingBfs = citiesThatNeedConnecting
.sortedBy { it.getCenterTile().aerialDistanceTo(unit.civInfo.getCapital().getCenterTile()) }
.map { city -> BFS(city.getCenterTile()) { it.isLand && unit.movement.canPassThrough(it) } }
val connectedCities = unit.civInfo.cities
.filter { it.isCapital() || it.cityStats.isConnectedToCapital(targetRoad) }.map { it.getCenterTile() }
/**
* Looks for work connecting cities
* @return whether we actually did anything
*/
private fun tryConnectingCities(unit: MapUnit): Boolean {
if (bestRoadAvailable == RoadStatus.None || citiesThatNeedConnecting.isEmpty()) 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.
for (bfs in citiesThatNeedConnectingBfs) {
while (!bfs.hasEnded()) {
bfs.nextStep()
for (city in connectedCities)
if (bfs.hasReachedTile(city)) { // we have a winner!
val pathToCity = bfs.getPathTo(city).asSequence()
val roadableTiles = pathToCity.filter { it.roadStatus < targetRoad }
val candidateCities = citiesThatNeedConnecting.asSequence().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 = { it: TileInfo -> it.isLand && unit.movement.canPassThrough(it) }
val currentTile = unit.getTile()
val cityTilesToSeek = tilesOfConnectedCities.sortedBy { it.aerialDistanceTo(currentTile) }
for (toConnectCity in candidateCities) {
val toConnectTile = toConnectCity.getCenterTile()
val bfs: BFS = bfsCache[toConnectTile.position] ?:
BFS(toConnectTile, isCandidateTilePredicate).apply {
maxSize = HexMath.getNumberOfTilesInHexagon(
WorkerAutomationConst.maxBfsReachPadding +
tilesOfConnectedCities.map { it.aerialDistanceTo(toConnectTile) }.minOrNull()!!
)
bfsCache[toConnectTile.position] = this@apply
}
while (true) {
for (cityTile in cityTilesToSeek) {
if (bfs.hasReachedTile(cityTile)) { // we have a winner!
val pathToCity = bfs.getPathTo(cityTile)
val roadableTiles = pathToCity.filter { it.roadStatus < bestRoadAvailable }
val tileToConstructRoadOn: TileInfo
if (unit.currentTile in roadableTiles) tileToConstructRoadOn = unit.currentTile
if (currentTile in roadableTiles) tileToConstructRoadOn =
currentTile
else {
val reachableTile = roadableTiles
.sortedBy { it.aerialDistanceTo(unit.getTile()) }
.firstOrNull { unit.movement.canMoveTo(it) && unit.movement.canReach(it) }
if (reachableTile == null) continue
.sortedBy { it.aerialDistanceTo(unit.getTile()) }
.firstOrNull {
unit.movement.canMoveTo(it) && unit.movement.canReach(
it
)
}
?: continue
tileToConstructRoadOn = reachableTile
unit.movement.headTowards(tileToConstructRoadOn)
}
if (unit.currentMovement > 0 && unit.currentTile == tileToConstructRoadOn
&& unit.currentTile.improvementInProgress != targetRoad.name) {
val improvement = targetRoad.improvement(unit.civInfo.gameInfo.ruleSet)!!
tileToConstructRoadOn.startWorkingOnImprovement(improvement, unit.civInfo)
if (unit.currentMovement > 0 && currentTile == tileToConstructRoadOn
&& currentTile.improvementInProgress != bestRoadAvailable.name) {
val improvement = bestRoadAvailable.improvement(ruleSet)!!
tileToConstructRoadOn.startWorkingOnImprovement(improvement, civInfo)
}
if (WorkerAutomationConst.consoleOutput)
println("WorkerAutomation: ${unit.label()} -> connect city ${bfs.startingPoint.getCity()?.name} to ${cityTile.getCity()!!.name} on $tileToConstructRoadOn")
return true
}
}
if (bfs.hasEnded()) break
bfs.nextStep()
}
if (WorkerAutomationConst.consoleOutput)
println("WorkerAutomation: ${unit.label()} -> connect city ${bfs.startingPoint.getCity()?.name} failed at BFS size ${bfs.size()}")
}
return false
}
/**
* Returns the current tile if no tile to work was found
* Looks for a worthwhile tile to improve
* @return The current tile if no tile to work was found
*/
private fun findTileToWork(): TileInfo {
private fun findTileToWork(unit: MapUnit): TileInfo {
val currentTile = unit.getTile()
val workableTiles = currentTile.getTilesInDistance(4)
.filter {
(it.civilianUnit == null || it == currentTile)
&& tileCanBeImproved(it, unit.civInfo)
&& tileCanBeImproved(unit, it)
&& it.getTilesInDistance(2)
.none { it.isCityCenter() && it.getCity()!!.civInfo.isAtWarWith(unit.civInfo) }
.none { tile -> tile.isCityCenter() && tile.getCity()!!.civInfo.isAtWarWith(civInfo) }
}
.sortedByDescending { getPriority(it, unit.civInfo) }
.sortedByDescending { getPriority(it) }
// the tile needs to be actually reachable - more difficult than it seems,
// which is why we DON'T calculate this for every possible tile in the radius,
@ -134,14 +291,18 @@ class WorkerAutomation(val unit: MapUnit) {
val selectedTile = workableTiles.firstOrNull { unit.movement.canReach(it) }
return if (selectedTile != null
&& getPriority(selectedTile, unit.civInfo) > 1
&& getPriority(selectedTile) > 1
&& (!workableTiles.contains(currentTile)
|| getPriority(selectedTile, unit.civInfo) > getPriority(currentTile, unit.civInfo)))
|| getPriority(selectedTile) > getPriority(currentTile)))
selectedTile
else currentTile
}
private fun tileCanBeImproved(tile: TileInfo, civInfo: CivilizationInfo): Boolean {
/**
* Tests if tile can be improved by a specific unit, or if no unit is passed, any unit at all
* (but does not check whether the ruleset contains any unit capable of it)
*/
private fun tileCanBeImproved(unit: MapUnit?, tile: TileInfo): Boolean {
if (!tile.isLand || tile.isImpassible() || tile.isCityCenter())
return false
val city = tile.getCity()
@ -151,18 +312,22 @@ class WorkerAutomation(val unit: MapUnit) {
return false
if (tile.improvement == null) {
if (unit == null) return true
if (tile.improvementInProgress != null && unit.canBuildImprovement(tile.getTileImprovementInProgress()!!, tile)) return true
val chosenImprovement = chooseImprovement(tile, civInfo)
val chosenImprovement = chooseImprovement(unit, tile)
if (chosenImprovement != null && tile.canBuildImprovement(chosenImprovement, civInfo) && unit.canBuildImprovement(chosenImprovement, tile)) return true
} else if (!tile.containsGreatImprovement() && tile.hasViewableResource(civInfo)
&& tile.getTileResource().improvement != tile.improvement
&& chooseImprovement(tile, civInfo) // if the chosen improvement is not null and buildable
.let { it != null && tile.canBuildImprovement(it, civInfo) && unit.canBuildImprovement(it, tile)})
&& tile.getTileResource().improvement != tile.improvement
&& (unit == null || chooseImprovement(unit, tile) // if the chosen improvement is not null and buildable
.let { it != null && tile.canBuildImprovement(it, civInfo) && unit.canBuildImprovement(it, tile)}))
return true
return false // couldn't find anything to construct here
}
fun getPriority(tileInfo: TileInfo, civInfo: CivilizationInfo): Int {
/**
* Calculate a priority for improving a tile
*/
private fun getPriority(tileInfo: TileInfo): Int {
var priority = 0
if (tileInfo.getOwner() == civInfo) {
priority += 2
@ -176,77 +341,97 @@ class WorkerAutomation(val unit: MapUnit) {
return priority
}
private fun chooseImprovement(tile: TileInfo, civInfo: CivilizationInfo): TileImprovement? {
/**
* Determine the improvement appropriate to a given tile and worker
*/
private fun chooseImprovement(unit: MapUnit, tile: TileInfo): TileImprovement? {
val improvementStringForResource: String? = when {
tile.resource == null || !tile.hasViewableResource(civInfo) -> null
tile.terrainFeatures.contains(Constants.marsh) && !isImprovementOnFeatureAllowed(tile, civInfo) -> "Remove Marsh"
tile.terrainFeatures.contains("Fallout") && !isImprovementOnFeatureAllowed(tile, civInfo) -> "Remove Fallout" // for really mad modders
tile.terrainFeatures.contains(Constants.jungle) && !isImprovementOnFeatureAllowed(tile, civInfo) -> "Remove Jungle"
tile.terrainFeatures.contains(Constants.forest) && !isImprovementOnFeatureAllowed(tile, civInfo) -> "Remove Forest"
tile.terrainFeatures.contains(Constants.marsh) && !isImprovementOnFeatureAllowed(tile) -> "Remove Marsh"
tile.terrainFeatures.contains("Fallout") && !isImprovementOnFeatureAllowed(tile) -> "Remove Fallout" // for really mad modders
tile.terrainFeatures.contains(Constants.jungle) && !isImprovementOnFeatureAllowed(tile) -> "Remove Jungle"
tile.terrainFeatures.contains(Constants.forest) && !isImprovementOnFeatureAllowed(tile) -> "Remove Forest"
else -> tile.getTileResource().improvement
}
// turnsToBuild is what defines them as buildable
val tileImprovements = civInfo.gameInfo.ruleSet.tileImprovements.filter { it.value.turnsToBuild != 0 }
val tileImprovements = ruleSet.tileImprovements.filter { it.value.turnsToBuild != 0 }
val uniqueImprovement = tileImprovements.values
.firstOrNull { it.uniqueTo == civInfo.civName }
.firstOrNull { it.uniqueTo == civInfo.civName }
val improvementString = when {
tile.improvementInProgress != null -> tile.improvementInProgress
tile.improvementInProgress != null -> tile.improvementInProgress!!
improvementStringForResource != null && tileImprovements.containsKey(improvementStringForResource)
&& tileImprovements[improvementStringForResource]!!.turnsToBuild != 0 -> improvementStringForResource
tile.containsGreatImprovement() -> null
tile.containsUnfinishedGreatImprovement() -> null
tile.containsGreatImprovement() -> return null
tile.containsUnfinishedGreatImprovement() -> return null
// Defence is more important that civilian improvements
// While AI sucks in strategical placement of forts, allow a human does it manually
!civInfo.isPlayerCivilization() && evaluateFortPlacement(tile, civInfo, false) -> Constants.fort
// I think we can assume that the unique improvement is better
uniqueImprovement != null && tile.canBuildImprovement(uniqueImprovement, civInfo)
&& unit.canBuildImprovement(uniqueImprovement, tile) ->
uniqueImprovement.name
uniqueImprovement != null && tile.canBuildImprovement(uniqueImprovement, civInfo)
&& unit.canBuildImprovement(uniqueImprovement, tile) ->
uniqueImprovement.name
tile.terrainFeatures.contains("Fallout") -> "Remove Fallout"
tile.terrainFeatures.contains(Constants.marsh) -> "Remove Marsh"
tile.terrainFeatures.contains(Constants.jungle) -> Constants.tradingPost
tile.terrainFeatures.contains("Oasis") -> null
tile.terrainFeatures.contains("Oasis") -> return null
tile.terrainFeatures.contains(Constants.forest) -> "Lumber mill"
tile.isHill() -> "Mine"
tile.baseTerrain in listOf(Constants.grassland, Constants.desert, Constants.plains) -> "Farm"
tile.isAdjacentToFreshwater -> "Farm"
tile.baseTerrain in listOf(Constants.tundra, Constants.snow) -> Constants.tradingPost
else -> null
else -> return null
}
if (improvementString == null) return null
return unit.civInfo.gameInfo.ruleSet.tileImprovements[improvementString] // For mods, the tile improvement may not exist, so don't assume.
return ruleSet.tileImprovements[improvementString] // For mods, the tile improvement may not exist, so don't assume.
}
private fun isImprovementOnFeatureAllowed(tile: TileInfo, civInfo: CivilizationInfo): Boolean {
// routine assumes the caller ensured that terrainFeature and resource are both present
/**
* Checks whether the improvement matching the tile resource requires any terrain feature to be removed first.
*
* Assumes the caller ensured that terrainFeature and resource are both present!
*/
private fun isImprovementOnFeatureAllowed(tile: TileInfo): Boolean {
val resourceImprovementName = tile.getTileResource().improvement
?: return false
val resourceImprovement = civInfo.gameInfo.ruleSet.tileImprovements[resourceImprovementName]
?: return false
?: return false
val resourceImprovement = ruleSet.tileImprovements[resourceImprovementName]
?: return false
return tile.terrainFeatures.any { resourceImprovement.isAllowedOnFeature(it) }
}
private fun isAcceptableTileForFort(tile: TileInfo, civInfo: CivilizationInfo): Boolean {
/**
* Checks whether a given tile allows a Fort and whether a Fort may be undesirable (without checking surroundings).
*
* -> Checks: city, already built, resource, great improvements.
* Used only in [evaluateFortPlacement].
*/
private fun isAcceptableTileForFort(tile: TileInfo): Boolean {
//todo Should this not also check impassable and the fort improvement's terrainsCanBeBuiltOn/uniques?
if (tile.isCityCenter() // don't build fort in the city
|| !tile.isLand // don't build fort in the water
|| tile.improvement == Constants.fort // don't build fort if it is already here
|| tile.hasViewableResource(civInfo) // don't build on resource tiles
|| tile.containsGreatImprovement() // don't build on great improvements (including citadel)
|| tile.containsUnfinishedGreatImprovement()) return false
|| !tile.isLand // don't build fort in the water
|| tile.improvement == Constants.fort // don't build fort if it is already here
|| tile.hasViewableResource(civInfo) // don't build on resource tiles
|| tile.containsGreatImprovement() // don't build on great improvements (including citadel)
|| tile.containsUnfinishedGreatImprovement()) return false
return true
}
fun evaluateFortPlacement(tile: TileInfo, civInfo: CivilizationInfo, isCitadel: Boolean): Boolean {
/**
* Do we want a Fort [here][tile] considering surroundings?
* @param isCitadel Controls within borders check - true also allows 1 tile outside borders
* @return Yes please build a Fort here
*/
private fun evaluateFortPlacement(tile: TileInfo, isCitadel: Boolean): Boolean {
//todo Is the Citadel code dead anyway? If not - why does the nearestTiles check not respect the param?
// build on our land only
if ((tile.owningCity?.civInfo != civInfo &&
// except citadel which can be built near-by
(!isCitadel || tile.neighbors.all { it.getOwner() != civInfo })) ||
!isAcceptableTileForFort(tile, civInfo)) return false
// except citadel which can be built near-by
(!isCitadel || tile.neighbors.all { it.getOwner() != civInfo })) ||
!isAcceptableTileForFort(tile)) return false
// if this place is not perfect, let's see if there is a better one
val nearestTiles = tile.getTilesInDistance(2).filter { it.owningCity?.civInfo == civInfo }.toList()
@ -255,15 +440,15 @@ class WorkerAutomation(val unit: MapUnit) {
if (closeTile.isCityCenter()) return false
// don't build forts too close to other forts
if (closeTile.improvement != null
&& closeTile.getTileImprovement()!!.uniqueObjects.any { it.placeholderText == "Gives a defensive bonus of []%" }
|| closeTile.improvementInProgress != Constants.fort) return false
&& closeTile.getTileImprovement()!!.uniqueObjects.any { it.placeholderText == "Gives a defensive bonus of []%" }
|| closeTile.improvementInProgress != Constants.fort) return false
// there is another better tile for the fort
if (!tile.isHill() && closeTile.isHill() &&
isAcceptableTileForFort(closeTile, civInfo)) return false
isAcceptableTileForFort(closeTile)) return false
}
val enemyCivs = civInfo.getKnownCivs()
.filterNot { it == civInfo || it.cities.isEmpty() || !civInfo.getDiplomacyManager(it).canAttack() }
.filterNot { it == civInfo || it.cities.isEmpty() || !civInfo.getDiplomacyManager(it).canAttack() }
// no potential enemies
if (enemyCivs.isEmpty()) return false
@ -302,4 +487,4 @@ class WorkerAutomation(val unit: MapUnit) {
return distanceBetweenCities + 2 > distanceToEnemy + distanceToOurCity
}
}
}

View File

@ -5,6 +5,7 @@ import com.unciv.UncivGame
import com.unciv.logic.GameInfo
import com.unciv.logic.UncivShowableException
import com.unciv.logic.automation.NextTurnAutomation
import com.unciv.logic.automation.WorkerAutomation
import com.unciv.logic.city.CityInfo
import com.unciv.logic.civilization.RuinsManager.RuinsManager
import com.unciv.logic.civilization.diplomacy.DiplomacyFlags
@ -35,6 +36,18 @@ import kotlin.math.roundToInt
class CivilizationInfo {
@Transient
private var workerAutomationCache: WorkerAutomation? = null
/** Returns an instance of WorkerAutomation valid for the duration of the current turn
* This instance carries cached data common for all Workers of this civ */
fun getWorkerAutomation(): WorkerAutomation {
val currentTurn = if (UncivGame.Current.isInitialized && UncivGame.Current.isGameInfoInitialized())
UncivGame.Current.gameInfo.turns else 0
if (workerAutomationCache == null || workerAutomationCache!!.cachedForTurn != currentTurn)
workerAutomationCache = workerAutomationCache?.clone() ?: WorkerAutomation(this, currentTurn)
return workerAutomationCache!!
}
@Transient
lateinit var gameInfo: GameInfo
@ -156,6 +169,7 @@ class CivilizationInfo {
toReturn.flagsCountdown.putAll(flagsCountdown)
toReturn.temporaryUniques.addAll(temporaryUniques)
toReturn.hasEverOwnedOriginalCapital = hasEverOwnedOriginalCapital
toReturn.workerAutomationCache = workerAutomationCache?.clone()
return toReturn
}
@ -563,7 +577,7 @@ class CivilizationInfo {
updateViewableTiles() // adds explored tiles so that the units will be able to perform automated actions better
transients().updateCitiesConnectedToCapital()
startTurnFlags()
for (city in cities) city.startTurn()
for (city in cities) city.startTurn() // Most expensive part of startTurn
for (unit in getCivUnits()) unit.startTurn()
@ -622,7 +636,7 @@ class CivilizationInfo {
}
goldenAges.endTurn(getHappiness())
getCivUnits().forEach { it.endTurn() }
getCivUnits().forEach { it.endTurn() } // This is the most expensive part of endTurn
diplomacy.values.toList().forEach { it.nextTurn() } // we copy the diplomacy values so if it changes in-loop we won't crash
updateAllyCivForCityState()
updateHasActiveGreatWall()

View File

@ -9,6 +9,9 @@ class BFS(
val startingPoint: TileInfo,
private val predicate : (TileInfo) -> Boolean
) {
/** Maximum number of tiles to search */
var maxSize = Int.MAX_VALUE
/** remaining tiles to check */
private val tilesToCheck = ArrayDeque<TileInfo>(37) // needs resize at distance 4
@ -44,6 +47,7 @@ class BFS(
* Will do nothing when [hasEnded] returns `true`
*/
fun nextStep() {
if (tilesReached.size >= maxSize) { tilesToCheck.clear(); return }
val current = tilesToCheck.removeFirstOrNull() ?: return
for (neighbor in current.neighbors) {
if (neighbor !in tilesReached && predicate(neighbor)) {

View File

@ -484,7 +484,7 @@ class MapUnit {
return
}
if (isAutomated()) WorkerAutomation(this).automateWorkerAction()
if (isAutomated()) WorkerAutomation.automateWorkerAction(this)
if (isExploring()) UnitAutomation.automatedExplore(this)
}

View File

@ -42,7 +42,7 @@ import kotlin.concurrent.thread
* app lifetime - and we do dispose them when the app is disposed.
*/
object Sounds {
private const val debugMessages = true
private const val debugMessages = false
@Suppress("EnumEntryName")
private enum class SupportedExtensions { mp3, ogg, wav } // Per Gdx docs, no aac/m4a

View File

@ -32,6 +32,7 @@ import com.unciv.ui.saves.LoadGameScreen
import com.unciv.ui.saves.SaveGameScreen
import com.unciv.ui.trade.DiplomacyScreen
import com.unciv.ui.utils.*
import com.unciv.ui.utils.UncivDateFormat.formatDate
import com.unciv.ui.victoryscreen.VictoryScreen
import com.unciv.ui.worldscreen.bottombar.BattleTable
import com.unciv.ui.worldscreen.bottombar.TileInfoTable
@ -83,6 +84,13 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam
private val notificationsScroll: NotificationsScroll
var shouldUpdate = false
companion object {
/** Switch for console logging of next turn duration */
private const val consoleLog = false
// this object must not be created multiple times
private var multiPlayerRefresher: Timer? = null
}
init {
topBar.setPosition(0f, stage.height - topBar.height)
@ -585,8 +593,12 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam
thread(name = "NextTurn") { // on a separate thread so the user can explore their world while we're passing the turn
if (consoleLog)
println("\nNext turn starting " + Date().formatDate())
val startTime = System.currentTimeMillis()
val gameInfoClone = gameInfo.clone()
gameInfoClone.setTransients()
gameInfoClone.setTransients() // this can get expensive on large games, not the clone itself
try {
gameInfoClone.nextTurn()
@ -611,6 +623,8 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam
}
game.gameInfo = gameInfoClone
if (consoleLog)
println("Next turn took ${System.currentTimeMillis()-startTime}ms")
val shouldAutoSave = gameInfoClone.turns % game.settings.turnsBetweenAutosaves == 0
@ -782,10 +796,4 @@ class WorldScreen(val gameInfo: GameInfo, val viewingCiv:CivilizationInfo) : Cam
ExitGamePopup(this, true)
}
companion object {
// this object must not be created multiple times
private var multiPlayerRefresher: Timer? = null
}
}

View File

@ -378,7 +378,7 @@ object UnitActions {
isCurrentAction = unit.isAutomated(),
action = {
unit.action = UnitActionType.Automate.value
WorkerAutomation(unit).automateWorkerAction()
WorkerAutomation.automateWorkerAction(unit)
}.takeIf { unit.currentMovement > 0 }
)
}