chore(purity): BattleHelper

This commit is contained in:
yairm210 2025-08-12 08:30:56 +03:00
parent 4a82d07ae0
commit 74b32e137a
6 changed files with 36 additions and 15 deletions

View File

@ -38,7 +38,7 @@ plugins {
// This is *with* gradle 8.2 downloaded according the project specs, no idea what that's about
kotlin("multiplatform") version "1.9.24"
kotlin("plugin.serialization") version "1.9.24"
id("io.github.yairm210.purity-plugin") version "0.0.51" apply(false)
id("io.github.yairm210.purity-plugin") version "1.1.0" apply(false)
}
allprojects {

View File

@ -17,7 +17,6 @@ import com.unciv.models.ruleset.IConstruction
import com.unciv.models.ruleset.INonPerpetualConstruction
import com.unciv.models.ruleset.MilestoneType
import com.unciv.models.ruleset.PerpetualConstruction
import com.unciv.models.ruleset.Victory
import com.unciv.models.ruleset.nation.PersonalityValue
import com.unciv.models.ruleset.unique.LocalUniqueCache
import com.unciv.models.ruleset.unique.UniqueType
@ -26,6 +25,7 @@ import com.unciv.models.stats.Stat
import com.unciv.models.stats.Stats
import com.unciv.ui.screens.cityscreen.CityScreen
import com.unciv.ui.screens.victoryscreen.RankingType
import yairm210.purity.annotations.Readonly
import kotlin.math.max
import kotlin.math.sqrt
@ -42,6 +42,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
private val constructionsToAvoid = personality.getMatchingUniques(UniqueType.WillNotBuild, cityState)
.map{ it.params[0] }
@Readonly
private fun shouldAvoidConstruction (construction: IConstruction): Boolean {
val stateForConditionals = cityState
for (toAvoid in constructionsToAvoid) {
@ -109,7 +111,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
fun chooseNextConstruction() {
if (cityConstructions.getCurrentConstruction() !is PerpetualConstruction) return // don't want to be stuck on these forever
addBuildingChoices()
if (!city.isPuppet) {
@ -197,6 +199,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
// Define what makes a tile worth sending a Workboat to
// todo Prepare for mods that allow improving water tiles without a resource?
@Readonly
fun Tile.isWorthImproving(): Boolean {
if (getOwner() != civInfo) return false
if (!WorkerAutomation.hasWorkableSeaResource(this, civInfo)) return false
@ -205,6 +208,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
// Search for a tile justifying producing a Workboat
// todo should workboatAutomationSearchMaxTiles depend on game state?
@Readonly
fun findTileWorthImproving(): Boolean {
val searchMaxTiles = civInfo.gameInfo.ruleset.modOptions.constants.workboatAutomationSearchMaxTiles
val bfs = BFS(city.getCenterTile()) {
@ -263,13 +267,14 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
private fun getValueOfBuilding(building: Building, localUniqueCache: LocalUniqueCache): Float {
var value = 0f
value += applyBuildingStats(building, localUniqueCache)
value += applyMilitaryBuildingValue(building)
value += applyVictoryBuildingValue(building)
value += applyOnetimeUniqueBonuses(building)
value += getMilitaryBuildingValue(building)
value += getVictoryBuildingValue(building)
value += getOnetimeUniqueBonuses(building)
return value
}
private fun applyOnetimeUniqueBonuses(building: Building): Float {
@Readonly
private fun getOnetimeUniqueBonuses(building: Building): Float {
var value = 0f
if (building.isWonder) {
// Buildings generally don't have these uniques, and Wonders generally only one of these, so we can save some time by not checking every building for every unique
@ -295,8 +300,9 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
}
return value
}
private fun applyVictoryBuildingValue(building: Building): Float {
@Readonly
private fun getVictoryBuildingValue(building: Building): Float {
var value = 0f
if (!cityIsOverAverageProduction) return value
if (building.hasUnique(UniqueType.TriggersCulturalVictory)
@ -305,7 +311,8 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
return value
}
private fun applyMilitaryBuildingValue(building: Building): Float {
@Readonly
private fun getMilitaryBuildingValue(building: Building): Float {
var value = 0f
var warModifier = if (isAtWar) 1f else .5f
// If this city is the closest city to another civ, that makes it a likely candidate for attack
@ -330,7 +337,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
private fun applyBuildingStats(building: Building, localUniqueCache: LocalUniqueCache): Float {
val buildingStats = getStatDifferenceFromBuilding(building.name, localUniqueCache)
getBuildingStatsFromUniques(building, buildingStats)
buildingStats.add(getBuildingStatsFromUniques(building, buildingStats))
buildingStats.food *= 3
@ -352,6 +359,7 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
return Automation.rankStatsValue(buildingStats.clone(), civInfo)
}
// NOT readonly safe, since it alters the tile ownership of real tiles
private fun getStatDifferenceFromBuilding(building: String, localUniqueCache: LocalUniqueCache): Stats {
val newCity = city.clone()
newCity.setTransients(city.civ) // Will break the owned tiles. Needs to be reverted before leaving this function
@ -365,20 +373,23 @@ class ConstructionAutomation(val cityConstructions: CityConstructions) {
return newCity.cityStats.currentCityStats - oldStats
}
private fun getBuildingStatsFromUniques(building: Building, buildingStats: Stats) {
@Readonly
private fun getBuildingStatsFromUniques(building: Building, buildingStats: Stats) : Stats {
val stats = Stats()
for (unique in building.getMatchingUniques(UniqueType.StatPercentBonusCities, cityState)) {
val statType = Stat.valueOf(unique.params[1])
val relativeAmount = unique.params[0].toFloat() / 100f
val amount = civInfo.stats.statsForNextTurn[statType] * relativeAmount
buildingStats[statType] += amount
stats[statType] += amount
}
for (unique in building.getMatchingUniques(UniqueType.CarryOverFood, cityState)) {
if (city.matchesFilter(unique.params[1]) && unique.params[0].toInt() != 0) {
val foodGain = cityStats.currentCityStats.food + buildingStats.food
val relativeAmount = unique.params[0].toFloat() / 100f
buildingStats[Stat.Food] += foodGain * relativeAmount // Essentialy gives us the food per turn this unique saves us
stats[Stat.Food] += foodGain * relativeAmount // Essentialy gives us the food per turn this unique saves us
}
}
return stats
}
}

View File

@ -10,6 +10,7 @@ import com.unciv.logic.city.City
import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.models.ruleset.unique.GameContext
import com.unciv.models.ruleset.unique.UniqueType
import yairm210.purity.annotations.Readonly
object BattleHelper {
@ -71,6 +72,7 @@ object BattleHelper {
/**
* Choses the best target in attackableEnemies, this could be a city or a unit.
*/
@Readonly
private fun chooseAttackTarget(unit: MapUnit, attackableEnemies: List<AttackableTile>): AttackableTile? {
// Get the highest valued attackableEnemy
var highestAttackValue = 0
@ -95,6 +97,7 @@ object BattleHelper {
* Siege units will almost always attack cities.
* Base value is 100(Mele) 110(Ranged) standard deviation is around 80 to 130
*/
@Readonly
private fun getCityAttackValue(attacker: MapUnit, city: City): Int {
val attackerUnit = MapUnitCombatant(attacker)
val cityUnit = CityCombatant(city)
@ -144,6 +147,7 @@ object BattleHelper {
* Returns a value which represents the attacker's motivation to attack a unit.
* Base value is 100 and standard deviation is around 80 to 130
*/
@Readonly
private fun getUnitAttackValue(attacker: MapUnit, attackTile: AttackableTile): Int {
// Base attack value, there is nothing there...
var attackValue = Int.MIN_VALUE

View File

@ -10,6 +10,7 @@ import com.unciv.logic.map.mapunit.MapUnit
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.ui.screens.worldscreen.unit.actions.UnitActions
import yairm210.purity.annotations.Readonly
object ReligiousUnitAutomation {
@ -21,6 +22,7 @@ object ReligiousUnitAutomation {
it.religion.getMajorityReligion() != unit.civ.religionManager.religion
}
@Readonly
fun isValidSpreadReligionTarget(city: City): Boolean {
val diplomacyManager = unit.civ.getDiplomacyManager(city.civ)
if (diplomacyManager?.hasFlag(DiplomacyFlags.AgreedToNotSpreadReligion) == true){
@ -33,6 +35,7 @@ object ReligiousUnitAutomation {
}
/** Lowest value will be chosen */
@Readonly
fun rankCityForReligionSpread(city: City): Int {
var rank = city.getCenterTile().aerialDistanceTo(unit.getTile())
@ -44,6 +47,7 @@ object ReligiousUnitAutomation {
return rank
}
val city =
if (ourCitiesWithoutReligion.any())
ourCitiesWithoutReligion.minByOrNull { it.getCenterTile().aerialDistanceTo(unit.getTile()) }
@ -118,6 +122,7 @@ object ReligiousUnitAutomation {
}
}
@Readonly
private fun determineBestInquisitorCityToConvert(
unit: MapUnit,
): City? {

View File

@ -497,6 +497,7 @@ class BaseUnit : RulesetObject(), INonPerpetualConstruction {
val isWaterUnit by lazy { type.isWaterUnit() }
@Readonly fun isAirUnit() = type.isAirUnit()
@Readonly
fun isProbablySiegeUnit() = isRanged()
&& getMatchingUniques(UniqueType.Strength, GameContext.IgnoreConditionals)
.any { it.params[0].toInt() > 0 && it.hasModifier(UniqueType.ConditionalVsCity) }

View File

@ -59,7 +59,7 @@ object Log {
*
* The [params] can contain value-producing lambdas, which will be called and their value used as parameter for the message instead.
*/
@Pure @Suppress("purity") // log considered pure everywhere
@Pure @Suppress("purity") // good suppression - log considered pure everywhere
fun debug(msg: String, vararg params: Any?) {
if (backend.isRelease()) return
debug(getTag(), msg, *params)