chore(purity): Espionage

This commit is contained in:
yairm210 2025-08-06 17:46:47 +03:00
parent 666c2056f3
commit 5c09f1b743
6 changed files with 34 additions and 26 deletions

View File

@ -61,6 +61,7 @@ allprojects {
"java.util.BitSet.get", // moved
"kotlin.collections.getValue", // moved
"kotlin.collections.randomOrNull",
)
wellKnownPureClasses = setOf<String>(
)

View File

@ -4,6 +4,7 @@ import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.models.Spy
import yairm210.purity.annotations.Readonly
enum class SpyFleeReason {
CityDestroyed,
@ -25,13 +26,11 @@ class CityEspionageManager : IsPartOfGameInfoSerialization {
this.city = city
}
fun hasSpyOf(civInfo: Civilization): Boolean {
return civInfo.espionageManager.spyList.any { it.getCityOrNull() == city }
}
@Readonly fun hasSpyOf(civInfo: Civilization): Boolean = civInfo.espionageManager.spyList.any { it.getCityOrNull() == city }
fun getAllStationedSpies(): List<Spy> {
return city.civ.gameInfo.civilizations.flatMap { it.espionageManager.getSpiesInCity(city) }
}
@Readonly fun getAllStationedSpies(): List<Spy> =
city.civ.gameInfo.civilizations.flatMap { it.espionageManager.getSpiesInCity(city) }
fun removeAllPresentSpies(reason: SpyFleeReason) {
for (spy in getAllStationedSpies()) {

View File

@ -71,6 +71,7 @@ class CityPopulationManager : IsPartOfGameInfoSerialization {
/** Take null to mean infinity. */
@Readonly
fun getNumTurnsToNewPopulation(): Int? {
if (!city.isGrowing()) return null
val roundedFoodPerTurn = city.foodForNextTurn().toFloat()

View File

@ -1039,7 +1039,7 @@ class Civilization : IsPartOfGameInfoSerialization {
moveCapitalTo(newCapital, oldCapital)
}
fun getAllyCiv(): Civilization? = if (allyCivName == null) null
@Readonly fun getAllyCiv(): Civilization? = if (allyCivName == null) null
else gameInfo.getCivilization(allyCivName!!)
@Readonly fun getAllyCivName() = allyCivName
fun setAllyCiv(newAllyName: String?) { allyCivName = newAllyName }

View File

@ -6,6 +6,7 @@ import com.unciv.logic.civilization.Civilization
import com.unciv.logic.map.tile.Tile
import com.unciv.models.Spy
import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.Readonly
class EspionageManager : IsPartOfGameInfoSerialization {
@ -42,6 +43,7 @@ class EspionageManager : IsPartOfGameInfoSerialization {
spy.endTurn()
}
@Readonly
fun getSpyName(): String {
val usedSpyNames = spyList.map { it.name }.toHashSet()
val validSpyNames = civInfo.nation.spyNames.filter { it !in usedSpyNames }
@ -57,6 +59,7 @@ class EspionageManager : IsPartOfGameInfoSerialization {
return newSpy
}
@Readonly
fun getTilesVisibleViaSpies(): Sequence<Tile> {
return spyList.asSequence()
.filter { it.isSetUp() }
@ -64,6 +67,7 @@ class EspionageManager : IsPartOfGameInfoSerialization {
.flatMap { it.getCenterTile().getTilesInDistance(1) }
}
@Readonly
fun getTechsToSteal(otherCiv: Civilization): Set<String> {
val techsToSteal = mutableSetOf<String>()
for (tech in otherCiv.tech.techsResearched) {
@ -74,30 +78,26 @@ class EspionageManager : IsPartOfGameInfoSerialization {
return techsToSteal
}
fun getSpiesInCity(city: City): List<Spy> {
return spyList.filterTo(mutableListOf()) { it.getCityOrNull() == city }
}
@Readonly fun getSpiesInCity(city: City): List<Spy> = spyList.filter { it.getCityOrNull() == city }
fun getStartingSpyRank(): Int = 1 + civInfo.getMatchingUniques(UniqueType.SpyStartingLevel).sumOf { it.params[0].toInt() }
@Readonly fun getStartingSpyRank(): Int = 1 + civInfo.getMatchingUniques(UniqueType.SpyStartingLevel).sumOf { it.params[0].toInt() }
/**
* Returns a list of all cities with our spies in them.
* The list needs to be stable across calls on the same turn.
*/
fun getCitiesWithOurSpies(): List<City> = spyList.filter { it.isSetUp() }.mapNotNull { it.getCityOrNull() }
@Readonly fun getCitiesWithOurSpies(): List<City> = spyList.filter { it.isSetUp() }.mapNotNull { it.getCityOrNull() }
fun getSpyAssignedToCity(city: City): Spy? = spyList.firstOrNull { it.getCityOrNull() == city }
@Readonly fun getSpyAssignedToCity(city: City): Spy? = spyList.firstOrNull { it.getCityOrNull() == city }
/**
* Determines whether the NextTurnAction MoveSpies should be shown or not
* @return true if there are spies waiting to be moved
*/
fun shouldShowMoveSpies(): Boolean = !dismissedShouldMoveSpies && spyList.any { it.isIdle() }
@Readonly fun shouldShowMoveSpies(): Boolean = !dismissedShouldMoveSpies && spyList.any { it.isIdle() }
&& civInfo.gameInfo.getCities().any { civInfo.hasExplored(it.getCenterTile()) && getSpyAssignedToCity(it) == null }
fun getIdleSpies(): List<Spy> {
return spyList.filterTo(mutableListOf()) { it.isIdle() }
}
@Readonly fun getIdleSpies(): List<Spy> = spyList.filter { it.isIdle() }
/**
* Takes all spies away from their cities.

View File

@ -14,6 +14,7 @@ import com.unciv.logic.civilization.diplomacy.DiplomaticModifiers
import com.unciv.logic.civilization.managers.EspionageManager
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.Readonly
import kotlin.math.ceil
import kotlin.random.Random
@ -166,6 +167,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
* A -1 means we have no techonologies to steal.
* A -2 means we the city produces no science
*/
@Readonly @Suppress("purity") // something here is wrong, it's actually mutating?!
private fun getTurnsRemainingToStealTech(): Int {
val stealableTechs = espionageManager.getTechsToSteal(getCity().civ)
if (stealableTechs.isEmpty()) return -1
@ -314,6 +316,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
/**
* Calculates the success chance of a coup in this city state.
*/
@Readonly
fun getCoupChanceOfSuccess(includeunknownFactors: Boolean): Float {
val cityState = getCity().civ
var successPercentage = 50f
@ -348,7 +351,8 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
this.city = city
setAction(SpyAction.Moving, 1)
}
@Readonly
private fun canDismissAgreementToNotSendSpies(city: City): Boolean {
val otherCivDiplomacyManager = city.civ.getDiplomacyManager(civInfo) ?: return false
val otherCiv = otherCivDiplomacyManager.civInfo
@ -370,6 +374,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
return false
}
@Readonly
fun canMoveTo(city: City): Boolean {
if (canDismissAgreementToNotSendSpies(city)) return false
if (getCityOrNull() == city) return true
@ -377,13 +382,14 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
return espionageManager.getSpyAssignedToCity(city) == null
}
fun isSetUp() = action.isSetUp
@Readonly fun isSetUp() = action.isSetUp
fun isIdle() = action == SpyAction.None
@Readonly fun isIdle() = action == SpyAction.None
fun isDoingWork() = action.isDoingWork(this)
@Readonly fun isDoingWork() = action.isDoingWork(this)
/** Returns the City this Spy is in, or `null` if it is in the hideout. */
@Readonly @Suppress("purity") // this also appears to be NOT readonly. Something is fishy.
fun getCityOrNull(): City? {
if (location == null) return null
if (city == null) city = civInfo.gameInfo.tileMap[location!!].getCity()
@ -392,9 +398,9 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
/** Non-null version of [getCityOrNull] for the frequent case it is known the spy cannot be in the hideout.
* @throws NullPointerException if the spy is in the hideout */
fun getCity(): City = getCityOrNull()!!
@Readonly fun getCity(): City = getCityOrNull()!!
fun getLocationName() = getCityOrNull()?.name ?: Constants.spyHideout
@Readonly fun getLocationName() = getCityOrNull()?.name ?: Constants.spyHideout
fun levelUpSpy(amount: Int = 1) {
if (rank >= civInfo.gameInfo.ruleset.modOptions.constants.maxSpyRank) return
@ -406,12 +412,13 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
}
/** Modifier of the skill bonus of the spy by percent */
fun getSkillModifierPercent() = rank * civInfo.gameInfo.ruleset.modOptions.constants.spyRankSkillPercentBonus
@Readonly fun getSkillModifierPercent() = rank * civInfo.gameInfo.ruleset.modOptions.constants.spyRankSkillPercentBonus
/**
* Gets a friendly and enemy efficiency uniques for the spy at the location
* @return a value centered around 1.0 for the work efficiency of the spy, won't be negative
*/
@Readonly
fun getEfficiencyModifier(): Double {
val friendlyUniques: Sequence<Unique>
val enemyUniques: Sequence<Unique>
@ -446,7 +453,7 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
rank = 1
}
fun isAlive(): Boolean = action != SpyAction.Dead
@Readonly fun isAlive(): Boolean = action != SpyAction.Dead
/** Shorthand for [Civilization.addNotification] specialized for espionage - action, category and icon are always the same */
fun addNotification(text: String) =
@ -454,5 +461,5 @@ class Spy private constructor() : IsPartOfGameInfoSerialization {
/** Anti-save-scum: Deterministic random from city and turn
* @throws NullPointerException for spies in the hideout */
private fun randomSeed() = (getCity().run { location.x * location.y } + 123f * civInfo.gameInfo.turns).toInt() + name.hashCode()
@Readonly private fun randomSeed() = (getCity().run { location.x * location.y } + 123f * civInfo.gameInfo.turns).toInt() + name.hashCode()
}