From 11be6e280466bb8f74b19b8da6b9d284bd206e2a Mon Sep 17 00:00:00 2001 From: WhoIsJohannes <126110113+WhoIsJohannes@users.noreply.github.com> Date: Tue, 21 Mar 2023 13:38:22 +0100 Subject: [PATCH] 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 --- core/src/com/unciv/json/UncivJson.kt | 2 + .../logic/civilization/CivRankingHistory.kt | 57 +++++++++++++++++++ .../unciv/logic/civilization/Civilization.kt | 3 + .../civilization/managers/TurnManager.kt | 4 ++ .../ui/screens/victoryscreen/RankingType.kt | 30 ++++++---- .../screens/victoryscreen/RankingTypeTest.kt | 22 +++++++ 6 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 core/src/com/unciv/logic/civilization/CivRankingHistory.kt create mode 100644 tests/src/com/unciv/ui/screens/victoryscreen/RankingTypeTest.kt diff --git a/core/src/com/unciv/json/UncivJson.kt b/core/src/com/unciv/json/UncivJson.kt index 7ab751da00..1470b741c0 100644 --- a/core/src/com/unciv/json/UncivJson.kt +++ b/core/src/com/unciv/json/UncivJson.kt @@ -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()) } /** diff --git a/core/src/com/unciv/logic/civilization/CivRankingHistory.kt b/core/src/com/unciv/logic/civilization/CivRankingHistory.kt new file mode 100644 index 0000000000..6e2d66282d --- /dev/null +++ b/core/src/com/unciv/logic/civilization/CivRankingHistory.kt @@ -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>(), + 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 { + 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() + for (rankingEntry in entry) { + val rankingType = RankingType.fromIdForSerialization(rankingEntry.name) + ?: continue // Silently drop unknown ranking types. + rankings[rankingType] = rankingEntry.asInt() + } + this[turn] = rankings + } + } + } +} diff --git a/core/src/com/unciv/logic/civilization/Civilization.kt b/core/src/com/unciv/logic/civilization/Civilization.kt index 78c17553a5..70d7aa621a 100644 --- a/core/src/com/unciv/logic/civilization/Civilization.kt +++ b/core/src/com/unciv/logic/civilization/Civilization.kt @@ -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 } diff --git a/core/src/com/unciv/logic/civilization/managers/TurnManager.kt b/core/src/com/unciv/logic/civilization/managers/TurnManager.kt index 9243e44490..08044fd8c0 100644 --- a/core/src/com/unciv/logic/civilization/managers/TurnManager.kt +++ b/core/src/com/unciv/logic/civilization/managers/TurnManager.kt @@ -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 diff --git a/core/src/com/unciv/ui/screens/victoryscreen/RankingType.kt b/core/src/com/unciv/ui/screens/victoryscreen/RankingType.kt index 6ce8dd5611..0f7fc56d1f 100644 --- a/core/src/com/unciv/ui/screens/victoryscreen/RankingType.kt +++ b/core/src/com/unciv/ui/screens/victoryscreen/RankingType.kt @@ -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 } } +} diff --git a/tests/src/com/unciv/ui/screens/victoryscreen/RankingTypeTest.kt b/tests/src/com/unciv/ui/screens/victoryscreen/RankingTypeTest.kt new file mode 100644 index 0000000000..1f143f2d0d --- /dev/null +++ b/tests/src/com/unciv/ui/screens/victoryscreen/RankingTypeTest.kt @@ -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() + for (rankingType in RankingType.values()) { + val id = rankingType.idForSerialization + Assert.assertTrue( + "Id $id for RankingType $rankingType is not unique", + uniqueIds.add(id) + ) + } + } +}