summaryrefslogtreecommitdiff
path: root/composeApp/src/commonTest
diff options
context:
space:
mode:
Diffstat (limited to 'composeApp/src/commonTest')
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt12
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt242
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt116
-rw-r--r--composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt238
4 files changed, 608 insertions, 0 deletions
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt
new file mode 100644
index 0000000..78e642f
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt
@@ -0,0 +1,12 @@
+package coffee.liz.abstractionengine
+
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class ComposeAppCommonTest {
+
+ @Test
+ fun example() {
+ assertEquals(3, 1 + 2)
+ }
+} \ No newline at end of file
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt
new file mode 100644
index 0000000..737e386
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt
@@ -0,0 +1,242 @@
+package coffee.liz.ecs
+
+import kotlin.reflect.KClass
+import kotlin.test.*
+
+// Test systems for circular dependency test
+class CircularSystemA : System {
+ override val dependencies: Set<KClass<out System>>
+ get() = setOf(CircularSystemB::class)
+ override fun update(world: World, deltaTime: Float) {}
+}
+
+class CircularSystemB : System {
+ override val dependencies: Set<KClass<out System>>
+ get() = setOf(CircularSystemA::class)
+ override fun update(world: World, deltaTime: Float) {}
+}
+
+class DAGWorldTest {
+
+ @Test
+ fun `can create entities with unique ids`() {
+ val world = DAGWorld(emptyList())
+
+ val entity1 = world.createEntity()
+ val entity2 = world.createEntity()
+
+ assertNotEquals(entity1.id, entity2.id)
+ }
+
+ @Test
+ fun `can destroy entity`() {
+ val world = DAGWorld(emptyList())
+ val entity = world.createEntity()
+ entity.add(Position(10f, 20f))
+
+ world.destroyEntity(entity)
+
+ val results = world.query<Position>()
+ assertFalse(results.contains(entity))
+ }
+
+ @Test
+ fun `query returns entities with matching components`() {
+ val world = DAGWorld(emptyList())
+ val entity1 = world.createEntity().add(Position(10f, 20f))
+ val entity2 = world.createEntity().add(Position(30f, 40f))
+ val entity3 = world.createEntity().add(Velocity(1f, 2f))
+
+ world.update(0f) // Trigger cache update
+
+ val results = world.query<Position>()
+
+ assertEquals(2, results.size)
+ assertTrue(results.contains(entity1))
+ assertTrue(results.contains(entity2))
+ assertFalse(results.contains(entity3))
+ }
+
+ @Test
+ fun `query with multiple components returns intersection`() {
+ val world = DAGWorld(emptyList())
+ val entity1 = world.createEntity()
+ .add(Position(10f, 20f))
+ .add(Velocity(1f, 2f))
+ val entity2 = world.createEntity()
+ .add(Position(30f, 40f))
+ val entity3 = world.createEntity()
+ .add(Velocity(3f, 4f))
+
+ world.update(0f) // Trigger cache update
+
+ val results = world.query<Position, Velocity>()
+
+ assertEquals(1, results.size)
+ assertTrue(results.contains(entity1))
+ }
+
+ @Test
+ fun `systems execute in dependency order`() {
+ val executionOrder = mutableListOf<String>()
+
+ val systemA = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("A")
+ }
+ }
+
+ val systemB = object : System {
+ override val dependencies = setOf(systemA::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("B")
+ }
+ }
+
+ val systemC = object : System {
+ override val dependencies = setOf(systemB::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("C")
+ }
+ }
+
+ val world = DAGWorld(listOf(systemC, systemA, systemB))
+ world.update(0f)
+
+ assertEquals(listOf("A", "B", "C"), executionOrder)
+ }
+
+ @Test
+ fun `systems with no dependencies can execute in any order`() {
+ val executionOrder = mutableListOf<String>()
+
+ val systemA = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("A")
+ }
+ }
+
+ val systemB = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("B")
+ }
+ }
+
+ val world = DAGWorld(listOf(systemA, systemB))
+ world.update(0f)
+
+ assertEquals(2, executionOrder.size)
+ assertTrue(executionOrder.contains("A"))
+ assertTrue(executionOrder.contains("B"))
+ }
+
+ @Test
+ fun `circular dependency throws exception`() {
+ val systemA = CircularSystemA()
+ val systemB = CircularSystemB()
+
+ assertFailsWith<IllegalArgumentException> {
+ DAGWorld(listOf(systemA, systemB))
+ }
+ }
+
+ @Test
+ fun `system receives correct deltaTime`() {
+ var receivedDelta = 0f
+
+ val system = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ receivedDelta = deltaTime
+ }
+ }
+
+ val world = DAGWorld(listOf(system))
+ world.update(0.016f)
+
+ assertEquals(0.016f, receivedDelta)
+ }
+
+ @Test
+ fun `systems can query entities during update`() {
+ var queriedEntities: Set<Entity>? = null
+
+ val system = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ queriedEntities = world.query<Position>()
+ }
+ }
+
+ val world = DAGWorld(listOf(system))
+ val entity = world.createEntity().add(Position(10f, 20f))
+
+ world.update(0f)
+
+ assertNotNull(queriedEntities)
+ assertEquals(1, queriedEntities!!.size)
+ assertTrue(queriedEntities!!.contains(entity))
+ }
+
+ @Test
+ fun `component cache updates after entity changes`() {
+ val world = DAGWorld(emptyList())
+ val entity = world.createEntity()
+
+ world.update(0f)
+ assertEquals(0, world.query<Position>().size)
+
+ entity.add(Position(10f, 20f))
+ world.update(0f)
+ assertEquals(1, world.query<Position>().size)
+
+ entity.remove<Position>()
+ world.update(0f)
+ assertEquals(0, world.query<Position>().size)
+ }
+
+ @Test
+ fun `diamond dependency resolves correctly`() {
+ val executionOrder = mutableListOf<String>()
+
+ // A
+ // / \
+ // B C
+ // \ /
+ // D
+
+ val systemA = object : System {
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("A")
+ }
+ }
+
+ val systemB = object : System {
+ override val dependencies = setOf(systemA::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("B")
+ }
+ }
+
+ val systemC = object : System {
+ override val dependencies = setOf(systemA::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("C")
+ }
+ }
+
+ val systemD = object : System {
+ override val dependencies = setOf(systemB::class, systemC::class)
+ override fun update(world: World, deltaTime: Float) {
+ executionOrder.add("D")
+ }
+ }
+
+ val world = DAGWorld(listOf(systemD, systemC, systemB, systemA))
+ world.update(0f)
+
+ // A must come first, D must come last, B and C in between
+ assertEquals("A", executionOrder.first())
+ assertEquals("D", executionOrder.last())
+ assertTrue(executionOrder.indexOf("B") < executionOrder.indexOf("D"))
+ assertTrue(executionOrder.indexOf("C") < executionOrder.indexOf("D"))
+ }
+}
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt
new file mode 100644
index 0000000..e807bd2
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt
@@ -0,0 +1,116 @@
+package coffee.liz.ecs
+
+import kotlin.test.*
+
+// Test components
+data class Position(val x: Float, val y: Float) : Component
+data class Velocity(val dx: Float, val dy: Float) : Component
+data class Health(val value: Int) : Component
+
+class EntityTest {
+
+ @Test
+ fun `entity has unique id`() {
+ val entity1 = Entity(1)
+ val entity2 = Entity(2)
+
+ assertNotEquals(entity1.id, entity2.id)
+ }
+
+ @Test
+ fun `can add component to entity`() {
+ val entity = Entity(0)
+ val position = Position(10f, 20f)
+
+ entity.add(position)
+
+ assertTrue(entity.has<Position>())
+ assertEquals(position, entity.get<Position>())
+ }
+
+ @Test
+ fun `can remove component from entity`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+
+ entity.remove<Position>()
+
+ assertFalse(entity.has<Position>())
+ assertNull(entity.get<Position>())
+ }
+
+ @Test
+ fun `add returns entity for chaining`() {
+ val entity = Entity(0)
+
+ val result = entity
+ .add(Position(10f, 20f))
+ .add(Velocity(1f, 2f))
+
+ assertSame(entity, result)
+ assertTrue(entity.has<Position>())
+ assertTrue(entity.has<Velocity>())
+ }
+
+ @Test
+ fun `can replace component by adding same type`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+ entity.add(Position(30f, 40f))
+
+ val position = entity.get<Position>()
+
+ assertNotNull(position)
+ assertEquals(30f, position.x)
+ assertEquals(40f, position.y)
+ }
+
+ @Test
+ fun `hasAll returns true when entity has all components`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+ entity.add(Velocity(1f, 2f))
+
+ assertTrue(entity.hasAll(Position::class, Velocity::class))
+ }
+
+ @Test
+ fun `hasAll returns false when entity missing component`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+
+ assertFalse(entity.hasAll(Position::class, Velocity::class))
+ }
+
+ @Test
+ fun `componentTypes returns all component types`() {
+ val entity = Entity(0)
+ entity.add(Position(10f, 20f))
+ entity.add(Velocity(1f, 2f))
+ entity.add(Health(100))
+
+ val types = entity.componentTypes()
+
+ assertEquals(3, types.size)
+ assertTrue(types.contains(Position::class))
+ assertTrue(types.contains(Velocity::class))
+ assertTrue(types.contains(Health::class))
+ }
+
+ @Test
+ fun `entities with same id are equal`() {
+ val entity1 = Entity(1)
+ val entity2 = Entity(1)
+
+ assertEquals(entity1, entity2)
+ assertEquals(entity1.hashCode(), entity2.hashCode())
+ }
+
+ @Test
+ fun `entities with different id are not equal`() {
+ val entity1 = Entity(1)
+ val entity2 = Entity(2)
+
+ assertNotEquals(entity1, entity2)
+ }
+}
diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt
new file mode 100644
index 0000000..bb7298e
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt
@@ -0,0 +1,238 @@
+package coffee.liz.ecs.animation
+
+import coffee.liz.ecs.DAGWorld
+import kotlin.test.*
+
+class AnimationSystemTest {
+
+ private fun createTestSpriteSheet(): SpriteSheet {
+ return SpriteSheet(
+ imagePath = "test.png",
+ frames = mapOf(
+ FrameName("idle_0") to Rect(0, 0, 32, 32),
+ FrameName("idle_1") to Rect(32, 0, 32, 32),
+ FrameName("walk_0") to Rect(0, 32, 32, 32),
+ FrameName("walk_1") to Rect(32, 32, 32, 32),
+ FrameName("walk_2") to Rect(64, 32, 32, 32)
+ )
+ )
+ }
+
+ private fun createTestAnimator(): Animator {
+ return Animator(
+ clips = mapOf(
+ AnimationName("idle") to AnimationClip(
+ frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")),
+ frameDuration = 0.1f,
+ loopMode = LoopMode.LOOP
+ ),
+ AnimationName("walk") to AnimationClip(
+ frameNames = listOf(
+ FrameName("walk_0"),
+ FrameName("walk_1"),
+ FrameName("walk_2")
+ ),
+ frameDuration = 0.15f,
+ loopMode = LoopMode.LOOP
+ )
+ ),
+ currentClip = AnimationName("idle")
+ )
+ }
+
+ @Test
+ fun `animation advances frame after frame duration`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ assertEquals(0, animator.frameIndex)
+ assertEquals(0f, animator.elapsed)
+
+ // Update with less than frame duration - should not advance
+ world.update(0.05f)
+ assertEquals(0, animator.frameIndex)
+ assertEquals(0.05f, animator.elapsed)
+
+ // Update to exceed frame duration - should advance to frame 1
+ world.update(0.06f)
+ assertEquals(1, animator.frameIndex)
+ assertTrue(animator.elapsed < 0.1f)
+ }
+
+ @Test
+ fun `LOOP mode wraps to beginning`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ // Idle animation has 2 frames at 0.1s each
+ world.update(0.1f) // Frame 1
+ assertEquals(1, animator.frameIndex)
+
+ world.update(0.1f) // Should wrap to Frame 0
+ assertEquals(0, animator.frameIndex)
+ assertTrue(animator.playing) // Still playing
+ }
+
+ @Test
+ fun `ONCE mode stops at last frame`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = Animator(
+ clips = mapOf(
+ AnimationName("once") to AnimationClip(
+ frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")),
+ frameDuration = 0.1f,
+ loopMode = LoopMode.ONCE
+ )
+ ),
+ currentClip = AnimationName("once")
+ )
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ world.update(0.1f) // Frame 1
+ assertEquals(1, animator.frameIndex)
+ assertTrue(animator.playing)
+
+ world.update(0.1f) // Should stop at frame 1
+ assertEquals(1, animator.frameIndex)
+ assertFalse(animator.playing)
+
+ // Further updates should not change anything
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ assertFalse(animator.playing)
+ }
+
+ @Test
+ fun `PING_PONG mode bounces back and forth`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = Animator(
+ clips = mapOf(
+ AnimationName("pingpong") to AnimationClip(
+ frameNames = listOf(
+ FrameName("walk_0"),
+ FrameName("walk_1"),
+ FrameName("walk_2")
+ ),
+ frameDuration = 0.1f,
+ loopMode = LoopMode.PING_PONG
+ )
+ ),
+ currentClip = AnimationName("pingpong")
+ )
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ // Forward: 0 -> 1 -> 2
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ assertEquals(1, animator.direction)
+
+ world.update(0.1f)
+ assertEquals(2, animator.frameIndex)
+ assertEquals(1, animator.direction)
+
+ // Hit end, should reverse: 2 -> 1
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ assertEquals(-1, animator.direction)
+
+ // Continue backward: 1 -> 0
+ world.update(0.1f)
+ assertEquals(0, animator.frameIndex)
+ assertEquals(-1, animator.direction)
+
+ // Hit beginning, should reverse: 0 -> 1
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ assertEquals(1, animator.direction)
+ }
+
+ @Test
+ fun `play() switches animation and resets state`() {
+ val animator = createTestAnimator()
+ assertEquals(AnimationName("idle"), animator.currentClip)
+ assertEquals(0, animator.frameIndex)
+
+ // Manually advance
+ animator.frameIndex = 1
+ animator.elapsed = 0.05f
+
+ // Switch to walk animation
+ animator.play(AnimationName("walk"))
+ assertEquals(AnimationName("walk"), animator.currentClip)
+ assertEquals(0, animator.frameIndex)
+ assertEquals(0f, animator.elapsed)
+ assertEquals(1, animator.direction)
+ assertTrue(animator.playing)
+ }
+
+ @Test
+ fun `pause() and resume() control playback`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ animator.pause()
+ assertFalse(animator.playing)
+
+ // Update should not advance animation
+ world.update(0.1f)
+ assertEquals(0, animator.frameIndex)
+
+ animator.resume()
+ assertTrue(animator.playing)
+
+ // Now it should advance
+ world.update(0.1f)
+ assertEquals(1, animator.frameIndex)
+ }
+
+ @Test
+ fun `getCurrentFrameName returns correct frame`() {
+ val animator = createTestAnimator()
+
+ assertEquals(FrameName("idle_0"), animator.getCurrentFrameName())
+
+ animator.frameIndex = 1
+ assertEquals(FrameName("idle_1"), animator.getCurrentFrameName())
+
+ animator.play(AnimationName("walk"))
+ assertEquals(FrameName("walk_0"), animator.getCurrentFrameName())
+ }
+
+ @Test
+ fun `multiple updates in single frame work correctly`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(createTestSpriteSheet())
+ entity.add(animator)
+
+ // Large delta time should advance multiple frames
+ world.update(0.25f) // Should advance 2 frames and have 0.05s remainder
+ assertEquals(0, animator.frameIndex) // Wrapped back to 0 (LOOP mode)
+ assertTrue(animator.elapsed >= 0.04f && animator.elapsed <= 0.06f)
+ }
+
+ @Test
+ fun `entities without SpriteSheet are skipped`() {
+ val world = DAGWorld(listOf(AnimationSystem()))
+ val entity = world.createEntity()
+ val animator = createTestAnimator()
+ entity.add(animator) // No SpriteSheet
+
+ // Should not crash
+ world.update(0.1f)
+ }
+}