chore(purity)

This commit is contained in:
yairm210 2025-08-15 13:25:05 +03:00
parent a4a573a945
commit 8e5b36984c
12 changed files with 37 additions and 4 deletions

View File

@ -67,6 +67,7 @@ allprojects {
"kotlin.collections.random", "kotlin.collections.random",
"kotlin.hashCode", "kotlin.hashCode",
"kotlin.collections.shuffled",
) )
wellKnownPureClasses = setOf( wellKnownPureClasses = setOf(
"java.lang.StackTraceElement" // moved "java.lang.StackTraceElement" // moved

View File

@ -9,6 +9,8 @@ import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly
object AirUnitAutomation { object AirUnitAutomation {
@ -29,7 +31,7 @@ object AirUnitAutomation {
if (friendlyUnusedFighterCount < enemyFighters) return if (friendlyUnusedFighterCount < enemyFighters) return
if (friendlyUsedFighterCount <= enemyFighters) { if (friendlyUsedFighterCount <= enemyFighters) {
fun airSweepDamagePercentBonus(): Int { @Readonly fun airSweepDamagePercentBonus(): Int {
return unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep) return unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep)
.sumOf { it.params[0].toInt() } .sumOf { it.params[0].toInt() }
} }
@ -149,6 +151,7 @@ object AirUnitAutomation {
* Ranks the tile to nuke based off of all tiles in it's blast radius * Ranks the tile to nuke based off of all tiles in it's blast radius
* By default the value is -500 to prevent inefficient nuking. * By default the value is -500 to prevent inefficient nuking.
*/ */
@Readonly
private fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int { private fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int {
val civ = nuke.civ val civ = nuke.civ
if (!Nuke.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE if (!Nuke.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE
@ -166,6 +169,7 @@ object AirUnitAutomation {
var explosionValue = -500 var explosionValue = -500
// Returns either ourValue or thierValue depending on if the input Civ matches the Nuke's Civ // Returns either ourValue or thierValue depending on if the input Civ matches the Nuke's Civ
@Pure
fun evaluateCivValue(targetCiv: Civilization, ourValue: Int, theirValue: Int): Int { fun evaluateCivValue(targetCiv: Civilization, ourValue: Int, theirValue: Int): Int {
if (targetCiv == civ) // We are nuking something that we own! if (targetCiv == civ) // We are nuking something that we own!
return ourValue return ourValue

View File

@ -11,9 +11,11 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionModifiers import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionModifiers
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionModifiers.canUse import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionModifiers.canUse
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import yairm210.purity.annotations.Readonly
object CivilianUnitAutomation { object CivilianUnitAutomation {
@Readonly
fun shouldClearTileForAddInCapitalUnits(unit: MapUnit, tile: Tile) = fun shouldClearTileForAddInCapitalUnits(unit: MapUnit, tile: Tile) =
tile.isCityCenter() && tile.getCity()!!.isCapital() tile.isCityCenter() && tile.getCity()!!.isCapital()
&& !unit.hasUnique(UniqueType.AddInCapital) && !unit.hasUnique(UniqueType.AddInCapital)
@ -23,6 +25,7 @@ object CivilianUnitAutomation {
// To allow "found city" actions that can only trigger a limited number of times // To allow "found city" actions that can only trigger a limited number of times
// Slightly modified getUsableUnitActionUniques() to allow for settlers with *conditional* settling uniques // Slightly modified getUsableUnitActionUniques() to allow for settlers with *conditional* settling uniques
@Readonly
fun hasSettlerAction(uniqueType: UniqueType) = fun hasSettlerAction(uniqueType: UniqueType) =
unit.getMatchingUniques(uniqueType, GameContext.IgnoreConditionals) unit.getMatchingUniques(uniqueType, GameContext.IgnoreConditionals)
.filter { unique -> !unique.hasModifier(UniqueType.UnitActionExtraLimitedTimes) } .filter { unique -> !unique.hasModifier(UniqueType.UnitActionExtraLimitedTimes) }
@ -172,6 +175,7 @@ object CivilianUnitAutomation {
return // The AI doesn't know how to handle unknown civilian units return // The AI doesn't know how to handle unknown civilian units
} }
@Readonly
private fun isLateGame(civ: Civilization): Boolean { private fun isLateGame(civ: Civilization): Boolean {
val researchCompletePercent = val researchCompletePercent =
(civ.tech.researchedTechnologies.size * 1.0f) / civ.gameInfo.ruleset.technologies.size (civ.tech.researchedTechnologies.size * 1.0f) / civ.gameInfo.ruleset.technologies.size

View File

@ -8,6 +8,7 @@ import com.unciv.logic.city.City
import com.unciv.logic.map.mapunit.MapUnit import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.logic.map.mapunit.movement.PathsToTilesWithinTurn import com.unciv.logic.map.mapunit.movement.PathsToTilesWithinTurn
import com.unciv.logic.map.tile.Tile import com.unciv.logic.map.tile.Tile
import yairm210.purity.annotations.Readonly
object HeadTowardsEnemyCityAutomation { object HeadTowardsEnemyCityAutomation {
@ -28,6 +29,7 @@ object HeadTowardsEnemyCityAutomation {
) )
} }
@Readonly
fun getEnemyCitiesByPriority(unit: MapUnit): Sequence<City> { fun getEnemyCitiesByPriority(unit: MapUnit): Sequence<City> {
val enemies = unit.civ.getKnownCivs() val enemies = unit.civ.getKnownCivs()
.filter { unit.civ.isAtWarWith(it) && it.cities.isNotEmpty() } .filter { unit.civ.isAtWarWith(it) && it.cities.isNotEmpty() }
@ -94,6 +96,7 @@ object HeadTowardsEnemyCityAutomation {
} }
/** Cannot take within 5 turns */ /** Cannot take within 5 turns */
@Readonly
private fun cannotTakeCitySoon( private fun cannotTakeCitySoon(
ourUnitsAroundEnemyCity: Sequence<MapUnit>, ourUnitsAroundEnemyCity: Sequence<MapUnit>,
city: City city: City

View File

@ -24,12 +24,14 @@ import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
import com.unciv.utils.randomWeighted import com.unciv.utils.randomWeighted
import yairm210.purity.annotations.Readonly
object UnitAutomation { object UnitAutomation {
private const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5 private const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
private const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f private const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
@Readonly
private fun isGoodTileToExplore(unit: MapUnit, tile: Tile): Boolean { private fun isGoodTileToExplore(unit: MapUnit, tile: Tile): Boolean {
return (tile.getOwner() == null || !tile.getOwner()!!.isCityState) return (tile.getOwner() == null || !tile.getOwner()!!.isCityState)
&& tile.getTilesInDistance(unit.getVisibilityRange()).any { !unit.civ.hasExplored(it) } && tile.getTilesInDistance(unit.getVisibilityRange()).any { !unit.civ.hasExplored(it) }
@ -103,6 +105,7 @@ object UnitAutomation {
return false return false
} }
@Readonly
private fun isGoodTileForFogBusting(unit: MapUnit, tile: Tile): Boolean { private fun isGoodTileForFogBusting(unit: MapUnit, tile: Tile): Boolean {
return unit.movement.canMoveTo(tile) return unit.movement.canMoveTo(tile)
&& tile.getOwner() == null && tile.getOwner() == null
@ -153,8 +156,10 @@ object UnitAutomation {
/** Get the base unit this map unit could upgrade to, respecting researched tech and nation uniques only. /** Get the base unit this map unit could upgrade to, respecting researched tech and nation uniques only.
* Note that if the unit can't upgrade, the current BaseUnit is returned. * Note that if the unit can't upgrade, the current BaseUnit is returned.
*/ */
@Readonly
private fun getUnitsToUpgradeTo(unit: MapUnit): Sequence<BaseUnit> { private fun getUnitsToUpgradeTo(unit: MapUnit): Sequence<BaseUnit> {
@Readonly
fun isInvalidUpgradeDestination(baseUnit: BaseUnit): Boolean { fun isInvalidUpgradeDestination(baseUnit: BaseUnit): Boolean {
if (!unit.civ.tech.isResearched(baseUnit)) if (!unit.civ.tech.isResearched(baseUnit))
return true return true
@ -423,6 +428,7 @@ object UnitAutomation {
/** /**
* @return true if the tile is safe and the unit can heal to full within [turns] * @return true if the tile is safe and the unit can heal to full within [turns]
*/ */
@Readonly
private fun canUnitHealInTurnsOnCurrentTile(unit: MapUnit, turns: Int, noEnemyDistance: Int = 3): Boolean { private fun canUnitHealInTurnsOnCurrentTile(unit: MapUnit, turns: Int, noEnemyDistance: Int = 3): Boolean {
if (unit.hasUnique(UniqueType.HealsEvenAfterAction)) return false // We can keep on moving if (unit.hasUnique(UniqueType.HealsEvenAfterAction)) return false // We can keep on moving
// Check if we are not in a safe city and there is an enemy nearby this isn't a good tile to heal on // Check if we are not in a safe city and there is an enemy nearby this isn't a good tile to heal on
@ -433,6 +439,7 @@ object UnitAutomation {
return healthRequiredPerTurn <= unit.rankTileForHealing(unit.getTile()) return healthRequiredPerTurn <= unit.rankTileForHealing(unit.getTile())
} }
@Readonly
private fun getDangerousTiles(unit: MapUnit): HashSet<Tile> { private fun getDangerousTiles(unit: MapUnit): HashSet<Tile> {
val nearbyEnemyUnits = unit.currentTile.getTilesInDistance(3) val nearbyEnemyUnits = unit.currentTile.getTilesInDistance(3)
.flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } } .flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } }
@ -517,6 +524,7 @@ object UnitAutomation {
private fun tryPrepare(unit: MapUnit): Boolean { private fun tryPrepare(unit: MapUnit): Boolean {
val civInfo = unit.civ val civInfo = unit.civ
@Readonly
fun hasPreparationFlag(targetCiv: Civilization): Boolean { fun hasPreparationFlag(targetCiv: Civilization): Boolean {
val diploManager = civInfo.getDiplomacyManager(targetCiv)!! val diploManager = civInfo.getDiplomacyManager(targetCiv)!!
if (diploManager.hasFlag(DiplomacyFlags.Denunciation) if (diploManager.hasFlag(DiplomacyFlags.Denunciation)
@ -685,7 +693,7 @@ object UnitAutomation {
unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile()) unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile())
return true return true
} }
@Readonly
private fun isCityThatNeedsDefendingInWartime(city: City): Boolean { private fun isCityThatNeedsDefendingInWartime(city: City): Boolean {
if (city.health < city.getMaxHealth()) return true // this city is under attack! if (city.health < city.getMaxHealth()) return true // this city is under attack!
for (enemyCivCity in city.civ.diplomacy.values for (enemyCivCity in city.civ.diplomacy.values
@ -696,7 +704,7 @@ object UnitAutomation {
} }
private fun tryStationingMeleeNavalUnit(unit: MapUnit): Boolean { private fun tryStationingMeleeNavalUnit(unit: MapUnit): Boolean {
fun isMeleeNaval(mapUnit: MapUnit) = mapUnit.baseUnit.isMelee() && mapUnit.type.isWaterUnit() @Readonly fun isMeleeNaval(mapUnit: MapUnit) = mapUnit.baseUnit.isMelee() && mapUnit.type.isWaterUnit()
if (!isMeleeNaval(unit)) return false if (!isMeleeNaval(unit)) return false
val closeCity = unit.getTile().getTilesInDistance(3) val closeCity = unit.getTile().getTilesInDistance(3)

View File

@ -25,6 +25,7 @@ import com.unciv.models.stats.SubStat
import com.unciv.ui.components.UnitMovementMemoryType import com.unciv.ui.components.UnitMovementMemoryType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsPillage
import com.unciv.utils.debug import com.unciv.utils.debug
import yairm210.purity.annotations.Readonly
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -678,7 +679,8 @@ object Battle {
city.isBeingRazed = true city.isBeingRazed = true
} }
} }
@Readonly
fun getMapCombatantOfTile(tile: Tile): ICombatant? { fun getMapCombatantOfTile(tile: Tile): ICombatant? {
if (tile.isCityCenter()) return CityCombatant(tile.getCity()!!) if (tile.isCityCenter()) return CityCombatant(tile.getCity()!!)
if (tile.militaryUnit != null) return MapUnitCombatant(tile.militaryUnit!!) if (tile.militaryUnit != null) return MapUnitCombatant(tile.militaryUnit!!)

View File

@ -15,6 +15,7 @@ import com.unciv.logic.map.tile.Tile
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.worldscreen.bottombar.BattleTable import com.unciv.ui.screens.worldscreen.bottombar.BattleTable
import yairm210.purity.annotations.Readonly
import kotlin.math.ulp import kotlin.math.ulp
import kotlin.random.Random import kotlin.random.Random
@ -28,6 +29,7 @@ object Nuke {
* *
* Both [BattleTable.simulateNuke] and [AirUnitAutomation.automateNukes] check range, so that check is omitted here. * Both [BattleTable.simulateNuke] and [AirUnitAutomation.automateNukes] check range, so that check is omitted here.
*/ */
@Readonly
fun mayUseNuke(nuke: MapUnitCombatant, targetTile: Tile): Boolean { fun mayUseNuke(nuke: MapUnitCombatant, targetTile: Tile): Boolean {
val attackerCiv = nuke.getCivInfo() val attackerCiv = nuke.getCivInfo()
val launchTile = nuke.getTile() val launchTile = nuke.getTile()

View File

@ -30,6 +30,7 @@ class ThreatManager(val civInfo: Civilization) {
* The result value is cached and since it is called each turn in NextTurnAutomation.getUnitPriority * The result value is cached and since it is called each turn in NextTurnAutomation.getUnitPriority
* each subsequent calls are likely to be free. * each subsequent calls are likely to be free.
*/ */
@Readonly @Suppress("purity") //tilesWithEnemies is implicit cache
fun getDistanceToClosestEnemyUnit(tile: Tile, maxDist: Int, takeLargerValues: Boolean = true): Int { fun getDistanceToClosestEnemyUnit(tile: Tile, maxDist: Int, takeLargerValues: Boolean = true): Int {
val tileData = distanceToClosestEnemyTiles[tile] val tileData = distanceToClosestEnemyTiles[tile]
// Needs to be a high value, but not the max value so we can still add to it. Example: nextTurnAutomation sorting // Needs to be a high value, but not the max value so we can still add to it. Example: nextTurnAutomation sorting

View File

@ -12,6 +12,7 @@ import com.unciv.models.ruleset.tile.TileResource
import com.unciv.models.ruleset.unique.GameContext import com.unciv.models.ruleset.unique.GameContext
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.utils.randomWeighted import com.unciv.utils.randomWeighted
import yairm210.purity.annotations.Readonly
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -80,6 +81,7 @@ object LuxuryResourcePlacementLogic {
return Pair(cityStateLuxuries, randomLuxuries) return Pair(cityStateLuxuries, randomLuxuries)
} }
@Readonly
private fun getLuxuriesForRandomPlacement( private fun getLuxuriesForRandomPlacement(
assignableLuxuries: List<TileResource>, assignableLuxuries: List<TileResource>,
amountRegionsWithLuxury: HashMap<String, Int>, amountRegionsWithLuxury: HashMap<String, Int>,
@ -97,6 +99,7 @@ object LuxuryResourcePlacementLogic {
return remainingLuxuries.drop(targetDisabledLuxuries) return remainingLuxuries.drop(targetDisabledLuxuries)
} }
@Readonly
private fun getCandidateLuxuries( private fun getCandidateLuxuries(
assignableLuxuries: List<TileResource>, assignableLuxuries: List<TileResource>,
amountRegionsWithLuxury: HashMap<String, Int>, amountRegionsWithLuxury: HashMap<String, Int>,

View File

@ -5,6 +5,7 @@ import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.ruleset.unit.BaseUnit import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.ui.components.extensions.toPercent import com.unciv.ui.components.extensions.toPercent
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
import yairm210.purity.annotations.Readonly
import kotlin.math.pow import kotlin.math.pow
class UnitUpgradeManager(val unit: MapUnit) { class UnitUpgradeManager(val unit: MapUnit) {
@ -16,6 +17,7 @@ class UnitUpgradeManager(val unit: MapUnit) {
* @param ignoreResources Ignore resource requirements (tech still counts) * @param ignoreResources Ignore resource requirements (tech still counts)
* Used to display disabled Upgrade button * Used to display disabled Upgrade button
*/ */
@Readonly
fun canUpgrade( fun canUpgrade(
unitToUpgradeTo: BaseUnit, unitToUpgradeTo: BaseUnit,
ignoreRequirements: Boolean = false, ignoreRequirements: Boolean = false,

View File

@ -108,6 +108,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
super<INonPerpetualConstruction>.isUnavailableBySettings(gameInfo) || super<INonPerpetualConstruction>.isUnavailableBySettings(gameInfo) ||
(!gameInfo.gameParameters.nuclearWeaponsEnabled && isNuclearWeapon()) (!gameInfo.gameParameters.nuclearWeaponsEnabled && isNuclearWeapon())
@Readonly
fun getUpgradeUnits(gameContext: GameContext = GameContext.EmptyState): Sequence<String> { fun getUpgradeUnits(gameContext: GameContext = GameContext.EmptyState): Sequence<String> {
return sequence { return sequence {
yieldIfNotNull(upgradesTo) yieldIfNotNull(upgradesTo)
@ -116,6 +117,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
} }
} }
@Readonly
fun getRulesetUpgradeUnits(gameContext: GameContext = GameContext.EmptyState): Sequence<BaseUnit> { fun getRulesetUpgradeUnits(gameContext: GameContext = GameContext.EmptyState): Sequence<BaseUnit> {
return sequence { return sequence {
for (unit in getUpgradeUnits(gameContext)) for (unit in getUpgradeUnits(gameContext))

View File

@ -83,6 +83,7 @@ fun <T> Sequence<T>.toGdxArray(): Array<T> {
} }
/** [yield][SequenceScope.yield]s [element] if it's not null */ /** [yield][SequenceScope.yield]s [element] if it's not null */
@Pure
suspend fun <T> SequenceScope<T>.yieldIfNotNull(element: T?) { suspend fun <T> SequenceScope<T>.yieldIfNotNull(element: T?) {
if (element != null) yield(element) if (element != null) yield(element)
} }