diff --git a/build.gradle b/build.gradle index 92579ba541..3ee56f9ad0 100644 --- a/build.gradle +++ b/build.gradle @@ -190,8 +190,9 @@ project(":tests"){ dependencies { testImplementation project(":core") - testImplementation('org.junit.jupiter:junit-jupiter-api:5.1.0') - testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.1.0') + testImplementation "org.junit.jupiter:junit-jupiter-params:5.3.1" + testImplementation "org.junit.jupiter:junit-jupiter-api:5.3.1" + testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.3.1" testImplementation "com.badlogicgames.gdx:gdx-backend-headless:$gdxVersion" testImplementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop" } diff --git a/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java b/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java index 0afbe9d2da..b17493349a 100644 --- a/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java +++ b/core/src/io/anuke/mindustry/world/blocks/power/PowerGraph.java @@ -99,15 +99,22 @@ public class PowerGraph{ } public void distributePower(float needed, float produced){ - if(MathUtils.isEqual(needed,0f)){ return; } + if(MathUtils.isEqual(needed, 0f)){ return; } float coverage = Math.min(1, produced / needed); for(Tile consumer : consumers){ Consumers consumes = consumer.block().consumes; - if(consumes.has(ConsumePower.class) && consumes.get(ConsumePower.class).isBuffered){ - consumer.entity.power.satisfaction += (1 - consumer.entity.power.satisfaction) * coverage; - }else{ - consumer.entity.power.satisfaction = coverage; + if(consumes.has(ConsumePower.class)){ + ConsumePower consumePower = consumes.get(ConsumePower.class); + if(consumePower.isBuffered){ + // Add a percentage of the requested amount, but limit it to the mission amount. + // TODO This can maybe be calculated without converting to absolute values first + float maximumRate = consumePower.requestedPower(consumer.block(), consumer.entity()) * coverage; + float missingAmount = consumePower.powerCapacity * (1 - consumer.entity.power.satisfaction); + consumer.entity.power.satisfaction += Math.min(missingAmount, maximumRate) / consumePower.powerCapacity; + }else{ + consumer.entity.power.satisfaction = coverage; + } } } } @@ -205,12 +212,12 @@ public class PowerGraph{ @Override public String toString(){ return "PowerGraph{" + - "producers=" + producers + - ", consumers=" + consumers + - ", batteries=" + batteries + - ", all=" + all + - ", lastFrameUpdated=" + lastFrameUpdated + - ", graphID=" + graphID + - '}'; + "producers=" + producers + + ", consumers=" + consumers + + ", batteries=" + batteries + + ", all=" + all + + ", lastFrameUpdated=" + lastFrameUpdated + + ", graphID=" + graphID + + '}'; } } diff --git a/core/src/io/anuke/mindustry/world/modules/PowerModule.java b/core/src/io/anuke/mindustry/world/modules/PowerModule.java index dead0056e5..edef76ba90 100644 --- a/core/src/io/anuke/mindustry/world/modules/PowerModule.java +++ b/core/src/io/anuke/mindustry/world/modules/PowerModule.java @@ -12,7 +12,7 @@ public class PowerModule extends BlockModule{ * Blocks will work at a reduced efficiency if this is not equal to 1.0f. * In case of buffered consumers, this is the percentage of power stored in relation to the maximum capacity. */ - public float satisfaction; + public float satisfaction = 0.0f; /** Specifies power which is required additionally, e.g. while a force projector is being shot at. */ public float extraUse = 0f; public PowerGraph graph = new PowerGraph(); diff --git a/tests/src/test/java/FakeThreadHandler.java b/tests/src/test/java/FakeThreadHandler.java new file mode 100644 index 0000000000..3588205e98 --- /dev/null +++ b/tests/src/test/java/FakeThreadHandler.java @@ -0,0 +1,19 @@ +import com.badlogic.gdx.Gdx; +import io.anuke.mindustry.core.ThreadHandler; +import io.anuke.ucore.core.Timers; + +/** Fake thread handler which produces a new frame each time getFrameID is called and always provides a delta of 1. */ +public class FakeThreadHandler extends ThreadHandler{ + private int fakeFrameId = 0; + + FakeThreadHandler(){ + super(); + + Timers.setDeltaProvider(() -> 1.0f); + } + @Override + public long getFrameID(){ + return ++fakeFrameId; + } + +} diff --git a/tests/src/test/java/PowerTestFixture.java b/tests/src/test/java/PowerTestFixture.java new file mode 100644 index 0000000000..2ecd5a7dc7 --- /dev/null +++ b/tests/src/test/java/PowerTestFixture.java @@ -0,0 +1,65 @@ +import io.anuke.mindustry.content.blocks.Blocks; +import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.Tile; +import io.anuke.mindustry.world.blocks.Floor; +import io.anuke.mindustry.world.blocks.power.Battery; +import io.anuke.mindustry.world.blocks.power.PowerGenerator; +import io.anuke.mindustry.world.modules.PowerModule; + +import java.lang.reflect.Field; + +/** This class provides objects commonly used by power related unit tests. + * For now, this is a helper with static methods, but this might change. + * */ +public class PowerTestFixture{ + + protected static PowerGenerator createFakeProducerBlock(float producedPower){ + return new PowerGenerator("fakegen"){{ + powerProduction = producedPower; + }}; + } + + protected static Battery createFakeBattery(float capacity, float ticksToFill){ + return new Battery("fakebattery"){{ + consumes.powerBuffered(capacity, ticksToFill); + }}; + } + + protected static Block createFakeDirectConsumer(float powerPerTick, float minimumSatisfaction){ + return new Block("fakedirectconsumer"){{ + consumes.powerDirect(powerPerTick, minimumSatisfaction); + }}; + } + + protected static Block createFakeBufferedConsumer(float capacity, float ticksToFill){ + return new Block("fakebufferedconsumer"){{ + consumes.powerBuffered(capacity, ticksToFill); + }}; + } + /** + * Creates a fake tile on the given location using the given block. + * @param x The X coordinate. + * @param y The y coordinate. + * @param block The block on the tile. + * @return The created tile or null in case of exceptions. + */ + protected static Tile createFakeTile(int x, int y, Block block){ + try{ + Tile tile = new Tile(x, y); + + Field field = Tile.class.getDeclaredField("wall"); + field.setAccessible(true); + field.set(tile, block); + + field = Tile.class.getDeclaredField("floor"); + field.setAccessible(true); + field.set(tile, Blocks.sand); + + tile.entity = block.newEntity(); + tile.entity.power = new PowerModule(); + return tile; + }catch(Exception ex){ + return null; + } + } +} diff --git a/tests/src/test/java/PowerTests.java b/tests/src/test/java/PowerTests.java index c6b4540f55..096e2d126a 100644 --- a/tests/src/test/java/PowerTests.java +++ b/tests/src/test/java/PowerTests.java @@ -1,126 +1,105 @@ +import com.badlogic.gdx.math.MathUtils; import io.anuke.mindustry.Vars; -import io.anuke.mindustry.content.blocks.Blocks; import io.anuke.mindustry.content.blocks.PowerBlocks; import io.anuke.mindustry.content.blocks.ProductionBlocks; import io.anuke.mindustry.core.ContentLoader; -import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.blocks.Floor; import io.anuke.mindustry.world.blocks.power.PowerGraph; -import io.anuke.mindustry.world.blocks.production.SolidPump; -import io.anuke.mindustry.world.modules.PowerModule; import org.junit.jupiter.api.*; +import org.junit.jupiter.params.provider.ValueSource; +import org.junit.jupiter.params.ParameterizedTest; -import java.lang.reflect.Field; -import java.util.LinkedList; -import java.util.List; - -import static io.anuke.mindustry.Vars.threads; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; -public class PowerTests{ +public class PowerTests extends PowerTestFixture{ @BeforeAll static void initializeDependencies(){ Vars.content = new ContentLoader(); Vars.content.load(); + Vars.threads = new FakeThreadHandler(); } @BeforeEach void initTest(){ } - /** - * Creates a fake tile on the given location using the given floor and block. - * @param x The X coordinate. - * @param y The y coordinate. - * @param floor The floor. - * @param block The block on the tile. - * @return The created tile or null in case of exceptions. - */ - private static Tile createFakeTile(int x, int y, Block block){ - try{ - Tile tile = new Tile(x, y); - Field field = Tile.class.getDeclaredField("wall"); - field.setAccessible(true); - field.set(tile, block); - field = Tile.class.getDeclaredField("floor"); - field.setAccessible(true); - field.set(tile, (Floor)Blocks.sand); - tile.entity = block.newEntity(); - tile.entity.power = new PowerModule(); - return tile; - }catch(Exception ex){ - return null; + @Nested + class PowerGraphTests{ + + /** Tests the satisfaction of a single consumer after a single update of the power graph which contains a single producer. + * + * Assumption: When the consumer requests zero power, satisfaction does not change. Default is 0.0f. + */ + @TestFactory + DynamicTest[] testDirectConsumption(){ + return new DynamicTest[]{ + // Note: Unfortunately, the display names are not yet output through gradle. See https://github.com/gradle/gradle/issues/5975 + // That's why we inject the description into the test method for now. + dynamicTest("01", () -> test_directConsumptionCalculation(0.0f, 1.0f, 0.0f, "0.0 produced, 1.0 consumed (no power available)")), + dynamicTest("02", () -> test_directConsumptionCalculation(0.0f, 0.0f, 0.0f, "0.0 produced, 0.0 consumed (no power anywhere)")), + dynamicTest("03", () -> test_directConsumptionCalculation(1.0f, 0.0f, 0.0f, "1.0 produced, 0.0 consumed (no power requested)")), + dynamicTest("04", () -> test_directConsumptionCalculation(1.0f, 1.0f, 1.0f, "1.0 produced, 1.0 consumed (stable consumption)")), + dynamicTest("05", () -> test_directConsumptionCalculation(0.5f, 1.0f, 0.5f, "0.5 produced, 1.0 consumed (power shortage)")), + dynamicTest("06", () -> test_directConsumptionCalculation(1.0f, 0.5f, 1.0f, "1.0 produced, 0.5 consumed (power excess)")), + dynamicTest("07", () -> test_directConsumptionCalculation(0.09f, 0.09f - MathUtils.FLOAT_ROUNDING_ERROR / 10.0f, 1.0f, "floating point inaccuracy (stable consumption)")) + }; } - } - private static final float epsilon = 0.00001f; + void test_directConsumptionCalculation(float producedPower, float requiredPower, float expectedSatisfaction, String parameterDescription){ + Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower)); + Tile directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requiredPower, 0.6f)); - /** Makes sure calculations are accurate for the case where produced power = consumed power. */ - @Test - void test_balancedPower(){ - PowerGraph powerGraph = new PowerGraph(); + PowerGraph powerGraph = new PowerGraph(); + powerGraph.add(producerTile); + powerGraph.add(directConsumerTile); - // Create one water extractor (5.4 power/Second = 0.09/tick) - Tile waterExtractorTile = createFakeTile(0, 0, ProductionBlocks.waterExtractor); - powerGraph.add(waterExtractorTile); + assumeTrue(MathUtils.isEqual(producedPower, powerGraph.getPowerProduced())); + assumeTrue(MathUtils.isEqual(requiredPower, powerGraph.getPowerNeeded())); - // Create 20 small solar panels (20*0.27=5.4 power/second = 0.09/tick) - List solarPanelTiles = new LinkedList<>(); - for(int counter = 0; counter < 20; counter++){ - Tile solarPanelTile = createFakeTile( 2 + counter / 2, counter % 2, PowerBlocks.solarPanel); - powerGraph.add(solarPanelTile); - solarPanelTiles.add(solarPanelTile); + // Update and check for the expected power satisfaction of the consumer + powerGraph.update(); + assertEquals(expectedSatisfaction, directConsumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match"); } - float powerNeeded = powerGraph.getPowerNeeded(); - float powerProduced = powerGraph.getPowerProduced(); + /** Tests the satisfaction of a single buffered consumer after a single update of the power graph which contains a single producer. */ + @TestFactory + DynamicTest[] testBufferedConsumption(){ + return new DynamicTest[]{ + // Note: powerPerTick may not be 0 in any of the test cases. This would equal a "ticksToFill" of infinite. + dynamicTest("01", () -> test_bufferedConsumptionCalculation(0.0f, 0.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power anywhere")), + dynamicTest("02", () -> test_bufferedConsumptionCalculation(0.0f, 1.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power provided")), + dynamicTest("03", () -> test_bufferedConsumptionCalculation(1.0f, 0.0f, 0.1f, 0.0f, 0.0f, "Empty Buffer, No power requested")), + dynamicTest("04", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 1.0f, 0.0f, 1.0f, "Empty Buffer, Stable Power, One tick to fill")), + dynamicTest("05", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.1f, 0.0f, 0.1f, "Empty Buffer, Stable Power, multiple ticks to fill")), + dynamicTest("06", () -> test_bufferedConsumptionCalculation(1.0f, 0.5f, 0.5f, 0.0f, 1.0f, "Empty Buffer, Power excess, one tick to fill")), + dynamicTest("07", () -> test_bufferedConsumptionCalculation(1.0f, 0.5f, 0.1f, 0.0f, 0.2f, "Empty Buffer, Power excess, multiple ticks to fill")), + dynamicTest("08", () -> test_bufferedConsumptionCalculation(0.5f, 1.0f, 1.0f, 0.0f, 0.5f, "Empty Buffer, Power shortage, one tick to fill")), + dynamicTest("09", () -> test_bufferedConsumptionCalculation(0.5f, 1.0f, 0.1f, 0.0f, 0.1f, "Empty Buffer, Power shortage, multiple ticks to fill")), + dynamicTest("10", () -> test_bufferedConsumptionCalculation(0.0f, 1.0f, 0.1f, 0.5f, 0.5f, "Unchanged buffer with no power produced")), + dynamicTest("11", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.1f, 1.0f, 1.0f, "Unchanged buffer when already full")), + dynamicTest("12", () -> test_bufferedConsumptionCalculation(0.2f, 1.0f, 0.5f, 0.5f, 0.7f, "Half buffer, power shortage")), + dynamicTest("13", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.5f, 0.7f, 1.0f, "Buffer does not get exceeded")), + dynamicTest("14", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.5f, 0.5f, 1.0f, "Half buffer, filled with excess")) + }; + } + void test_bufferedConsumptionCalculation(float producedPower, float maxBuffer, float powerPerTick, float initialSatisfaction, float expectedSatisfaction, String parameterDescription){ + Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower)); + Tile bufferedConsumerTile = createFakeTile(0, 1, createFakeBufferedConsumer(maxBuffer, maxBuffer > 0.0f ? maxBuffer/powerPerTick : 1.0f)); + bufferedConsumerTile.entity.power.satisfaction = initialSatisfaction; - // If these lines fail, you probably changed power production/consumption and need to adapt this test - // OR their implementation is corrupt. - // TODO: Create fake blocks which are independent of such changes - assertEquals(powerNeeded, 0.09f, epsilon); - assertEquals(powerProduced, 0.09f, epsilon); - // Note: The assertions above induce that powerNeeded = powerProduced (with floating point inaccuracy) + PowerGraph powerGraph = new PowerGraph(); + powerGraph.add(producerTile); + powerGraph.add(bufferedConsumerTile); - // Distribute power and make sure the water extractor is powered - powerGraph.distributePower(powerNeeded, powerProduced); - assertEquals(waterExtractorTile.entity.power.satisfaction, 1.0f, epsilon); - } + assumeTrue(MathUtils.isEqual(producedPower, powerGraph.getPowerProduced())); + //assumeTrue(MathUtils.isEqual(Math.min(maxBuffer, powerPerTick), powerGraph.getPowerNeeded())); - /** Makes sure there are no problems with zero production. */ - @Test - void test_noProducers(){ - PowerGraph powerGraph = new PowerGraph(); - - Tile waterExtractorTile = createFakeTile(0, 0, ProductionBlocks.waterExtractor); - powerGraph.add(waterExtractorTile); - - float powerNeeded = powerGraph.getPowerNeeded(); - float powerProduced = powerGraph.getPowerProduced(); - - assertEquals(powerGraph.getPowerNeeded(), 0.09f, epsilon); - assertEquals(powerGraph.getPowerProduced(), 0.0f, epsilon); - - powerGraph.distributePower(powerNeeded, powerProduced); - assertEquals(waterExtractorTile.entity.power.satisfaction, 0.0f, epsilon); - } - - /** Makes sure there are no problems with zero consumers. */ - @Test - void test_noConsumers(){ - PowerGraph powerGraph = new PowerGraph(); - - Tile solarPanelTile = createFakeTile( 0, 0, PowerBlocks.solarPanel); - powerGraph.add(solarPanelTile); - - float powerNeeded = powerGraph.getPowerNeeded(); - float powerProduced = powerGraph.getPowerProduced(); - - assertEquals(powerGraph.getPowerNeeded(), 0.0f, epsilon); - assertEquals(powerGraph.getPowerProduced(), 0.0045f, epsilon); - - powerGraph.distributePower(powerNeeded, powerProduced); + // Update and check for the expected power satisfaction of the consumer + powerGraph.update(); + assertEquals(expectedSatisfaction, bufferedConsumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of buffered consumer did not match"); + } } }