From 394ddf01adf0ed23e13d0fe29e480578f99b02c4 Mon Sep 17 00:00:00 2001 From: Quillraven Date: Fri, 13 Dec 2024 07:23:45 +0100 Subject: [PATCH] add additional family bag functions: - containsAll - all - any - none - filter - filterNot - filterIndexed - filterTo - filterNotTo - filterIndexedTo - find - groupBy - groupByTo - map - mapIndexed - mapTo - mapIndexedTo - mapNotNull - mapNotNullTo - partition - partitionTo - random - randomOrNull - single - singleOrNull - take --- .../com/github/quillraven/fleks/family.kt | 200 ++++++++++++ .../fleks/FamilyBagFunctionsTest.kt | 299 ++++++++++++++++++ 2 files changed, 499 insertions(+) create mode 100644 src/commonTest/kotlin/com/github/quillraven/fleks/FamilyBagFunctionsTest.kt diff --git a/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt b/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt index 8a6a7c4..d093782 100644 --- a/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt +++ b/src/commonMain/kotlin/com/github/quillraven/fleks/family.kt @@ -165,6 +165,21 @@ data class Family( */ operator fun contains(entity: Entity): Boolean = activeEntities.hasValueAtIndex(entity.id) + /** + * Returns true if and only if all given [entities] are part of the family. + */ + fun containsAll(entities: Collection): Boolean = mutableEntities.containsAll(entities) + + /** + * Returns true if and only if all given [entities] are part of the family. + */ + fun containsAll(entities: EntityBag): Boolean = mutableEntities.containsAll(entities) + + /** + * Returns true if and only if all entities of the given [family] are part of this family. + */ + fun containsAll(family: Family): Boolean = mutableEntities.containsAll(family.entities) + /** * Updates this family if needed and runs the given [action] for all [entities][Entity]. * @@ -224,6 +239,191 @@ data class Family( */ fun sort(comparator: EntityComparator) = mutableEntities.sort(comparator) + /** + * Returns true if all [entities][Entity] of the family match the given [predicate]. + */ + fun all(predicate: (Entity) -> Boolean): Boolean = mutableEntities.all(predicate) + + /** + * Returns true if at least one [entity][Entity] of the family matches the given [predicate]. + */ + fun any(predicate: (Entity) -> Boolean): Boolean = mutableEntities.any(predicate) + + /** + * Returns true if no [entity][Entity] of the family matches the given [predicate]. + */ + fun none(predicate: (Entity) -> Boolean): Boolean = mutableEntities.none(predicate) + + /** + * Returns a [List] containing only [entities][Entity] matching the given [predicate]. + */ + fun filter(predicate: (Entity) -> Boolean): EntityBag = mutableEntities.filter(predicate) + + /** + * Returns a [List] containing all [entities][Entity] not matching the given [predicate]. + */ + fun filterNot(predicate: (Entity) -> Boolean): EntityBag = mutableEntities.filterNot(predicate) + + /** + * Returns a [List] containing only [entities][Entity] matching the given [predicate]. + */ + fun filterIndexed(predicate: (index: Int, Entity) -> Boolean): EntityBag = mutableEntities.filterIndexed(predicate) + + /** + * Appends all [entities][Entity] matching the given [predicate] to the given [destination]. + */ + fun filterTo(destination: MutableEntityBag, predicate: (Entity) -> Boolean): MutableEntityBag = + mutableEntities.filterTo(destination, predicate) + + /** + * Appends all [entities][Entity] not matching the given [predicate] to the given [destination]. + */ + fun filterNotTo(destination: MutableEntityBag, predicate: (Entity) -> Boolean): MutableEntityBag = + mutableEntities.filterNotTo(destination, predicate) + + /** + * Appends all [entities][Entity] matching the given [predicate] to the given [destination]. + */ + fun filterIndexedTo( + destination: MutableEntityBag, + predicate: (index: Int, Entity) -> Boolean + ): MutableEntityBag = mutableEntities.filterIndexedTo(destination, predicate) + + /** + * Returns the first [entity][Entity] matching the given [predicate], or null if no such + * [entity][Entity] was found. + */ + fun find(predicate: (Entity) -> Boolean): Entity? = mutableEntities.find(predicate) + + /** + * Groups [entities][Entity] by the key returned by the given [keySelector] function + * applied to each [entity][Entity] and returns a map where each group key is associated with an [EntityBag] + * of corresponding [entities][Entity]. + */ + fun groupBy(keySelector: (Entity) -> K): Map = mutableEntities.groupBy(keySelector) + + /** + * Groups values returned by the [valueTransform] function applied to each [entity][Entity] of the family + * by the key returned by the given [keySelector] function applied to the [entity][Entity] and returns + * a map where each group key is associated with a list of corresponding values. + */ + fun groupBy(keySelector: (Entity) -> K, valueTransform: (Entity) -> V): Map> = + mutableEntities.groupBy(keySelector, valueTransform) + + /** + * Groups [entities][Entity] by the key returned by the given [keySelector] function + * applied to each [entity][Entity] and puts to the [destination] map each group key associated with + * an [EntityBag] of corresponding elements. + */ + fun > groupByTo(destination: M, keySelector: (Entity) -> K): M = + mutableEntities.groupByTo(destination, keySelector) + + /** + * Groups values returned by the [valueTransform] function applied to each [entity][Entity] of the family + * by the key returned by the given [keySelector] function applied to the [entity][Entity] and puts + * to the [destination] map each group key associated with a list of corresponding values. + */ + fun >> groupByTo( + destination: M, + keySelector: (Entity) -> K, + valueTransform: (Entity) -> V + ): M = mutableEntities.groupByTo(destination, keySelector, valueTransform) + + /** + * Returns a [List] containing the results of applying the given [transform] function + * to each [entity][Entity] of the family. + */ + fun map(transform: (Entity) -> R): List = mutableEntities.map(transform) + + /** + * Returns a [List] containing the results of applying the given [transform] function + * to each [entity][Entity] and its index of the family. + */ + fun mapIndexed(transform: (index: Int, Entity) -> R): List = mutableEntities.mapIndexed(transform) + + /** + * Applies the given [transform] function to each [entity][Entity] of the family and appends + * the results to the given [destination]. + */ + fun > mapTo(destination: C, transform: (Entity) -> R): C = + mutableEntities.mapTo(destination, transform) + + /** + * Applies the given [transform] function to each [entity][Entity] and its index of the family and appends + * the results to the given [destination]. + */ + fun > mapIndexedTo(destination: C, transform: (index: Int, Entity) -> R): C = + mutableEntities.mapIndexedTo(destination, transform) + + /** + * Returns a list containing only the non-null results of applying the given [transform] function + * to each [entity][Entity] of the family. + */ + fun mapNotNull(transform: (Entity) -> R?): List = mutableEntities.mapNotNull(transform) + + /** + * Applies the given [transform] function to each [entity][Entity] of the family and appends only + * the non-null results to the given [destination]. + */ + fun > mapNotNullTo(destination: C, transform: (Entity) -> R?): C = + mutableEntities.mapNotNullTo(destination, transform) + + /** + * Splits the original family into a pair of bags, + * where the first bag contains elements for which predicate yielded true, + * while the second bag contains elements for which predicate yielded false. + */ + fun partition(predicate: (Entity) -> Boolean): Pair = mutableEntities.partition(predicate) + + /** + * Splits the original family into two bags, + * where [first] contains elements for which predicate yielded true, + * while [second] contains elements for which predicate yielded false. + */ + fun partitionTo(first: MutableEntityBag, second: MutableEntityBag, predicate: (Entity) -> Boolean) = + mutableEntities.partitionTo(first, second, predicate) + + /** + * Returns a random [entity][Entity] of the family. + * + * @throws [NoSuchElementException] if the family is empty. + */ + fun random(): Entity = mutableEntities.random() + + /** + * Returns a random [entity][Entity] of the family, or null if the family is empty. + */ + fun randomOrNull(): Entity? = mutableEntities.randomOrNull() + + /** + * Returns the single [entity][Entity] of the family, or throws an exception + * if the family is empty or has more than one [entity][Entity]. + */ + fun single(): Entity = mutableEntities.single() + + /** + * Returns the single [entity][Entity] of the family matching the given [predicate], + * or throws an exception if the family is empty or has more than one [entity][Entity]. + */ + fun single(predicate: (Entity) -> Boolean): Entity = mutableEntities.single(predicate) + + /** + * Returns single [entity][Entity] of the family, or null + * if the family is empty or has more than one [entity][Entity]. + */ + fun singleOrNull(): Entity? = mutableEntities.singleOrNull() + + /** + * Returns the single [entity][Entity] of the family matching the given [predicate], + * or null if the family is empty or has more than one [entity][Entity]. + */ + fun singleOrNull(predicate: (Entity) -> Boolean): Entity? = mutableEntities.singleOrNull(predicate) + + /** + * Returns a [List] containing the first [n] [entities][Entity]. + */ + fun take(n: Int): EntityBag = mutableEntities.take(n) + /** * Adds the [entity] to the family and sets the [isDirty] flag if and only * if the entity's [compMask] is matching the family configuration. diff --git a/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyBagFunctionsTest.kt b/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyBagFunctionsTest.kt new file mode 100644 index 0000000..19565b0 --- /dev/null +++ b/src/commonTest/kotlin/com/github/quillraven/fleks/FamilyBagFunctionsTest.kt @@ -0,0 +1,299 @@ +package com.github.quillraven.fleks + +import com.github.quillraven.fleks.collection.BitArray +import com.github.quillraven.fleks.collection.MutableEntityBag +import com.github.quillraven.fleks.collection.entityBagOf +import com.github.quillraven.fleks.collection.mutableEntityBagOf +import kotlin.test.* + +class FamilyBagFunctionsTest { + + private val testEntity1 = Entity(0, version = 0u) + private val testEntity2 = Entity(1, version = 0u) + + private val testWorld = configureWorld { } + private val testFamily = Family(world = testWorld).apply { + onEntityAdded(testEntity1, BitArray()) + onEntityAdded(testEntity2, BitArray()) + } + private val testFamilySingle = Family(world = testWorld).apply { + onEntityAdded(testEntity1, BitArray()) + } + + @Test + fun testContainsAll() { + assertTrue(testFamily.containsAll(listOf(testEntity2, testEntity1))) + assertTrue(testFamily.containsAll(testFamily)) + assertFalse(testFamily.containsAll(listOf(Entity(2, version = 0u)))) + assertTrue(testFamily.containsAll(listOf(Entity(1, version = 0u)))) + assertTrue(testFamily.containsAll(emptyList())) + } + + @Test + fun testAll() { + assertTrue(testFamily.all { it.id >= 0 }) + assertFalse(testFamily.all { it.id == 0 }) + } + + @Test + fun testAny() { + assertTrue(testFamily.any { it.id == 0 }) + assertFalse(testFamily.any { it.id == 2 }) + } + + @Test + fun testNone() { + assertTrue(testFamily.none { it.id == 2 }) + assertFalse(testFamily.none { it.id == 0 }) + } + + @Test + fun testFilter() { + val expected = entityBagOf(testEntity1) + val expectedIndices = listOf(0, 1) + + val actual1 = testFamily.filter { it == testEntity1 } + val actualIndices = mutableListOf() + val actual2 = testFamily.filterIndexed { index, entity -> + actualIndices += index + entity == testEntity1 + } + + assertEquals(expected, actual1) + assertEquals(expected, actual2) + assertEquals(expectedIndices, actualIndices) + } + + @Test + fun testFilterNot() { + val expected = entityBagOf(testEntity2) + + val actual = testFamily.filterNot { it == testEntity1 } + + assertEquals(expected, actual) + } + + @Test + fun testFilterTo() { + val testEntity3 = Entity(2, version = 0u) + val expected = entityBagOf(testEntity3, testEntity1) + val expectedIndices = listOf(0, 1) + val destination1 = mutableEntityBagOf(testEntity3) + val destination2 = mutableEntityBagOf(testEntity3) + + val actual1 = testFamily.filterTo(destination1) { it == testEntity1 } + val actualIndices = mutableListOf() + val actual2 = testFamily.filterIndexedTo(destination2) { index, entity -> + actualIndices += index + entity == testEntity1 + } + + assertEquals(expected, actual1) + assertEquals(expected, actual2) + assertEquals(expectedIndices, actualIndices) + } + + @Test + fun testFilterNotTo() { + val testEntity3 = Entity(2, version = 0u) + val expected = entityBagOf(testEntity3, testEntity2) + val destination = mutableEntityBagOf(testEntity3) + + val actual = testFamily.filterNotTo(destination) { it == testEntity1 } + + assertEquals(expected, actual) + } + + @Test + fun testFind() { + assertEquals(testEntity1, testFamily.find { it == testEntity1 }) + assertNull(testFamily.find { it.id == 3 }) + } + + @Test + fun testFirst() { + assertEquals(testEntity1, testFamily.first()) + assertEquals(testEntity2, testFamily.first { it == testEntity2 }) + assertFailsWith { MutableEntityBag().first() } + assertFailsWith { testFamily.first { it.id == 3 } } + } + + @Test + fun testFirstOrNull() { + assertEquals(testEntity1, testFamily.firstOrNull()) + assertEquals(testEntity2, testFamily.firstOrNull { it == testEntity2 }) + assertNull(MutableEntityBag().firstOrNull()) + assertNull(testFamily.firstOrNull { it.id == 3 }) + } + + @Test + fun testGroupBy() { + val expected1 = mapOf(0 to mutableEntityBagOf(testEntity1), 1 to mutableEntityBagOf(testEntity2)) + val expected2 = mapOf(0 to listOf(3), 1 to listOf(3)) + + val actual1 = testFamily.groupBy { it.id } + val actual2 = testFamily.groupBy( + { it.id }, + { 3 } + ) + + assertTrue(expected1.keys.containsAll(actual1.keys)) + assertTrue(actual1.keys.containsAll(expected1.keys)) + expected1.forEach { (id, bag) -> + assertEquals(bag, actual1[id]) + } + assertTrue(expected2.keys.containsAll(actual2.keys)) + assertTrue(actual2.keys.containsAll(expected2.keys)) + expected2.forEach { (id, intList) -> + assertContentEquals(intList, actual2[id]) + } + } + + @Test + fun testGroupByTo() { + val expected1 = mapOf( + 0 to mutableEntityBagOf(testEntity1), + 1 to mutableEntityBagOf(testEntity2), + 2 to mutableEntityBagOf(Entity(2, version = 0u)) + ) + val expected2 = mapOf(0 to listOf(3), 1 to listOf(3), 2 to listOf(3)) + + val actual = testFamily.groupByTo(mutableMapOf(2 to mutableEntityBagOf(Entity(2, version = 0u)))) { it.id } + val actual2 = testFamily.groupByTo( + mutableMapOf(2 to mutableListOf(3)), + { it.id }, + { 3 } + ) + + assertTrue(expected1.keys.containsAll(actual.keys)) + assertTrue(actual.keys.containsAll(expected1.keys)) + expected1.forEach { (id, bag) -> + assertEquals(bag, actual[id]) + } + assertTrue(expected2.keys.containsAll(actual2.keys)) + assertTrue(actual2.keys.containsAll(expected2.keys)) + expected2.forEach { (id, intList) -> + assertContentEquals(intList, actual2[id]) + } + } + + @Test + fun testMap() { + val expected = listOf(2, 3) + val expectedIndices = listOf(0, 1) + + val actual1 = testFamily.map { it.id + 2 } + val actualIndices = mutableListOf() + val actual2 = testFamily.mapIndexed { index, entity -> + actualIndices += index + entity.id + 2 + } + + assertContentEquals(expected, actual1) + assertContentEquals(expected, actual2) + assertContentEquals(expectedIndices, actualIndices) + } + + @Test + fun testMapTo() { + val expected = listOf(5, 2, 3) + val expectedIndices = listOf(0, 1) + + val actual1 = testFamily.mapTo(mutableListOf(5)) { it.id + 2 } + val actualIndices = mutableListOf() + val actual2 = testFamily.mapIndexedTo(mutableListOf(5)) { index, entity -> + actualIndices += index + entity.id + 2 + } + + assertContentEquals(expected, actual1) + assertContentEquals(expected, actual2) + assertContentEquals(expectedIndices, actualIndices) + } + + @Test + fun testMapNotNull() { + val expected = listOf(2) + + val actual = testFamily.mapNotNull { if (it.id == 0) 2 else null } + + assertContentEquals(expected, actual) + } + + @Test + fun testMapNotNullTo() { + val expected = listOf(5, 2) + + val actual = testFamily.mapNotNullTo(mutableListOf(5)) { if (it.id == 0) 2 else null } + + assertContentEquals(expected, actual) + } + + @Test + fun testPartition() { + val (first, second) = testFamily.partition { it.id <= 0 } + + assertEquals(1, first.size) + assertEquals(1, second.size) + assertTrue(testEntity1 in first) + assertTrue(testEntity2 in second) + } + + @Test + fun testPartitionTo() { + val first = MutableEntityBag() + val second = MutableEntityBag() + + testFamily.partitionTo(first, second) { it.id <= 0 } + + assertEquals(1, first.size) + assertEquals(1, second.size) + assertTrue(testEntity1 in first) + assertTrue(testEntity2 in second) + } + + @Test + fun testRandom() { + val actual = testFamily.random() + + assertFailsWith { MutableEntityBag().random() } + assertTrue(actual == testEntity1 || actual == testEntity2) + } + + @Test + fun testRandomOrNull() { + val actual = testFamily.randomOrNull() + + assertNull(MutableEntityBag().randomOrNull()) + assertTrue(actual == testEntity1 || actual == testEntity2) + } + + @Test + fun testSingle() { + assertEquals(testEntity1, testFamilySingle.single()) + assertEquals(testEntity1, testFamilySingle.single { it == testEntity1 }) + assertFailsWith { testFamily.single() } + assertFailsWith { entityBagOf(testEntity1, testEntity1).single { it == testEntity1 } } + assertFailsWith { MutableEntityBag().single() } + assertFailsWith { testFamily.single { it.id == 3 } } + } + + @Test + fun testSingleOrNull() { + assertEquals(testEntity1, testFamilySingle.singleOrNull()) + assertEquals(testEntity1, testFamilySingle.singleOrNull { it == testEntity1 }) + assertNull(testFamily.singleOrNull()) + assertNull(entityBagOf(testEntity1, testEntity1).singleOrNull { it == testEntity1 }) + assertNull(MutableEntityBag().singleOrNull()) + assertNull(testFamily.singleOrNull { it.id == 3 }) + } + + @Test + fun testTake() { + assertTrue(testFamily.take(-1).isEmpty()) + assertTrue(testFamily.take(0).isEmpty()) + assertEquals(entityBagOf(testEntity1), testFamily.take(1)) + assertEquals(entityBagOf(testEntity1, testEntity2), testFamily.take(2)) + assertEquals(entityBagOf(testEntity1, testEntity2), testFamily.take(3)) + } +}