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.hashCode",
"kotlin.collections.shuffled",
)
wellKnownPureClasses = setOf(
"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.tile.Tile
import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.Pure
import yairm210.purity.annotations.Readonly
object AirUnitAutomation {
@ -29,7 +31,7 @@ object AirUnitAutomation {
if (friendlyUnusedFighterCount < enemyFighters) return
if (friendlyUsedFighterCount <= enemyFighters) {
fun airSweepDamagePercentBonus(): Int {
@Readonly fun airSweepDamagePercentBonus(): Int {
return unit.getMatchingUniques(UniqueType.StrengthWhenAirsweep)
.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
* By default the value is -500 to prevent inefficient nuking.
*/
@Readonly
private fun getNukeLocationValue(nuke: MapUnit, tile: Tile): Int {
val civ = nuke.civ
if (!Nuke.mayUseNuke(MapUnitCombatant(nuke), tile)) return Int.MIN_VALUE
@ -166,6 +169,7 @@ object AirUnitAutomation {
var explosionValue = -500
// 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 {
if (targetCiv == civ) // We are nuking something that we own!
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.canUse
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import yairm210.purity.annotations.Readonly
object CivilianUnitAutomation {
@Readonly
fun shouldClearTileForAddInCapitalUnits(unit: MapUnit, tile: Tile) =
tile.isCityCenter() && tile.getCity()!!.isCapital()
&& !unit.hasUnique(UniqueType.AddInCapital)
@ -23,6 +25,7 @@ object CivilianUnitAutomation {
// 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
@Readonly
fun hasSettlerAction(uniqueType: UniqueType) =
unit.getMatchingUniques(uniqueType, GameContext.IgnoreConditionals)
.filter { unique -> !unique.hasModifier(UniqueType.UnitActionExtraLimitedTimes) }
@ -172,6 +175,7 @@ object CivilianUnitAutomation {
return // The AI doesn't know how to handle unknown civilian units
}
@Readonly
private fun isLateGame(civ: Civilization): Boolean {
val researchCompletePercent =
(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.movement.PathsToTilesWithinTurn
import com.unciv.logic.map.tile.Tile
import yairm210.purity.annotations.Readonly
object HeadTowardsEnemyCityAutomation {
@ -28,6 +29,7 @@ object HeadTowardsEnemyCityAutomation {
)
}
@Readonly
fun getEnemyCitiesByPriority(unit: MapUnit): Sequence<City> {
val enemies = unit.civ.getKnownCivs()
.filter { unit.civ.isAtWarWith(it) && it.cities.isNotEmpty() }
@ -94,6 +96,7 @@ object HeadTowardsEnemyCityAutomation {
}
/** Cannot take within 5 turns */
@Readonly
private fun cannotTakeCitySoon(
ourUnitsAroundEnemyCity: Sequence<MapUnit>,
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.UnitActionsUpgrade
import com.unciv.utils.randomWeighted
import yairm210.purity.annotations.Readonly
object UnitAutomation {
private const val CLOSE_ENEMY_TILES_AWAY_LIMIT = 5
private const val CLOSE_ENEMY_TURNS_AWAY_LIMIT = 3f
@Readonly
private fun isGoodTileToExplore(unit: MapUnit, tile: Tile): Boolean {
return (tile.getOwner() == null || !tile.getOwner()!!.isCityState)
&& tile.getTilesInDistance(unit.getVisibilityRange()).any { !unit.civ.hasExplored(it) }
@ -103,6 +105,7 @@ object UnitAutomation {
return false
}
@Readonly
private fun isGoodTileForFogBusting(unit: MapUnit, tile: Tile): Boolean {
return unit.movement.canMoveTo(tile)
&& 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.
* Note that if the unit can't upgrade, the current BaseUnit is returned.
*/
@Readonly
private fun getUnitsToUpgradeTo(unit: MapUnit): Sequence<BaseUnit> {
@Readonly
fun isInvalidUpgradeDestination(baseUnit: BaseUnit): Boolean {
if (!unit.civ.tech.isResearched(baseUnit))
return true
@ -423,6 +428,7 @@ object UnitAutomation {
/**
* @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 {
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
@ -433,6 +439,7 @@ object UnitAutomation {
return healthRequiredPerTurn <= unit.rankTileForHealing(unit.getTile())
}
@Readonly
private fun getDangerousTiles(unit: MapUnit): HashSet<Tile> {
val nearbyEnemyUnits = unit.currentTile.getTilesInDistance(3)
.flatMap { tile -> tile.getUnits().filter { unit.civ.isAtWarWith(it.civ) } }
@ -517,6 +524,7 @@ object UnitAutomation {
private fun tryPrepare(unit: MapUnit): Boolean {
val civInfo = unit.civ
@Readonly
fun hasPreparationFlag(targetCiv: Civilization): Boolean {
val diploManager = civInfo.getDiplomacyManager(targetCiv)!!
if (diploManager.hasFlag(DiplomacyFlags.Denunciation)
@ -685,7 +693,7 @@ object UnitAutomation {
unit.movement.headTowards(closestReachableCityNeedsDefending.getCenterTile())
return true
}
@Readonly
private fun isCityThatNeedsDefendingInWartime(city: City): Boolean {
if (city.health < city.getMaxHealth()) return true // this city is under attack!
for (enemyCivCity in city.civ.diplomacy.values
@ -696,7 +704,7 @@ object UnitAutomation {
}
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
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.screens.worldscreen.unit.actions.UnitActionsPillage
import com.unciv.utils.debug
import yairm210.purity.annotations.Readonly
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@ -678,7 +679,8 @@ object Battle {
city.isBeingRazed = true
}
}
@Readonly
fun getMapCombatantOfTile(tile: Tile): ICombatant? {
if (tile.isCityCenter()) return CityCombatant(tile.getCity()!!)
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.ui.components.extensions.toPercent
import com.unciv.ui.screens.worldscreen.bottombar.BattleTable
import yairm210.purity.annotations.Readonly
import kotlin.math.ulp
import kotlin.random.Random
@ -28,6 +29,7 @@ object Nuke {
*
* Both [BattleTable.simulateNuke] and [AirUnitAutomation.automateNukes] check range, so that check is omitted here.
*/
@Readonly
fun mayUseNuke(nuke: MapUnitCombatant, targetTile: Tile): Boolean {
val attackerCiv = nuke.getCivInfo()
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
* 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 {
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

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.UniqueType
import com.unciv.utils.randomWeighted
import yairm210.purity.annotations.Readonly
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
@ -80,6 +81,7 @@ object LuxuryResourcePlacementLogic {
return Pair(cityStateLuxuries, randomLuxuries)
}
@Readonly
private fun getLuxuriesForRandomPlacement(
assignableLuxuries: List<TileResource>,
amountRegionsWithLuxury: HashMap<String, Int>,
@ -97,6 +99,7 @@ object LuxuryResourcePlacementLogic {
return remainingLuxuries.drop(targetDisabledLuxuries)
}
@Readonly
private fun getCandidateLuxuries(
assignableLuxuries: List<TileResource>,
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.ui.components.extensions.toPercent
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActionsUpgrade
import yairm210.purity.annotations.Readonly
import kotlin.math.pow
class UnitUpgradeManager(val unit: MapUnit) {
@ -16,6 +17,7 @@ class UnitUpgradeManager(val unit: MapUnit) {
* @param ignoreResources Ignore resource requirements (tech still counts)
* Used to display disabled Upgrade button
*/
@Readonly
fun canUpgrade(
unitToUpgradeTo: BaseUnit,
ignoreRequirements: Boolean = false,

View File

@ -108,6 +108,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
super<INonPerpetualConstruction>.isUnavailableBySettings(gameInfo) ||
(!gameInfo.gameParameters.nuclearWeaponsEnabled && isNuclearWeapon())
@Readonly
fun getUpgradeUnits(gameContext: GameContext = GameContext.EmptyState): Sequence<String> {
return sequence {
yieldIfNotNull(upgradesTo)
@ -116,6 +117,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
}
}
@Readonly
fun getRulesetUpgradeUnits(gameContext: GameContext = GameContext.EmptyState): Sequence<BaseUnit> {
return sequence {
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 */
@Pure
suspend fun <T> SequenceScope<T>.yieldIfNotNull(element: T?) {
if (element != null) yield(element)
}