Another countables test (#13184)

* Faster TestGame class

* Fix fragile unit tests

* Prerequisites for a Countables.FilteredBuildings unit test

* A Countables.FilteredBuildings unit test

* Revert buildingFilter duplicate tagUnique support
This commit is contained in:
SomeTroglodyte 2025-04-11 13:13:20 +02:00 committed by GitHub
parent b0c9295372
commit a6da025705
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 92 additions and 46 deletions

View File

@ -71,11 +71,11 @@ class Nation : RulesetObject() {
override fun getUniqueTarget() = UniqueTarget.Nation override fun getUniqueTarget() = UniqueTarget.Nation
@Transient @Transient
private lateinit var outerColorObject: Color private var outerColorObject = Color.WHITE // Not lateinit for unit tests
fun getOuterColor(): Color = outerColorObject fun getOuterColor(): Color = outerColorObject
@Transient @Transient
private lateinit var innerColorObject: Color private var innerColorObject = Color.BLACK // Not lateinit for unit tests
fun getInnerColor(): Color = innerColorObject fun getInnerColor(): Color = innerColorObject

View File

@ -211,7 +211,9 @@ enum class Countables(
companion object { companion object {
fun getMatching(parameterText: String, ruleset: Ruleset?) = Countables.entries fun getMatching(parameterText: String, ruleset: Ruleset?) = Countables.entries
.filter { .filter {
if (it.matchesWithRuleset) it.matches(parameterText, ruleset!!) else it.matches(parameterText) if (it.matchesWithRuleset)
ruleset != null && it.matches(parameterText, ruleset!!)
else it.matches(parameterText)
} }
fun getCountableAmount(parameterText: String, stateForConditionals: StateForConditionals): Int? { fun getCountableAmount(parameterText: String, stateForConditionals: StateForConditionals): Int? {

View File

@ -389,8 +389,8 @@ class BattleTest {
@Test @Test
fun `should always destroy unit directly hit by nuke`() { fun `should always destroy unit directly hit by nuke`() {
// given // given
val defenderUnit = testGame.addUnit("Warrior", defenderCiv, testGame.getTile(Vector2.Y)) val megaWarrior = testGame.createBaseUnit("Sword").apply { strength = 1_000_000 }
defenderUnit.baseUnit.strength = 1_000_000 val defenderUnit = testGame.addUnit(megaWarrior.name, defenderCiv, testGame.getTile(Vector2.Y))
testGame.addCity(attackerCiv, testGame.getTile(Vector2.Y)) testGame.addCity(attackerCiv, testGame.getTile(Vector2.Y))
val attackerUnit = testGame.addUnit("Atomic Bomb", attackerCiv, testGame.getTile(Vector2.Y)) val attackerUnit = testGame.addUnit("Atomic Bomb", attackerCiv, testGame.getTile(Vector2.Y))

View File

@ -32,7 +32,11 @@ import com.unciv.models.ruleset.unit.BaseUnit
import com.unciv.models.ruleset.unit.Promotion import com.unciv.models.ruleset.unit.Promotion
import com.unciv.models.ruleset.unit.UnitType import com.unciv.models.ruleset.unit.UnitType
class TestGame(overrideRuleset: (() -> Ruleset)? = null) { /**
* A testing game using a fresh clone of the Civ_V_GnK ruleset so it can be modded in-place
* @param addGlobalUniques optional global uniques to add to the ruleset
*/
class TestGame(vararg addGlobalUniques: String) {
private var objectsCreated = 0 private var objectsCreated = 0
val ruleset: Ruleset val ruleset: Ruleset
@ -49,8 +53,14 @@ class TestGame(overrideRuleset: (() -> Ruleset)? = null) {
UncivGame.Current.gameInfo = gameInfo UncivGame.Current.gameInfo = gameInfo
// Create a new ruleset we can easily edit, and set the important variables of gameInfo // Create a new ruleset we can easily edit, and set the important variables of gameInfo
RulesetCache.loadRulesets(noMods = true) if (RulesetCache.isEmpty())
ruleset = overrideRuleset?.invoke() ?: RulesetCache[BaseRuleset.Civ_V_GnK.fullName]!! RulesetCache.loadRulesets(noMods = true)
ruleset = RulesetCache[BaseRuleset.Civ_V_GnK.fullName]!!.clone()
ruleset.globalUniques.uniques.run {
for (unique in addGlobalUniques)
add(unique)
}
gameInfo.ruleset = ruleset gameInfo.ruleset = ruleset
gameInfo.difficulty = "Prince" gameInfo.difficulty = "Prince"
gameInfo.difficultyObject = ruleset.difficulties["Prince"]!! gameInfo.difficultyObject = ruleset.difficulties["Prince"]!!
@ -128,9 +138,8 @@ class TestGame(overrideRuleset: (() -> Ruleset)? = null) {
cities = arrayListOf("The Capital") cities = arrayListOf("The Capital")
this.cityStateType = cityStateType this.cityStateType = cityStateType
} }
val nation = createRulesetObject(ruleset.nations, *uniques) { val nation = createRulesetObject(ruleset.nations, *uniques, factory = ::nationFactory)
nationFactory()
}
val civInfo = Civilization() val civInfo = Civilization()
civInfo.nation = nation civInfo.nation = nation
civInfo.gameInfo = gameInfo civInfo.gameInfo = gameInfo

View File

@ -1,14 +1,13 @@
package com.unciv.uniques package com.unciv.uniques
import com.unciv.models.metadata.BaseRuleset import com.unciv.logic.city.City
import com.unciv.logic.civilization.Civilization
import com.unciv.models.ruleset.Ruleset import com.unciv.models.ruleset.Ruleset
import com.unciv.models.ruleset.RulesetCache
import com.unciv.models.ruleset.unique.Countables import com.unciv.models.ruleset.unique.Countables
import com.unciv.models.ruleset.unique.StateForConditionals import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueParameterType import com.unciv.models.ruleset.unique.UniqueParameterType
import com.unciv.models.ruleset.unique.UniqueTriggerActivation import com.unciv.models.ruleset.unique.UniqueTriggerActivation
import com.unciv.models.ruleset.validation.RulesetErrorList
import com.unciv.models.ruleset.validation.RulesetValidator import com.unciv.models.ruleset.validation.RulesetValidator
import com.unciv.models.stats.Stat import com.unciv.models.stats.Stat
import com.unciv.models.translations.getPlaceholderParameters import com.unciv.models.translations.getPlaceholderParameters
@ -25,9 +24,9 @@ import org.junit.runner.RunWith
@RunWith(GdxTestRunner::class) @RunWith(GdxTestRunner::class)
class CountableTests { class CountableTests {
private var game = TestGame().apply { makeHexagonalMap(3) } private lateinit var game: TestGame
private var civInfo = game.addCiv() private lateinit var civ: Civilization
private var city = game.addCity(civInfo, game.tileMap[2,0]) private lateinit var city: City
@Test @Test
fun testCountableConventions() { fun testCountableConventions() {
@ -87,11 +86,12 @@ class CountableTests {
@Test @Test
fun testPerCountableForGlobalAndLocalResources() { fun testPerCountableForGlobalAndLocalResources() {
setupModdedGame()
// one coal provided locally // one coal provided locally
val providesCoal = game.createBuilding("Provides [1] [Coal]") val providesCoal = game.createBuilding("Provides [1] [Coal]")
city.cityConstructions.addBuilding(providesCoal) city.cityConstructions.addBuilding(providesCoal)
// one globally // one globally
UniqueTriggerActivation.triggerUnique(Unique("Provides [1] [Coal] <for [2] turns>"), civInfo) UniqueTriggerActivation.triggerUnique(Unique("Provides [1] [Coal] <for [2] turns>"), civ)
val providesFaithPerCoal = game.createBuilding("[+1 Faith] [in this city] <for every [Coal]>") val providesFaithPerCoal = game.createBuilding("[+1 Faith] [in this city] <for every [Coal]>")
city.cityConstructions.addBuilding(providesFaithPerCoal) city.cityConstructions.addBuilding(providesFaithPerCoal)
assertEquals(2f, city.cityStats.currentCityStats.faith) assertEquals(2f, city.cityStats.currentCityStats.faith)
@ -99,10 +99,11 @@ class CountableTests {
@Test @Test
fun testStatsCountables() { fun testStatsCountables() {
setupModdedGame()
fun verifyStats(state: StateForConditionals) { fun verifyStats(state: StateForConditionals) {
for (stat in Stat.entries) { for (stat in Stat.entries) {
val countableResult = Countables.Stats.eval(stat.name, state) val countableResult = Countables.Stats.eval(stat.name, state)
val expected = if (stat == Stat.Happiness) civInfo.getHappiness() val expected = if (stat == Stat.Happiness) civ.getHappiness()
else state.getStatAmount(stat) else state.getStatAmount(stat)
assertEquals("Testing $stat countable:", countableResult, expected) assertEquals("Testing $stat countable:", countableResult, expected)
} }
@ -111,21 +112,22 @@ class CountableTests {
val providesStats = val providesStats =
game.createBuilding("[+1 Gold, +2 Food, +3 Production, +4 Happiness, +3 Science, +2 Culture, +1 Faith] [in this city] <when number of [Cities] is equal to [1]>") game.createBuilding("[+1 Gold, +2 Food, +3 Production, +4 Happiness, +3 Science, +2 Culture, +1 Faith] [in this city] <when number of [Cities] is equal to [1]>")
city.cityConstructions.addBuilding(providesStats) city.cityConstructions.addBuilding(providesStats)
verifyStats(StateForConditionals(civInfo, city)) verifyStats(StateForConditionals(civ, city))
val city2 = game.addCity(civInfo, game.tileMap[-2,0]) val city2 = game.addCity(civ, game.tileMap[-2,0])
val providesStats2 = val providesStats2 =
game.createBuilding("[+3 Gold, +2 Food, +1 Production, -4 Happiness, +1 Science, +2 Culture, +3 Faith] [in this city] <when number of [Cities] is more than [1]>") game.createBuilding("[+3 Gold, +2 Food, +1 Production, -4 Happiness, +1 Science, +2 Culture, +3 Faith] [in this city] <when number of [Cities] is more than [1]>")
city2.cityConstructions.addBuilding(providesStats2) city2.cityConstructions.addBuilding(providesStats2)
verifyStats(StateForConditionals(civInfo, city2)) verifyStats(StateForConditionals(civ, city2))
} }
@Test @Test
fun testOwnedTilesCountable() { fun testOwnedTilesCountable() {
UniqueTriggerActivation.triggerUnique(Unique("Turn this tile into a [Coast] tile"), civInfo, tile = game.tileMap[-3,0]) setupModdedGame()
UniqueTriggerActivation.triggerUnique(Unique("Turn this tile into a [Coast] tile"), civInfo, tile = game.tileMap[3,0]) UniqueTriggerActivation.triggerUnique(Unique("Turn this tile into a [Coast] tile"), civ, tile = game.tileMap[-3,0])
UniqueTriggerActivation.triggerUnique(Unique("Turn this tile into a [Coast] tile"), civ, tile = game.tileMap[3,0])
game.addCity(civInfo, game.tileMap[-2,0], initialPopulation = 9) game.addCity(civ, game.tileMap[-2,0], initialPopulation = 9)
val tests = listOf( val tests = listOf(
"Owned [All] Tiles" to 14, "Owned [All] Tiles" to 14,
"Owned [worked] Tiles" to 8, "Owned [worked] Tiles" to 8,
@ -135,16 +137,17 @@ class CountableTests {
"Owned [Farm] Tiles" to 0, "Owned [Farm] Tiles" to 0,
) )
for ((test, expected) in tests) { for ((test, expected) in tests) {
val actual = Countables.getCountableAmount(test, StateForConditionals(civInfo)) val actual = Countables.getCountableAmount(test, StateForConditionals(civ))
assertEquals("Testing `$test` countable:", expected, actual) assertEquals("Testing `$test` countable:", expected, actual)
} }
} }
@Test @Test
fun testFilteredCitiesCountable() { fun testFilteredCitiesCountable() {
UniqueTriggerActivation.triggerUnique(Unique("Turn this tile into a [Coast] tile"), civInfo, tile = game.tileMap[-3,0]) setupModdedGame()
UniqueTriggerActivation.triggerUnique(Unique("Turn this tile into a [Coast] tile"), civ, tile = game.tileMap[-3,0])
val city2 = game.addCity(civInfo, game.tileMap[-2,0], initialPopulation = 9) val city2 = game.addCity(civ, game.tileMap[-2,0], initialPopulation = 9)
city2.isPuppet = true city2.isPuppet = true
val tests = listOf( val tests = listOf(
"[Capital] Cities" to 1, "[Capital] Cities" to 1,
@ -153,11 +156,40 @@ class CountableTests {
"[Your] Cities" to 2, "[Your] Cities" to 2,
) )
for ((test, expected) in tests) { for ((test, expected) in tests) {
val actual = Countables.getCountableAmount(test, StateForConditionals(civInfo)) val actual = Countables.getCountableAmount(test, StateForConditionals(civ))
assertEquals("Testing `$test` countable:", expected, actual) assertEquals("Testing `$test` countable:", expected, actual)
} }
} }
@Test
fun testFilteredBuildingsCountable () {
setupModdedGame(withCiv = false)
val building = game.createBuilding("Ancestor Tree") // That's a filtering Unique, not the building name
building[Stat.Culture] = 1f
civ = game.addCiv(
"[+1 Culture] from all [Ancestor Tree] buildings <for every [1] [[Ancestor Tree] Buildings]> <when number of [[Ancestor Tree] Buildings] is more than [1]>",
"[+50]% [Culture] from every [Ancestor Tree] <when number of [[Ancestor Tree] Buildings] is more than [0]>",
)
city = game.addCity(civ, game.tileMap[2,0])
city.cityConstructions.addBuilding(building)
val city2 = game.addCity(civ, game.tileMap[-2,0])
city2.cityConstructions.addBuilding(building)
// updateStatsForNextTurn won't run the city part because no happiness change across a boundary
for (city3 in civ.cities)
city3.cityStats.update(updateCivStats = false)
civ.updateStatsForNextTurn()
// Expect: (1 Palace + 1 Base Ancestor Tree + 2 for-every) * 1.5 = 6
val capitalCulture = city.cityStats.currentCityStats.culture
// Expect: capitalCulture + (1 Base Ancestor Tree + 2 for-every) * 1.5 = 10.5
val civCulture = civ.stats.statsForNextTurn.culture
assertEquals(6f, capitalCulture, 0.005f)
assertEquals(10.5f, civCulture, 0.005f)
}
@Test @Test
fun testRulesetValidation() { fun testRulesetValidation() {
/** These are `Pair<String, Int>` with the second being the expected number of parameters to fail UniqueParameterType validation */ /** These are `Pair<String, Int>` with the second being the expected number of parameters to fail UniqueParameterType validation */
@ -190,7 +222,10 @@ class CountableTests {
val totalNotACountableExpected = testData.sumOf { it.second } val totalNotACountableExpected = testData.sumOf { it.second }
val notACountableRegex = Regex(""".*parameter "(.*)" which does not fit parameter type countable.*""") val notACountableRegex = Regex(""".*parameter "(.*)" which does not fit parameter type countable.*""")
val ruleset = setupModdedGame(*testData.map { it.first }.toTypedArray()) val ruleset = setupModdedGame(
*testData.map { it.first }.toTypedArray(),
withCiv = false // City founding would only slow this down
)
ruleset.modOptions.isBaseRuleset = true // To get ruleset-specific validation ruleset.modOptions.isBaseRuleset = true // To get ruleset-specific validation
val errors = RulesetValidator(ruleset).getErrorList() val errors = RulesetValidator(ruleset).getErrorList()
@ -235,28 +270,21 @@ class CountableTests {
"[+1 Happiness] <for every [[42] Monkeys]>", "[+1 Happiness] <for every [[42] Monkeys]>",
) )
game.makeHexagonalMap(3)
civInfo = game.addCiv()
city = game.addCity(civInfo, game.tileMap[2,0])
val cityState = game.addCiv(cityStateType = game.ruleset.cityStateTypes.keys.first()) val cityState = game.addCiv(cityStateType = game.ruleset.cityStateTypes.keys.first())
game.addCity(cityState, game.tileMap[-2,0], true) game.addCity(cityState, game.tileMap[-2,0], true)
civInfo.updateStatsForNextTurn() civ.updateStatsForNextTurn()
val happiness = Countables.getCountableAmount("Happiness", StateForConditionals(civInfo, city)) val happiness = Countables.getCountableAmount("Happiness", StateForConditionals(civ, city))
// Base 9, -1 city, -3 population +1 deprecated countable should still work, but the bogus one should not // Base 9, -1 city, -3 population +1 deprecated countable should still work, but the bogus one should not
assertEquals("Testing Happiness", 6, happiness) assertEquals("Testing Happiness", 6, happiness)
} }
private fun setupModdedGame(vararg uniques: String): Ruleset { private fun setupModdedGame(vararg addGlobalUniques: String, withCiv: Boolean = true): Ruleset {
val mod = Ruleset() game = TestGame(*addGlobalUniques)
mod.name = "Testing" game.makeHexagonalMap(3)
for (unique in uniques) if (!withCiv) return game.ruleset
mod.globalUniques.uniques.add(unique) civ = game.addCiv()
game = TestGame { city = game.addCity(civ, game.tileMap[2,0])
RulesetCache[mod.name] = mod
RulesetCache.getComplexRuleset(RulesetCache[BaseRuleset.Civ_V_GnK.fullName]!!, listOf(mod))
}
return game.ruleset return game.ruleset
} }
} }

View File

@ -1,8 +1,10 @@
package com.unciv.uniques package com.unciv.uniques
import com.badlogic.gdx.math.Vector2 import com.badlogic.gdx.math.Vector2
import com.unciv.json.json
import com.unciv.logic.map.mapunit.UnitTurnManager import com.unciv.logic.map.mapunit.UnitTurnManager
import com.unciv.models.UnitActionType import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.UniqueType import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.fillPlaceholders import com.unciv.models.translations.fillPlaceholders
import com.unciv.testing.GdxTestRunner import com.unciv.testing.GdxTestRunner
@ -52,11 +54,14 @@ class UnitUniquesTests {
@Test @Test
fun canConstructResourceRequiringImprovement() { fun canConstructResourceRequiringImprovement() {
// Do this early so the uniqueObjects lazy is still un-triggered // Do this early so the uniqueObjects lazy is still un-triggered
val improvement = game.ruleset.tileImprovements["Manufactory"]!!
val requireUnique = UniqueType.ConsumesResources.text.fillPlaceholders("3", "Iron") val requireUnique = UniqueType.ConsumesResources.text.fillPlaceholders("3", "Iron")
// Get a clone with lazies un-tripped
val oldImprovement = game.ruleset.tileImprovements["Manufactory"]!!
val improvement = json().run { fromJson(TileImprovement::class.java, toJson(oldImprovement)) }
improvement.uniques.add(requireUnique) improvement.uniques.add(requireUnique)
Assert.assertFalse("Test preparation failed to add ConsumesResources to Manufactory", Assert.assertFalse("Test preparation failed to add ConsumesResources to Manufactory",
improvement.uniqueObjects.none { it.type == UniqueType.ConsumesResources }) improvement.uniqueObjects.none { it.type == UniqueType.ConsumesResources })
game.ruleset.tileImprovements["Manufactory"] = improvement
game.makeHexagonalMap(1) game.makeHexagonalMap(1)
val civ = game.addCiv(isPlayer = true) val civ = game.addCiv(isPlayer = true)
@ -72,6 +77,7 @@ class UnitUniquesTests {
} catch (ex: Throwable) { } catch (ex: Throwable) {
// Give that IndexOutOfBoundsException a nicer name // Give that IndexOutOfBoundsException a nicer name
Assert.fail("getImprovementConstructionActions throws Exception ${ex.javaClass.simpleName}") Assert.fail("getImprovementConstructionActions throws Exception ${ex.javaClass.simpleName}")
game.ruleset.tileImprovements["Manufactory"] = oldImprovement
return return
}.filter { it.action != null } }.filter { it.action != null }
Assert.assertTrue("Great Engineer should NOT be able to create a Manufactory modded to require Iron with 0 Iron", Assert.assertTrue("Great Engineer should NOT be able to create a Manufactory modded to require Iron with 0 Iron",
@ -94,6 +100,7 @@ class UnitUniquesTests {
.filter { it.action != null } .filter { it.action != null }
Assert.assertFalse("Great Engineer SHOULD be able to create a Manufactory modded to require Iron once Iron is available", Assert.assertFalse("Great Engineer SHOULD be able to create a Manufactory modded to require Iron once Iron is available",
actionsWithIron.none()) actionsWithIron.none())
game.ruleset.tileImprovements["Manufactory"] = oldImprovement
} }
@Test @Test