aboutsummaryrefslogtreecommitdiff
path: root/core/src/test/java/coffee/liz/ecs
diff options
context:
space:
mode:
authorElizabeth Hunt <me@liz.coffee>2026-01-23 20:22:30 -0800
committerElizabeth Hunt <me@liz.coffee>2026-01-23 20:22:30 -0800
commit52864cb701e59a1d847fd5586245519eb5e3b3bc (patch)
tree1d3df85b939e2c50ebf154ab4fcac6f02ad087c2 /core/src/test/java/coffee/liz/ecs
downloadthe-abstraction-engine-v2-52864cb701e59a1d847fd5586245519eb5e3b3bc.tar.gz
the-abstraction-engine-v2-52864cb701e59a1d847fd5586245519eb5e3b3bc.zip
Move code over
Diffstat (limited to 'core/src/test/java/coffee/liz/ecs')
-rw-r--r--core/src/test/java/coffee/liz/ecs/DAGWorldTest.java164
-rw-r--r--core/src/test/java/coffee/liz/ecs/model/EntityTest.java97
2 files changed, 261 insertions, 0 deletions
diff --git a/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java
new file mode 100644
index 0000000..2f948d0
--- /dev/null
+++ b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java
@@ -0,0 +1,164 @@
+package coffee.liz.ecs;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import coffee.liz.ecs.model.Component;
+import coffee.liz.ecs.model.Entity;
+import coffee.liz.ecs.model.System;
+import coffee.liz.ecs.model.World;
+
+import lombok.RequiredArgsConstructor;
+
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+final class DAGWorldTest {
+ @Test
+ public void queryReturnsEntitiesMatchingAllRequestedComponents() {
+ final DAGWorld<String> world = new DAGWorld<>(Map.of());
+ final Entity matching = world.createEntity();
+ matching.add(new PositionComponent());
+ matching.add(new VelocityComponent());
+ final Entity partial = world.createEntity();
+ partial.add(new PositionComponent());
+ final Entity nonMatching = world.createEntity();
+ nonMatching.add(new VelocityComponent());
+
+ world.update("state", Duration.ZERO);
+
+ final Set<Entity> results = world.query(Set.of(PositionComponent.class, VelocityComponent.class));
+
+ assertEquals(Set.of(matching), results);
+ }
+
+ @Test
+ public void queryWithoutComponentsReturnsAllEntities() {
+ final DAGWorld<String> world = new DAGWorld<>(Map.of());
+ final Entity entityOne = world.createEntity();
+ final Entity entityTwo = world.createEntity();
+
+ final Set<Entity> results = world.query(List.<Class<? extends Component>>of());
+
+ assertEquals(Set.of(entityOne, entityTwo), results);
+ }
+
+ @Test
+ public void updateExecutesSystemsInTopologicalOrder() {
+ final CopyOnWriteArrayList<String> executionLog = new CopyOnWriteArrayList<>();
+
+ final DAGWorld<String> world = new DAGWorld<>(Map.of(SystemC.class, new SystemC(executionLog), SystemA.class,
+ new SystemA(executionLog), SystemB.class, new SystemB(executionLog)));
+ world.update("state", Duration.ZERO);
+
+ assertEquals(List.of("A", "B", "C"), executionLog);
+ }
+
+ @Test
+ public void updateRefreshesComponentCacheAfterEntityMutations() {
+ final DAGWorld<String> world = new DAGWorld<>(Map.of());
+ final Entity subject = world.createEntity();
+
+ world.update("state", Duration.ZERO);
+ assertTrue(world.query(Set.of(PositionComponent.class)).isEmpty());
+
+ subject.add(new PositionComponent());
+ world.update("state", Duration.ZERO);
+ assertEquals(1, world.query(Set.of(PositionComponent.class)).size());
+
+ subject.remove(PositionComponent.class);
+ world.update("state", Duration.ZERO);
+ assertTrue(world.query(Set.of(PositionComponent.class)).isEmpty());
+ }
+
+ @Test
+ public void circularDependencyDetectionThrowsIllegalStateException() {
+ final Map<Class<? extends System<String>>, System<String>> systems = new LinkedHashMap<>();
+ final SystemCycleA systemA = new SystemCycleA();
+ final SystemCycleB systemB = new SystemCycleB();
+ systems.put(SystemCycleA.class, systemA);
+ systems.put(SystemCycleB.class, systemB);
+
+ assertThrows(IllegalStateException.class, () -> new DAGWorld<>(systems));
+ }
+
+ private static final class PositionComponent implements Component {
+ }
+
+ private static final class VelocityComponent implements Component {
+ }
+
+ @RequiredArgsConstructor
+ private abstract static class RecordingSystem implements System<String> {
+ private final List<String> log;
+ private final String label;
+
+ @Override
+ public final void update(final World<String> world, final String state, final Duration duration) {
+ log.add(label);
+ }
+ }
+
+ private static final class SystemA extends RecordingSystem {
+ private SystemA(final List<String> log) {
+ super(log, "A");
+ }
+
+ @Override
+ public Collection<Class<? extends System<String>>> getDependencies() {
+ return List.of();
+ }
+ }
+
+ private static final class SystemB extends RecordingSystem {
+ private SystemB(final List<String> log) {
+ super(log, "B");
+ }
+
+ @Override
+ public Collection<Class<? extends System<String>>> getDependencies() {
+ return Set.of(SystemA.class);
+ }
+ }
+
+ private static final class SystemC extends RecordingSystem {
+ private SystemC(final List<String> log) {
+ super(log, "C");
+ }
+
+ @Override
+ public Collection<Class<? extends System<String>>> getDependencies() {
+ return Set.of(SystemB.class);
+ }
+ }
+
+ private static final class SystemCycleA implements System<String> {
+ @Override
+ public Collection<Class<? extends System<String>>> getDependencies() {
+ return Set.of(SystemCycleB.class);
+ }
+
+ @Override
+ public void update(final World<String> world, final String state, final Duration duration) {
+ }
+ }
+
+ private static final class SystemCycleB implements System<String> {
+ @Override
+ public Collection<Class<? extends System<String>>> getDependencies() {
+ return Set.of(SystemCycleA.class);
+ }
+
+ @Override
+ public void update(final World<String> world, final String state, final Duration duration) {
+ }
+ }
+}
diff --git a/core/src/test/java/coffee/liz/ecs/model/EntityTest.java b/core/src/test/java/coffee/liz/ecs/model/EntityTest.java
new file mode 100644
index 0000000..c9ce59a
--- /dev/null
+++ b/core/src/test/java/coffee/liz/ecs/model/EntityTest.java
@@ -0,0 +1,97 @@
+package coffee.liz.ecs.model;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import lombok.RequiredArgsConstructor;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Stream;
+
+final class EntityTest {
+ @ParameterizedTest
+ @MethodSource("componentCombinationProvider")
+ public void hasAllReportsPresenceForComponentSets(final Collection<Class<? extends Component>> query,
+ final boolean expected) {
+ final Entity entity = Entity.builder().id(7).build();
+ entity.add(new AlphaComponent("first"));
+ entity.add(new BetaComponent(3));
+ entity.add(new GammaKeyedComponent());
+
+ assertEquals(expected, entity.hasAll(query));
+ }
+
+ private static Stream<Arguments> componentCombinationProvider() {
+ return Stream
+ .of(Arguments.of(List.of(AlphaComponent.class), true), Arguments.of(List.of(BetaComponent.class), true),
+ Arguments.of(List.of(AlphaComponent.class, BetaComponent.class, GammaComponent.class), true),
+ Arguments.of(List.of(AlphaComponent.class, BetaComponent.class, GammaKeyedComponent.class),
+ false),
+ Arguments.of(List.of(GammaComponent.class), true),
+ Arguments.of(List.of(GammaKeyedComponent.class), false));
+ }
+
+ @Test
+ public void getThrowsForMissingComponentsWithHelpfulMessage() {
+ final Entity entity = Entity.builder().id(99).build();
+
+ final IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
+ () -> entity.get(AlphaComponent.class));
+
+ assertTrue(thrown.getMessage().contains("AlphaComponent"));
+ assertTrue(thrown.getMessage().contains("99"));
+ }
+
+ @Test
+ public void addReplacesExistingComponentInstance() {
+ final Entity entity = Entity.builder().id(17).build();
+ final AlphaComponent initial = new AlphaComponent("initial");
+ entity.add(initial);
+
+ final AlphaComponent replacement = new AlphaComponent("replacement");
+ entity.add(replacement);
+
+ assertSame(replacement, entity.get(AlphaComponent.class));
+ }
+
+ @Test
+ public void removeClearsComponentPresence() {
+ final Entity entity = Entity.builder().id(45).build();
+ entity.add(new BetaComponent(2));
+ assertTrue(entity.has(BetaComponent.class));
+
+ entity.remove(BetaComponent.class);
+
+ assertFalse(entity.has(BetaComponent.class));
+ assertTrue(entity.componentTypes().isEmpty());
+ }
+
+ private record AlphaComponent(String name) implements Component {
+ }
+
+ private record BetaComponent(int level) implements Component {
+ }
+
+ @RequiredArgsConstructor
+ private class GammaComponent implements Component {
+ @Override
+ public Class<? extends Component> getKey() {
+ return GammaComponent.class;
+ }
+ }
+
+ private class GammaKeyedComponent extends GammaComponent {
+ }
+
+ private record ContextualComponent(int ownerId) implements Component {
+ }
+}