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
@Transient
private lateinit var outerColorObject: Color
private var outerColorObject = Color.WHITE // Not lateinit for unit tests
fun getOuterColor(): Color = outerColorObject
@Transient
private lateinit var innerColorObject: Color
private var innerColorObject = Color.BLACK // Not lateinit for unit tests
fun getInnerColor(): Color = innerColorObject

View File

@ -211,7 +211,9 @@ enum class Countables(
companion object {
fun getMatching(parameterText: String, ruleset: Ruleset?) = Countables.entries
.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? {

View File

@ -389,8 +389,8 @@ class BattleTest {
@Test
fun `should always destroy unit directly hit by nuke`() {
// given
val defenderUnit = testGame.addUnit("Warrior", defenderCiv, testGame.getTile(Vector2.Y))
defenderUnit.baseUnit.strength = 1_000_000
val megaWarrior = testGame.createBaseUnit("Sword").apply { strength = 1_000_000 }
val defenderUnit = testGame.addUnit(megaWarrior.name, defenderCiv, testGame.getTile(Vector2.Y))
testGame.addCity(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.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
val ruleset: Ruleset
@ -49,8 +53,14 @@ class TestGame(overrideRuleset: (() -> Ruleset)? = null) {
UncivGame.Current.gameInfo = gameInfo
// Create a new ruleset we can easily edit, and set the important variables of gameInfo
RulesetCache.loadRulesets(noMods = true)
ruleset = overrideRuleset?.invoke() ?: RulesetCache[BaseRuleset.Civ_V_GnK.fullName]!!
if (RulesetCache.isEmpty())
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.difficulty = "Prince"
gameInfo.difficultyObject = ruleset.difficulties["Prince"]!!
@ -128,9 +138,8 @@ class TestGame(overrideRuleset: (() -> Ruleset)? = null) {
cities = arrayListOf("The Capital")
this.cityStateType = cityStateType
}
val nation = createRulesetObject(ruleset.nations, *uniques) {
nationFactory()
}
val nation = createRulesetObject(ruleset.nations, *uniques, factory = ::nationFactory)
val civInfo = Civilization()
civInfo.nation = nation
civInfo.gameInfo = gameInfo

View File

@ -1,14 +1,13 @@
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.RulesetCache
import com.unciv.models.ruleset.unique.Countables
import com.unciv.models.ruleset.unique.StateForConditionals
import com.unciv.models.ruleset.unique.Unique
import com.unciv.models.ruleset.unique.UniqueParameterType
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.stats.Stat
import com.unciv.models.translations.getPlaceholderParameters
@ -25,9 +24,9 @@ import org.junit.runner.RunWith
@RunWith(GdxTestRunner::class)
class CountableTests {
private var game = TestGame().apply { makeHexagonalMap(3) }
private var civInfo = game.addCiv()
private var city = game.addCity(civInfo, game.tileMap[2,0])
private lateinit var game: TestGame
private lateinit var civ: Civilization
private lateinit var city: City
@Test
fun testCountableConventions() {
@ -87,11 +86,12 @@ class CountableTests {
@Test
fun testPerCountableForGlobalAndLocalResources() {
setupModdedGame()
// one coal provided locally
val providesCoal = game.createBuilding("Provides [1] [Coal]")
city.cityConstructions.addBuilding(providesCoal)
// 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]>")
city.cityConstructions.addBuilding(providesFaithPerCoal)
assertEquals(2f, city.cityStats.currentCityStats.faith)
@ -99,10 +99,11 @@ class CountableTests {
@Test
fun testStatsCountables() {
setupModdedGame()
fun verifyStats(state: StateForConditionals) {
for (stat in Stat.entries) {
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)
assertEquals("Testing $stat countable:", countableResult, expected)
}
@ -111,21 +112,22 @@ class CountableTests {
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]>")
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 =
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)
verifyStats(StateForConditionals(civInfo, city2))
verifyStats(StateForConditionals(civ, city2))
}
@Test
fun testOwnedTilesCountable() {
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"), civInfo, tile = game.tileMap[3,0])
setupModdedGame()
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(
"Owned [All] Tiles" to 14,
"Owned [worked] Tiles" to 8,
@ -135,16 +137,17 @@ class CountableTests {
"Owned [Farm] Tiles" to 0,
)
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)
}
}
@Test
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
val tests = listOf(
"[Capital] Cities" to 1,
@ -153,11 +156,40 @@ class CountableTests {
"[Your] Cities" to 2,
)
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)
}
}
@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
fun testRulesetValidation() {
/** 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 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
val errors = RulesetValidator(ruleset).getErrorList()
@ -235,28 +270,21 @@ class CountableTests {
"[+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())
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
assertEquals("Testing Happiness", 6, happiness)
}
private fun setupModdedGame(vararg uniques: String): Ruleset {
val mod = Ruleset()
mod.name = "Testing"
for (unique in uniques)
mod.globalUniques.uniques.add(unique)
game = TestGame {
RulesetCache[mod.name] = mod
RulesetCache.getComplexRuleset(RulesetCache[BaseRuleset.Civ_V_GnK.fullName]!!, listOf(mod))
}
private fun setupModdedGame(vararg addGlobalUniques: String, withCiv: Boolean = true): Ruleset {
game = TestGame(*addGlobalUniques)
game.makeHexagonalMap(3)
if (!withCiv) return game.ruleset
civ = game.addCiv()
city = game.addCity(civ, game.tileMap[2,0])
return game.ruleset
}
}

View File

@ -1,8 +1,10 @@
package com.unciv.uniques
import com.badlogic.gdx.math.Vector2
import com.unciv.json.json
import com.unciv.logic.map.mapunit.UnitTurnManager
import com.unciv.models.UnitActionType
import com.unciv.models.ruleset.tile.TileImprovement
import com.unciv.models.ruleset.unique.UniqueType
import com.unciv.models.translations.fillPlaceholders
import com.unciv.testing.GdxTestRunner
@ -52,11 +54,14 @@ class UnitUniquesTests {
@Test
fun canConstructResourceRequiringImprovement() {
// 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")
// 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)
Assert.assertFalse("Test preparation failed to add ConsumesResources to Manufactory",
improvement.uniqueObjects.none { it.type == UniqueType.ConsumesResources })
game.ruleset.tileImprovements["Manufactory"] = improvement
game.makeHexagonalMap(1)
val civ = game.addCiv(isPlayer = true)
@ -72,6 +77,7 @@ class UnitUniquesTests {
} catch (ex: Throwable) {
// Give that IndexOutOfBoundsException a nicer name
Assert.fail("getImprovementConstructionActions throws Exception ${ex.javaClass.simpleName}")
game.ruleset.tileImprovements["Manufactory"] = oldImprovement
return
}.filter { it.action != null }
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 }
Assert.assertFalse("Great Engineer SHOULD be able to create a Manufactory modded to require Iron once Iron is available",
actionsWithIron.none())
game.ruleset.tileImprovements["Manufactory"] = oldImprovement
}
@Test