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 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 results = world.query(Set.of(PositionComponent.class, VelocityComponent.class)); assertEquals(Set.of(matching), results); } @Test public void queryWithoutComponentsReturnsAllEntities() { final DAGWorld world = new DAGWorld<>(Map.of()); final Entity entityOne = world.createEntity(); final Entity entityTwo = world.createEntity(); final Set results = world.query(List.>of()); assertEquals(Set.of(entityOne, entityTwo), results); } @Test public void updateExecutesSystemsInTopologicalOrder() { final CopyOnWriteArrayList executionLog = new CopyOnWriteArrayList<>(); final DAGWorld 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 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 queryFindsComponentsAddedByEarlierSystemInSameTick() { final List> queryResults = new CopyOnWriteArrayList<>(); final Map>, System> systems = new LinkedHashMap<>(); systems.put(ComponentAdderSystem.class, new ComponentAdderSystem()); systems.put(ComponentReaderSystem.class, new ComponentReaderSystem(queryResults)); final DAGWorld world = new DAGWorld<>(systems); final Entity entity = world.createEntity(); entity.add(new PositionComponent()); world.update("state", Duration.ZERO); assertEquals(1, queryResults.size()); assertEquals(Set.of(entity), queryResults.getFirst()); } @Test public void circularDependencyDetectionThrowsIllegalStateException() { final Map>, System> 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 { private final List log; private final String label; @Override public final void update(final World world, final String state, final Duration duration) { log.add(label); } } private static final class SystemA extends RecordingSystem { private SystemA(final List log) { super(log, "A"); } @Override public Collection>> getDependencies() { return List.of(); } } private static final class SystemB extends RecordingSystem { private SystemB(final List log) { super(log, "B"); } @Override public Collection>> getDependencies() { return Set.of(SystemA.class); } } private static final class SystemC extends RecordingSystem { private SystemC(final List log) { super(log, "C"); } @Override public Collection>> getDependencies() { return Set.of(SystemB.class); } } private static final class SystemCycleA implements System { @Override public Collection>> getDependencies() { return Set.of(SystemCycleB.class); } @Override public void update(final World world, final String state, final Duration duration) { } } private static final class SystemCycleB implements System { @Override public Collection>> getDependencies() { return Set.of(SystemCycleA.class); } @Override public void update(final World world, final String state, final Duration duration) { } } private static final class ComponentAdderSystem implements System { @Override public Collection>> getDependencies() { return List.of(); } @Override public void update(final World world, final String state, final Duration duration) { world.query(Set.of(PositionComponent.class)).forEach(e -> e.add(new VelocityComponent())); } } @RequiredArgsConstructor private static final class ComponentReaderSystem implements System { private final List> queryResults; @Override public Collection>> getDependencies() { return Set.of(ComponentAdderSystem.class); } @Override public void update(final World world, final String state, final Duration duration) { queryResults.add(world.query(Set.of(VelocityComponent.class, PositionComponent.class))); } } }