diff options
| author | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-02-27 20:00:05 -0800 |
|---|---|---|
| committer | Elizabeth Alexander Hunt <me@liz.coffee> | 2026-02-27 20:00:05 -0800 |
| commit | 6e2fb9b12870a24f99071ea726d1c49ed57593ad (patch) | |
| tree | ffd77e884206ddd2f8312dce4ce3191524df0796 /core/src/test/java | |
| parent | c9c7e69714707262e8bdea25853bf33447a57951 (diff) | |
| download | dyl-6e2fb9b12870a24f99071ea726d1c49ed57593ad.tar.gz dyl-6e2fb9b12870a24f99071ea726d1c49ed57593ad.zip | |
Moving penguin
Diffstat (limited to 'core/src/test/java')
| -rw-r--r-- | core/src/test/java/coffee/liz/ecs/DAGWorldTest.java | 206 | ||||
| -rw-r--r-- | core/src/test/java/coffee/liz/ecs/model/EntityTest.java | 97 |
2 files changed, 303 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..cf6cdad --- /dev/null +++ b/core/src/test/java/coffee/liz/ecs/DAGWorldTest.java @@ -0,0 +1,206 @@ +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 coffee.liz.ecs.model.Query; + +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.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class DAGWorldTest { + @ParameterizedTest + @MethodSource("queryScenarios") + public void queryResolvesExpectedEntities(final Query query, final Set<String> expectedEntityKeys) { + final World<String> world = new DAGWorld<>(); + final Entity both = world.createEntity(); + both.add(new PositionComponent()); + both.add(new VelocityComponent()); + final Entity positionOnly = world.createEntity(); + positionOnly.add(new PositionComponent()); + final Entity velocityOnly = world.createEntity(); + velocityOnly.add(new VelocityComponent()); + final Entity neither = world.createEntity(); + + world.update("state", 0); + + final Map<String, Entity> entities = Map.of("both", both, "positionOnly", positionOnly, "velocityOnly", + velocityOnly, "neither", neither); + final Set<Entity> expectedEntities = expectedEntityKeys.stream().map(entities::get).collect(Collectors.toSet()); + assertEquals(expectedEntities, world.resolve(query)); + } + + private static Stream<Arguments> queryScenarios() { + return Stream.of(Arguments.of(Query.allOf(PositionComponent.class, VelocityComponent.class), Set.of("both")), + Arguments.of(Query.allOf(), Set.of("both", "positionOnly", "velocityOnly", "neither")), + Arguments.of(Query.anyOf(PositionComponent.class, VelocityComponent.class), + Set.of("both", "positionOnly", "velocityOnly")), + Arguments.of(Query.noneOf(PositionComponent.class, VelocityComponent.class), Set.of("neither"))); + } + + @Test + public void updateExecutesSystemsInTopologicalOrder() { + final CopyOnWriteArrayList<String> executionLog = new CopyOnWriteArrayList<>(); + + final DAGWorld<String> world = new DAGWorld<>(new SystemC(executionLog), new SystemA(executionLog), + new SystemB(executionLog)); + world.update("state", 0); + + assertEquals(List.of("A", "B", "C"), executionLog); + } + + @Test + public void updateRefreshesComponentCacheAfterEntityMutations() { + final DAGWorld<String> world = new DAGWorld<>(); + final Entity subject = world.createEntity(); + + world.update("state", 0); + assertTrue(world.resolve(Query.allOf(PositionComponent.class)).isEmpty()); + + subject.add(new PositionComponent()); + world.update("state", 0); + assertEquals(1, world.resolve(Query.allOf(PositionComponent.class)).size()); + + subject.remove(PositionComponent.class); + world.update("state", 0); + assertTrue(world.resolve(Query.allOf(PositionComponent.class)).isEmpty()); + } + + @Test + public void queryFindsComponentsAddedByEarlierSystemInSameTick() { + final List<Set<Entity>> queryResults = new CopyOnWriteArrayList<>(); + + final DAGWorld<String> world = new DAGWorld<>(new ComponentAdderSystem(), + new ComponentReaderSystem(queryResults)); + + final Entity entity = world.createEntity(); + entity.add(new PositionComponent()); + + world.update("state", 0); + + assertEquals(1, queryResults.size()); + assertEquals(Set.of(entity), queryResults.get(0)); + } + + @Test + public void circularDependencyDetectionThrowsIllegalStateException() { + assertThrows(IllegalStateException.class, () -> new DAGWorld<>(new SystemCycleA(), new SystemCycleB())); + } + + 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 float deltaSeconds) { + 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 final void update(final World<String> world, final String state, final float deltaSeconds) { + } + } + + private static final class SystemCycleB implements System<String> { + @Override + public Collection<Class<? extends System<String>>> getDependencies() { + return Set.of(SystemCycleA.class); + } + + @Override + public final void update(final World<String> world, final String state, final float deltaSeconds) { + } + } + + private static final class ComponentAdderSystem implements System<String> { + @Override + public Collection<Class<? extends System<String>>> getDependencies() { + return List.of(); + } + + @Override + public final void update(final World<String> world, final String state, final float deltaSeconds) { + world.resolve(Query.allOf(PositionComponent.class)).forEach(e -> e.add(new VelocityComponent())); + } + } + + @RequiredArgsConstructor + private static final class ComponentReaderSystem implements System<String> { + private final List<Set<Entity>> queryResults; + + @Override + public Collection<Class<? extends System<String>>> getDependencies() { + return Set.of(ComponentAdderSystem.class); + } + + @Override + public final void update(final World<String> world, final String state, final float deltaSeconds) { + queryResults.add(world.resolve(Query.allOf(VelocityComponent.class, PositionComponent.class))); + } + } +} 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..a8fd1e3 --- /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 { + } +} |
