mirror of
https://github.com/yairm210/Unciv.git
synced 2025-02-20 19:56:51 +01:00
WorkerAutomation cached per Civ - BFS cached (#4868)
* WorkerAutomation cached per Civ - BFS cached * WorkerAutomation cached per Civ - more linting
This commit is contained in:
parent
d856efac06
commit
970259a0ea
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ class MapUnit {
|
|||
return
|
||||
}
|
||||
|
||||
if (isAutomated()) WorkerAutomation(this).automateWorkerAction()
|
||||
if (isAutomated()) WorkerAutomation.automateWorkerAction(this)
|
||||
|
||||
if (isExploring()) UnitAutomation.automatedExplore(this)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user