Silently log ranking stats for each major civ every turn (#8964)

* Record each stat each round for each civilization.

* Implement custom serialization and encapsulate logic in separate CivRankingHistory.kt

* Address comments

* Address comments and add RankingTypeTest.kt
This commit is contained in:
WhoIsJohannes
2023-03-21 13:38:22 +01:00
committed by GitHub
parent 4ba6574419
commit 11be6e2804
6 changed files with 107 additions and 11 deletions

View File

@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx
import com.badlogic.gdx.files.FileHandle
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.SerializationException
import com.unciv.logic.civilization.CivRankingHistory
import com.unciv.logic.map.tile.TileHistory
import com.unciv.ui.components.KeyCharAndCode
import com.unciv.ui.components.KeyboardBindings
@ -27,6 +28,7 @@ fun json() = Json().apply {
setSerializer(KeyCharAndCode::class.java, KeyCharAndCode.Serializer())
setSerializer(KeyboardBindings::class.java, KeyboardBindings.Serializer())
setSerializer(TileHistory::class.java, TileHistory.Serializer())
setSerializer(CivRankingHistory::class.java, CivRankingHistory.Serializer())
}
/**

View File

@ -0,0 +1,57 @@
package com.unciv.logic.civilization
import com.badlogic.gdx.utils.Json
import com.badlogic.gdx.utils.JsonValue
import com.unciv.logic.IsPartOfGameInfoSerialization
import com.unciv.ui.screens.victoryscreen.RankingType
/** Records for each turn (key of outer map) what the score (value of inner map) was for each RankingType. */
open class CivRankingHistory : HashMap<Int, Map<RankingType, Int>>(),
IsPartOfGameInfoSerialization {
/**
* Returns a shallow copy of this [CivRankingHistory] instance.
* The inner [Map] instances are not cloned, only their references are copied.
*/
override fun clone(): CivRankingHistory {
val toReturn = CivRankingHistory()
toReturn.putAll(this)
return toReturn
}
fun recordRankingStats(civilization: Civilization) {
this[civilization.gameInfo.turns] =
RankingType.values().associateWith { civilization.getStatForRanking(it) }
}
/** Custom Json formatter for a [CivRankingHistory].
* Output looks like this: `statsHistory:{0:{S:50,G:120,...},1:{S:55,G:80,...}}`
*/
class Serializer : Json.Serializer<CivRankingHistory> {
override fun write(json: Json, `object`: CivRankingHistory, knownType: Class<*>?) {
json.writeObjectStart()
for ((turn, rankings) in `object`) {
json.writeObjectStart(turn.toString())
for ((rankingType, score) in rankings) {
json.writeValue(rankingType.idForSerialization, score)
}
json.writeObjectEnd()
}
json.writeObjectEnd()
}
override fun read(json: Json, jsonData: JsonValue, type: Class<*>?) =
CivRankingHistory().apply {
for (entry in jsonData) {
val turn = entry.name.toInt()
val rankings = mutableMapOf<RankingType, Int>()
for (rankingEntry in entry) {
val rankingType = RankingType.fromIdForSerialization(rankingEntry.name)
?: continue // Silently drop unknown ranking types.
rankings[rankingType] = rankingEntry.asInt()
}
this[turn] = rankings
}
}
}
}

View File

@ -237,6 +237,8 @@ class Civilization : IsPartOfGameInfoSerialization {
@Transient
var hasLongCountDisplayUnique = false
var statsHistory = CivRankingHistory()
constructor()
constructor(civName: String) {
@ -285,6 +287,7 @@ class Civilization : IsPartOfGameInfoSerialization {
toReturn.totalFaithForContests = totalFaithForContests
toReturn.attacksSinceTurnStart = attacksSinceTurnStart.copy()
toReturn.hasMovedAutomatedUnits = hasMovedAutomatedUnits
toReturn.statsHistory = statsHistory.clone()
return toReturn
}

View File

@ -26,6 +26,10 @@ class TurnManager(val civInfo: Civilization) {
fun startTurn() {
if (civInfo.isMajorCiv() && civInfo.isAlive()) {
civInfo.statsHistory.recordRankingStats(civInfo)
}
civInfo.civConstructions.startTurn()
civInfo.attacksSinceTurnStart.clear()
civInfo.updateStatsForNextTurn() // for things that change when turn passes e.g. golden age, city state influence

View File

@ -4,16 +4,24 @@ import com.badlogic.gdx.graphics.Color
import com.badlogic.gdx.scenes.scene2d.ui.Image
import com.unciv.ui.images.ImageGetter
enum class RankingType(val getImage: ()->Image?) {
enum class RankingType(val getImage: () -> Image?, val idForSerialization: String) {
// production, gold, happiness, and culture already have icons added when the line is `tr()`anslated
Score({ ImageGetter.getImage("CityStateIcons/Cultured").apply { color = Color.FIREBRICK } }),
Population({ ImageGetter.getStatIcon("Population") }),
Crop_Yield({ ImageGetter.getStatIcon("Food") }),
Production({ null }),
Gold({ null }),
Territory({ ImageGetter.getImage("OtherIcons/Hexagon") }),
Force({ ImageGetter.getImage("OtherIcons/Shield") }),
Happiness({ null }),
Technologies({ ImageGetter.getStatIcon("Science") }),
Culture({ null })
Score(
{ ImageGetter.getImage("CityStateIcons/Cultured").apply { color = Color.FIREBRICK } },
"S"
),
Population({ ImageGetter.getStatIcon("Population") }, "N"),
Crop_Yield({ ImageGetter.getStatIcon("Food") }, "C"),
Production({ null }, "P"),
Gold({ null }, "G"),
Territory({ ImageGetter.getImage("OtherIcons/Hexagon") }, "T"),
Force({ ImageGetter.getImage("OtherIcons/Shield") }, "F"),
Happiness({ null }, "H"),
Technologies({ ImageGetter.getStatIcon("Science") }, "W"),
Culture({ null }, "A");
companion object {
fun fromIdForSerialization(s: String): RankingType? =
values().firstOrNull { it.idForSerialization == s }
}
}

View File

@ -0,0 +1,22 @@
package com.unciv.ui.screens.victoryscreen
import com.unciv.testing.GdxTestRunner
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(GdxTestRunner::class)
class RankingTypeTests {
@Test
fun checkIdForSerializationUniqueness() {
val uniqueIds = HashSet<String>()
for (rankingType in RankingType.values()) {
val id = rankingType.idForSerialization
Assert.assertTrue(
"Id $id for RankingType $rankingType is not unique",
uniqueIds.add(id)
)
}
}
}