mirror of
https://github.com/yairm210/Unciv.git
synced 2025-09-28 14:24:43 -04:00
Stats optimization (#4830)
* Stats rework part 1 * Stats rework part 1 -patch1 * Stats rework part 1 - documentation
This commit is contained in:
parent
4dc5cdd58a
commit
8fdff9a940
@ -1,21 +1,69 @@
|
||||
package com.unciv.models.stats
|
||||
|
||||
import com.unciv.models.translations.tr
|
||||
import kotlin.reflect.KMutableProperty0
|
||||
|
||||
|
||||
open class Stats() {
|
||||
var production: Float = 0f
|
||||
var food: Float = 0f
|
||||
var gold: Float = 0f
|
||||
var science: Float = 0f
|
||||
var culture: Float = 0f
|
||||
var happiness: Float = 0f
|
||||
/**
|
||||
* A container for the seven basic ["currencies"][Stat] in Unciv,
|
||||
* **Mutable**, allowing for easy merging of sources and applying bonuses.
|
||||
*/
|
||||
open class Stats(
|
||||
var production: Float = 0f,
|
||||
var food: Float = 0f,
|
||||
var gold: Float = 0f,
|
||||
var science: Float = 0f,
|
||||
var culture: Float = 0f,
|
||||
var happiness: Float = 0f,
|
||||
var faith: Float = 0f
|
||||
|
||||
constructor(hashMap: HashMap<Stat, Float>) : this() {
|
||||
setStats(hashMap)
|
||||
) {
|
||||
// This is what facilitates indexed access by [Stat] or add(Stat,Float)
|
||||
// without additional memory allocation or expensive conditionals
|
||||
@delegate:Transient
|
||||
private val mapView: Map<Stat, KMutableProperty0<Float>> by lazy {
|
||||
mapOf(
|
||||
Stat.Production to ::production,
|
||||
Stat.Food to ::food,
|
||||
Stat.Gold to ::gold,
|
||||
Stat.Science to ::science,
|
||||
Stat.Culture to ::culture,
|
||||
Stat.Happiness to ::happiness,
|
||||
Stat.Faith to ::faith
|
||||
)
|
||||
}
|
||||
|
||||
/** Indexed read of a value for a given [Stat], e.g. `this.gold == this[Stat.Gold]` */
|
||||
operator fun get(stat: Stat) = mapView[stat]!!.get()
|
||||
/** Indexed write of a value for a given [Stat], e.g. `this.gold += 1f` is equivalent to `this[Stat.Gold] += 1f` */
|
||||
operator fun set(stat: Stat, value: Float) = mapView[stat]!!.set(value)
|
||||
|
||||
/** Compares two instances. Not callable via `==`. */
|
||||
// This is an overload, not an override conforming to the kotlin conventions of `equals(Any?)`,
|
||||
// so do not rely on it to be called for the `==` operator! A tad more efficient, though.
|
||||
@Suppress("CovariantEquals") // historical reasons to keep this function signature
|
||||
fun equals(otherStats: Stats): Boolean {
|
||||
return production == otherStats.production
|
||||
&& food == otherStats.food
|
||||
&& gold == otherStats.gold
|
||||
&& science == otherStats.science
|
||||
&& culture == otherStats.culture
|
||||
&& happiness == otherStats.happiness
|
||||
&& faith == otherStats.faith
|
||||
}
|
||||
|
||||
/** @return a new instance containing the same values as `this` */
|
||||
fun clone() = Stats(production, food, gold, science, culture, happiness, faith)
|
||||
|
||||
/** @return `true` if all values are zero */
|
||||
fun isEmpty() = (
|
||||
production == 0f
|
||||
&& food == 0f
|
||||
&& gold == 0f
|
||||
&& science == 0f
|
||||
&& culture == 0f
|
||||
&& happiness == 0f
|
||||
&& faith == 0f )
|
||||
|
||||
/** Reset all values to zero (in place) */
|
||||
fun clear() {
|
||||
production = 0f
|
||||
food = 0f
|
||||
@ -26,8 +74,8 @@ open class Stats() {
|
||||
faith = 0f
|
||||
}
|
||||
|
||||
/** Adds each value of another [Stats] instance to this one in place */
|
||||
fun add(other: Stats) {
|
||||
// Doing this through the hashmap is nicer code but is SUPER INEFFICIENT!
|
||||
production += other.production
|
||||
food += other.food
|
||||
gold += other.gold
|
||||
@ -37,98 +85,96 @@ open class Stats() {
|
||||
faith += other.faith
|
||||
}
|
||||
|
||||
/** @return a new [Stats] instance containing the sum of its operands value by value */
|
||||
operator fun plus(stats: Stats) = clone().apply { add(stats) }
|
||||
|
||||
/** Adds the [value] parameter to the instance value specified by [stat] in place
|
||||
* @return `this` to allow chaining */
|
||||
fun add(stat: Stat, value: Float): Stats {
|
||||
val hashMap = toHashMap()
|
||||
hashMap[stat] = hashMap[stat]!! + value
|
||||
setStats(hashMap)
|
||||
mapView[stat]!!.set(value + mapView[stat]!!.get())
|
||||
return this
|
||||
}
|
||||
|
||||
operator fun plus(stat: Stats): Stats {
|
||||
val clone = clone()
|
||||
clone.add(stat)
|
||||
return clone
|
||||
}
|
||||
|
||||
fun clone(): Stats {
|
||||
val stats = Stats()
|
||||
stats.add(this)
|
||||
return stats
|
||||
}
|
||||
|
||||
/** @return The result of multiplying each value of this instance by [number] as a new instance */
|
||||
operator fun times(number: Int) = times(number.toFloat())
|
||||
/** @return The result of multiplying each value of this instance by [number] as a new instance */
|
||||
operator fun times(number: Float) = Stats(
|
||||
production * number,
|
||||
food * number,
|
||||
gold * number,
|
||||
science * number,
|
||||
culture * number,
|
||||
happiness * number,
|
||||
faith * number
|
||||
)
|
||||
|
||||
operator fun times(number: Float): Stats {
|
||||
val hashMap = toHashMap()
|
||||
for (stat in Stat.values()) hashMap[stat] = number * hashMap[stat]!!
|
||||
return Stats(hashMap)
|
||||
}
|
||||
|
||||
/** Multiplies each value of this instance by [number] in place */
|
||||
fun timesInPlace(number: Float) {
|
||||
val hashMap = toHashMap()
|
||||
for (stat in Stat.values()) hashMap[stat] = number * hashMap[stat]!!
|
||||
setStats(hashMap)
|
||||
production *= number
|
||||
food *= number
|
||||
gold *= number
|
||||
science *= number
|
||||
culture *= number
|
||||
happiness *= number
|
||||
faith *= number
|
||||
}
|
||||
|
||||
fun isEmpty() = equals(Stats())
|
||||
|
||||
/** ***Not*** only a debug helper. It returns a string representing the content, already _translated_.
|
||||
*
|
||||
* Example output: `+1 Production, -1 Food`.
|
||||
*/
|
||||
override fun toString(): String {
|
||||
return toHashMap().filter { it.value != 0f }
|
||||
.map { (if (it.value > 0) "+" else "") + it.value.toInt() + " " + it.key.toString().tr() }.joinToString()
|
||||
}
|
||||
|
||||
/** @return a Map copy of the values in this instance, can be used to iterate over the values */
|
||||
fun toHashMap(): HashMap<Stat, Float> {
|
||||
return linkedMapOf(Stat.Production to production,
|
||||
Stat.Culture to culture,
|
||||
Stat.Gold to gold,
|
||||
Stat.Food to food,
|
||||
Stat.Happiness to happiness,
|
||||
Stat.Science to science,
|
||||
Stat.Faith to faith
|
||||
return linkedMapOf(
|
||||
Stat.Production to production,
|
||||
Stat.Food to food,
|
||||
Stat.Gold to gold,
|
||||
Stat.Science to science,
|
||||
Stat.Culture to culture,
|
||||
Stat.Happiness to happiness,
|
||||
Stat.Faith to faith
|
||||
)
|
||||
}
|
||||
|
||||
fun get(stat: Stat): Float {
|
||||
return this.toHashMap()[stat]!!
|
||||
}
|
||||
|
||||
private fun setStats(hashMap: HashMap<Stat, Float>) {
|
||||
culture = hashMap[Stat.Culture]!!
|
||||
gold = hashMap[Stat.Gold]!!
|
||||
production = hashMap[Stat.Production]!!
|
||||
food = hashMap[Stat.Food]!!
|
||||
happiness = hashMap[Stat.Happiness]!!
|
||||
science = hashMap[Stat.Science]!!
|
||||
faith = hashMap[Stat.Faith]!!
|
||||
}
|
||||
|
||||
fun equals(otherStats: Stats): Boolean {
|
||||
return culture == otherStats.culture
|
||||
&& gold == otherStats.gold
|
||||
&& production == otherStats.production
|
||||
&& food == otherStats.food
|
||||
&& happiness == otherStats.happiness
|
||||
&& science == otherStats.science
|
||||
&& faith == otherStats.faith
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val allStatNames = Stat.values().joinToString("|") { it.name }
|
||||
private val statRegexPattern = "([+-])(\\d+) ($allStatNames)"
|
||||
private val statRegex = Regex(statRegexPattern)
|
||||
private val entireStringRegexPattern = Regex("$statRegexPattern(, $statRegexPattern)*")
|
||||
fun isStats(string:String): Boolean {
|
||||
|
||||
/** Tests a given string whether it is a valid representation of [Stats],
|
||||
* close to what [toString] would produce.
|
||||
* - Values _must_ carry a sign - "1 Gold" tests `false`, "+1 Gold" is OK.
|
||||
* - Separator is ", " - comma space - the space is _not_ optional.
|
||||
* - Stat names must be untranslated and match case.
|
||||
* - Order is not important.
|
||||
* @see [parse]
|
||||
*/
|
||||
fun isStats(string: String): Boolean {
|
||||
if (string.isEmpty() || string[0] !in "+-") return false // very quick negative check before the heavy Regex
|
||||
return entireStringRegexPattern.matches(string)
|
||||
}
|
||||
fun parse(string:String):Stats{
|
||||
|
||||
/** Parses a string to a [Stats] instance
|
||||
* - Values _must_ carry a sign - "1 Gold" will not parse, "+1 Gold" is OK.
|
||||
* - Separator is ", " - comma space - the space is _not_ optional.
|
||||
* - Stat names must be untranslated and match case.
|
||||
* - Order is not important.
|
||||
* @see [isStats]
|
||||
*/
|
||||
fun parse(string: String): Stats {
|
||||
val toReturn = Stats()
|
||||
val statsWithBonuses = string.split(", ")
|
||||
for(statWithBonuses in statsWithBonuses){
|
||||
val match = statRegex.matchEntire(statWithBonuses)!!
|
||||
val statName = match.groupValues[3]
|
||||
val statAmount = match.groupValues[2].toFloat() * (if(match.groupValues[1]=="-") -1 else 1)
|
||||
val statAmount = match.groupValues[2].toFloat() * (if (match.groupValues[1] == "-") -1 else 1)
|
||||
toReturn.add(Stat.valueOf(statName), statAmount)
|
||||
}
|
||||
return toReturn
|
||||
|
@ -14,6 +14,8 @@ import org.junit.Assert
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import kotlin.math.abs
|
||||
import kotlin.random.Random
|
||||
|
||||
@RunWith(GdxTestRunner::class)
|
||||
class BasicTests {
|
||||
@ -85,4 +87,50 @@ class BasicTests {
|
||||
Assert.assertFalse(modCheck.isNotOK())
|
||||
}
|
||||
|
||||
//@Test // commented so github doesn't run this
|
||||
fun statMathStressTest() {
|
||||
val runtime = Runtime.getRuntime()
|
||||
runtime.gc()
|
||||
Thread.sleep(5000) // makes timings a little more repeatable
|
||||
val startTime = System.nanoTime()
|
||||
statMathRunner(iterations = 1_000_000)
|
||||
println("statMathStressTest took ${(System.nanoTime()-startTime)/1000}µs")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun statMathRandomResultTest() {
|
||||
val iterations = 42
|
||||
val expectedStats = Stats().apply {
|
||||
production = 12970.174f
|
||||
food = -153216.12f
|
||||
gold = 28614.738f
|
||||
science = 142650.89f
|
||||
culture = -45024.03f
|
||||
happiness = -7081.2495f
|
||||
faith = -14933.622f
|
||||
}
|
||||
val stats = statMathRunner(iterations)
|
||||
Assert.assertTrue(stats.equals(expectedStats))
|
||||
}
|
||||
|
||||
private fun statMathRunner(iterations: Int): Stats {
|
||||
val random = Random(42)
|
||||
val statCount = Stat.values().size
|
||||
val stats = Stats()
|
||||
|
||||
for (i in 0 until iterations) {
|
||||
val value: Float = random.nextDouble(-10.0, 10.0).toFloat()
|
||||
stats.add( Stats(gold = value) )
|
||||
stats.toHashMap().forEach {
|
||||
val stat = Stat.values()[(it.key.ordinal + random.nextInt(1,statCount)).rem(statCount)]
|
||||
stats.add(stat, -it.value)
|
||||
}
|
||||
val stat = Stat.values()[random.nextInt(statCount)]
|
||||
stats.add(stat, stats.times(4).get(stat))
|
||||
stats.timesInPlace(0.8f)
|
||||
if (abs(stats.toHashMap().maxOfOrNull { it.value }!!) > 1000000f)
|
||||
stats.timesInPlace(0.1f)
|
||||
}
|
||||
return stats
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user