diff --git a/core/src/com/unciv/logic/GameInfo.kt b/core/src/com/unciv/logic/GameInfo.kt index 8a49f15b3b..5d916696ee 100644 --- a/core/src/com/unciv/logic/GameInfo.kt +++ b/core/src/com/unciv/logic/GameInfo.kt @@ -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 } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt index f4e1df6234..efb1c896fc 100644 --- a/core/src/com/unciv/logic/automation/NextTurnAutomation.kt +++ b/core/src/com/unciv/logic/automation/NextTurnAutomation.kt @@ -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()) { diff --git a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt index b9636d98ee..f5d975ad2f 100644 --- a/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/SpecificUnitAutomation.kt @@ -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 } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/automation/UnitAutomation.kt b/core/src/com/unciv/logic/automation/UnitAutomation.kt index 4e4e98d761..8cd96d70ad 100644 --- a/core/src/com/unciv/logic/automation/UnitAutomation.kt +++ b/core/src/com/unciv/logic/automation/UnitAutomation.kt @@ -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 - - 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) -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/automation/WorkerAutomation.kt b/core/src/com/unciv/logic/automation/WorkerAutomation.kt index 7b5f348e75..820f198fbb 100644 --- a/core/src/com/unciv/logic/automation/WorkerAutomation.kt +++ b/core/src/com/unciv/logic/automation/WorkerAutomation.kt @@ -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 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 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() + + //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() 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 } -} \ No newline at end of file +} diff --git a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt index fb87816bfe..3e9910b1a8 100644 --- a/core/src/com/unciv/logic/civilization/CivilizationInfo.kt +++ b/core/src/com/unciv/logic/civilization/CivilizationInfo.kt @@ -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() diff --git a/core/src/com/unciv/logic/map/BFS.kt b/core/src/com/unciv/logic/map/BFS.kt index 161233fb87..f7d1d9bb61 100644 --- a/core/src/com/unciv/logic/map/BFS.kt +++ b/core/src/com/unciv/logic/map/BFS.kt @@ -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(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)) { diff --git a/core/src/com/unciv/logic/map/MapUnit.kt b/core/src/com/unciv/logic/map/MapUnit.kt index a119813ef2..ffb8ff7740 100644 --- a/core/src/com/unciv/logic/map/MapUnit.kt +++ b/core/src/com/unciv/logic/map/MapUnit.kt @@ -484,7 +484,7 @@ class MapUnit { return } - if (isAutomated()) WorkerAutomation(this).automateWorkerAction() + if (isAutomated()) WorkerAutomation.automateWorkerAction(this) if (isExploring()) UnitAutomation.automatedExplore(this) } diff --git a/core/src/com/unciv/ui/utils/Sounds.kt b/core/src/com/unciv/ui/utils/Sounds.kt index 80b7608756..0feb92d3fd 100644 --- a/core/src/com/unciv/ui/utils/Sounds.kt +++ b/core/src/com/unciv/ui/utils/Sounds.kt @@ -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 diff --git a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt index 27a46b07d8..488b6e56bb 100644 --- a/core/src/com/unciv/ui/worldscreen/WorldScreen.kt +++ b/core/src/com/unciv/ui/worldscreen/WorldScreen.kt @@ -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 - } } diff --git a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt index df989f4e1b..5ec96070dc 100644 --- a/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt +++ b/core/src/com/unciv/ui/worldscreen/unit/UnitActions.kt @@ -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 } ) }