MultiFilter gets 'or' capability

This commit is contained in:
SomeTroglodyte
2024-04-22 01:43:08 +02:00
parent 24e2ea1c5f
commit 9f6e7c794a
4 changed files with 53 additions and 5 deletions

View File

@ -1583,7 +1583,9 @@ Resource [resource] does not exist in ruleset! =
Improvement [improvement] does not exist in ruleset! =
Nation [nation] does not exist in ruleset! =
Natural Wonder [naturalWonder] does not exist in ruleset! =
non-[filter] =
[filter1] or [filter2] =
# Civilopedia difficulty levels
Player settings =

View File

@ -6,12 +6,16 @@ object MultiFilter {
private const val andSuffix = "}"
private const val notPrefix = "non-["
private const val notSuffix = "]"
private const val orPrefix = "["
private const val orSeparator = "] or ["
private const val orSuffix = "]"
/**
* Implements `and` and `not` logic on top of a [filterFunction].
* Implements `and`, `or` and `not` logic on top of a [filterFunction].
*
* Syntax:
* - `and`: `{filter1} {filter2}`... (can repeat as needed)
* - `or`: `[filter1] or [filter2]`... (can repeat as needed)
* - `not`: `non-[filter]`
* @param input The complex filtering term
* @param filterFunction The single filter implementation
@ -25,6 +29,9 @@ object MultiFilter {
if (input.hasSurrounding(andPrefix, andSuffix) && input.contains(andSeparator))
return input.removeSurrounding(andPrefix, andSuffix).split(andSeparator)
.all { multiFilter(it, filterFunction, forUniqueValidityTests) }
if (input.hasSurrounding(orPrefix, orSuffix) && input.contains(orSeparator))
return input.removeSurrounding(orPrefix, orSuffix).split(orSeparator)
.any { multiFilter(it, filterFunction, forUniqueValidityTests) }
if (input.hasSurrounding(notPrefix, notSuffix)) {
//same as `return multiFilter() == forUniqueValidityTests`, but clearer
val internalResult = multiFilter(input.removeSurrounding(notPrefix, notSuffix), filterFunction, forUniqueValidityTests)
@ -39,6 +46,11 @@ object MultiFilter {
input.removeSurrounding(andPrefix, andSuffix)
.splitToSequence(andSeparator)
.flatMap { getAllSingleFilters(it) }
input.hasSurrounding(orPrefix, orSuffix) && input.contains(orSeparator) ->
// Resolve "OR" filters
input.removeSurrounding(orPrefix, orSuffix)
.splitToSequence(orSeparator)
.flatMap { getAllSingleFilters(it) }
input.hasSurrounding(notPrefix, notSuffix) ->
// Simply remove "non" syntax
getAllSingleFilters(input.removeSurrounding(notPrefix, notSuffix))

View File

@ -11,22 +11,29 @@ Note that all of these are case-sensitive!
## General Filter Rules
All filters except for `populationFilter` accept multiple values in the format: `{A} {B} {C}` etc, meaning "the object must match ALL of these filters"
All filters except for `populationFilter` accept combining sub-filters with 'and', 'or' or 'not' logic.
'And' logic combines multiple values in the format: `{A} {B} {C}` etc, meaning "the object must match ALL of these filters".
> Example: `[{Military} {Water}] units`, `[{Wounded} {Armor}] units`, etc.
No space or other text is allowed between the `[` and the first `{`, nor between the last `}` and the ending `]`. The space in `} {`, however, is mandatory.
All filters accept `non-[filter]` as a possible value
'Or' logic combines multiple values in the format: `[A] or [B] or [C]` etc, meaning "the object must match ANY of these filters".
> Example: `[[Water] or [Helicopter]] units`, `[[Puppeted] or [Razing]] cities`, etc.
'Not' logic uses the syntax `non-[filter]`.
> Example: `[non-[Wounded]] units`
These can be combined by nesting, with the exception that an "ALL" filter cannot contain another "ALL" filter, even with a NON-filter in between.
These can be combined by nesting, with the exception that an "ALL" filter cannot contain another "ALL" filter, or an "ANY" filter cannot contain another "ANY" filter, even with other logic types in between.
> Example: `[{non-[Wounded]} {Armor}] units` means unit is type Armor and at full health.
> Example: `[non-[{Wounded} {Armor}]] units` means unit is neither wounded nor an Armor one.
> Example: `[[{Wounded} {Water}] or [{Embarked} {Scout}]] units`
`[{non-[{Wounded} {Armor}]} {Embarked}] units` WILL FAIL because the game will treat both "} {" at the same time and see `non-[{Wounded` and `Armor}]`, both invalid.
However, `[{non-[{Wounded} {Armor}]} {Embarked}] units` WILL FAIL because the game will treat both "} {" at the same time and see `non-[{Wounded` and `Armor}]`, both invalid.
Display of complex filters in Civilopedia may become unreadable. If so, consider hiding that unique and provide a better wording using the `Comment []` unique separately.

View File

@ -62,6 +62,20 @@ class MultiFilterTests {
Assert.assertNull(UniqueParameterType.MapUnitFilter.getErrorSeverity("{Wounded} {Melee} {Land}", game.ruleset))
}
@Test
fun testOrLogic() {
Assert.assertTrue(MultiFilter.multiFilter("[A] or [B]", { it=="A"}))
Assert.assertTrue(MultiFilter.multiFilter("[A] or [B]", { it=="B"}))
Assert.assertFalse(MultiFilter.multiFilter("[A] or [B]", { it=="C"}))
}
@Test
fun testAndNestedInOrLogic() {
Assert.assertTrue(MultiFilter.multiFilter("[{A} {B}] or [{C} {D}]", { it=="A" || it == "B"}))
Assert.assertTrue(MultiFilter.multiFilter("[{A} {B}] or [{C} {D}]", { it=="C" || it == "D"}))
Assert.assertFalse(MultiFilter.multiFilter("[{A} {B}] or [{C} {D}]", { it=="A" || it == "C"}))
}
@Test
fun `test a complete Unique with a complex multi-filter is parsed and validated correctly`() {
val text = "Only available <if [Colosseum] is constructed in all [non-[{non-[Resisting]} {non-[Razing]} {non-[Coastal]}]] cities>"
@ -109,4 +123,17 @@ class MultiFilterTests {
city.isPuppet = true
Assert.assertFalse(Conditionals.conditionalApplies(null, conditional, stateForConditionals))
}
@Test
fun `test cityFilter nesting 'or' logic in 'and' logic`() {
val condition = "in [{[Puppeted] or [Razing]} {[Coastal] or [non-[Garrisoned]]}] cities"
val conditional = Unique(condition)
Assert.assertFalse(Conditionals.conditionalApplies(null, conditional, stateForConditionals)) // only [non-[Garrisoned]] is true
city.isPuppet = true
Assert.assertTrue(Conditionals.conditionalApplies(null, conditional, stateForConditionals)) // Puppeted fulfills left of AND, no garrison right
game.addUnit("Warrior", civ, game.getTile(Vector2.Zero)).fortify()
Assert.assertFalse(Conditionals.conditionalApplies(null, conditional, stateForConditionals)) // Adding garrison will make right term false
}
}