@@ -190,8 +190,9 @@ project(":tests"){
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
testImplementation project(":core")
|
testImplementation project(":core")
|
||||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.1.0')
|
testImplementation "org.junit.jupiter:junit-jupiter-params:5.3.1"
|
||||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.1.0')
|
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-backend-headless:$gdxVersion"
|
||||||
testImplementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop"
|
testImplementation "com.badlogicgames.gdx:gdx-platform:$gdxVersion:natives-desktop"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,15 +99,22 @@ public class PowerGraph{
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void distributePower(float needed, float produced){
|
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);
|
float coverage = Math.min(1, produced / needed);
|
||||||
for(Tile consumer : consumers){
|
for(Tile consumer : consumers){
|
||||||
Consumers consumes = consumer.block().consumes;
|
Consumers consumes = consumer.block().consumes;
|
||||||
if(consumes.has(ConsumePower.class) && consumes.get(ConsumePower.class).isBuffered){
|
if(consumes.has(ConsumePower.class)){
|
||||||
consumer.entity.power.satisfaction += (1 - consumer.entity.power.satisfaction) * coverage;
|
ConsumePower consumePower = consumes.get(ConsumePower.class);
|
||||||
}else{
|
if(consumePower.isBuffered){
|
||||||
consumer.entity.power.satisfaction = coverage;
|
// 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
|
@Override
|
||||||
public String toString(){
|
public String toString(){
|
||||||
return "PowerGraph{" +
|
return "PowerGraph{" +
|
||||||
"producers=" + producers +
|
"producers=" + producers +
|
||||||
", consumers=" + consumers +
|
", consumers=" + consumers +
|
||||||
", batteries=" + batteries +
|
", batteries=" + batteries +
|
||||||
", all=" + all +
|
", all=" + all +
|
||||||
", lastFrameUpdated=" + lastFrameUpdated +
|
", lastFrameUpdated=" + lastFrameUpdated +
|
||||||
", graphID=" + graphID +
|
", graphID=" + graphID +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public class PowerModule extends BlockModule{
|
|||||||
* Blocks will work at a reduced efficiency if this is not equal to 1.0f.
|
* 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.
|
* 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. */
|
/** Specifies power which is required additionally, e.g. while a force projector is being shot at. */
|
||||||
public float extraUse = 0f;
|
public float extraUse = 0f;
|
||||||
public PowerGraph graph = new PowerGraph();
|
public PowerGraph graph = new PowerGraph();
|
||||||
|
|||||||
19
tests/src/test/java/FakeThreadHandler.java
Normal file
19
tests/src/test/java/FakeThreadHandler.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
65
tests/src/test/java/PowerTestFixture.java
Normal file
65
tests/src/test/java/PowerTestFixture.java
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,91 +1,105 @@
|
|||||||
|
import com.badlogic.gdx.math.MathUtils;
|
||||||
import io.anuke.mindustry.Vars;
|
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.PowerBlocks;
|
||||||
import io.anuke.mindustry.content.blocks.ProductionBlocks;
|
import io.anuke.mindustry.content.blocks.ProductionBlocks;
|
||||||
import io.anuke.mindustry.core.ContentLoader;
|
import io.anuke.mindustry.core.ContentLoader;
|
||||||
import io.anuke.mindustry.world.Block;
|
|
||||||
import io.anuke.mindustry.world.Tile;
|
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.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.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.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
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
|
@BeforeAll
|
||||||
static void initializeDependencies(){
|
static void initializeDependencies(){
|
||||||
Vars.content = new ContentLoader();
|
Vars.content = new ContentLoader();
|
||||||
Vars.content.load();
|
Vars.content.load();
|
||||||
|
Vars.threads = new FakeThreadHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void initTest(){
|
void initTest(){
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
@Nested
|
||||||
* Creates a fake tile on the given location using the given floor and block.
|
class PowerGraphTests{
|
||||||
* @param x The X coordinate.
|
|
||||||
* @param y The y coordinate.
|
/** Tests the satisfaction of a single consumer after a single update of the power graph which contains a single producer.
|
||||||
* @param floor The floor.
|
*
|
||||||
* @param block The block on the tile.
|
* Assumption: When the consumer requests zero power, satisfaction does not change. Default is 0.0f.
|
||||||
* @return The created tile or null in case of exceptions.
|
*/
|
||||||
*/
|
@TestFactory
|
||||||
private static Tile createFakeTile(int x, int y, Floor floor, Block block){
|
DynamicTest[] testDirectConsumption(){
|
||||||
try{
|
return new DynamicTest[]{
|
||||||
Tile tile = new Tile(x, y);
|
// Note: Unfortunately, the display names are not yet output through gradle. See https://github.com/gradle/gradle/issues/5975
|
||||||
Field field = Tile.class.getDeclaredField("wall");
|
// That's why we inject the description into the test method for now.
|
||||||
field.setAccessible(true);
|
dynamicTest("01", () -> test_directConsumptionCalculation(0.0f, 1.0f, 0.0f, "0.0 produced, 1.0 consumed (no power available)")),
|
||||||
field.set(tile, block);
|
dynamicTest("02", () -> test_directConsumptionCalculation(0.0f, 0.0f, 0.0f, "0.0 produced, 0.0 consumed (no power anywhere)")),
|
||||||
field = Tile.class.getDeclaredField("floor");
|
dynamicTest("03", () -> test_directConsumptionCalculation(1.0f, 0.0f, 0.0f, "1.0 produced, 0.0 consumed (no power requested)")),
|
||||||
field.setAccessible(true);
|
dynamicTest("04", () -> test_directConsumptionCalculation(1.0f, 1.0f, 1.0f, "1.0 produced, 1.0 consumed (stable consumption)")),
|
||||||
field.set(tile, floor);
|
dynamicTest("05", () -> test_directConsumptionCalculation(0.5f, 1.0f, 0.5f, "0.5 produced, 1.0 consumed (power shortage)")),
|
||||||
tile.entity = block.newEntity();
|
dynamicTest("06", () -> test_directConsumptionCalculation(1.0f, 0.5f, 1.0f, "1.0 produced, 0.5 consumed (power excess)")),
|
||||||
tile.entity.power = new PowerModule();
|
dynamicTest("07", () -> test_directConsumptionCalculation(0.09f, 0.09f - MathUtils.FLOAT_ROUNDING_ERROR / 10.0f, 1.0f, "floating point inaccuracy (stable consumption)"))
|
||||||
return tile;
|
};
|
||||||
}catch(Exception ex){
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
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. */
|
PowerGraph powerGraph = new PowerGraph();
|
||||||
@Test
|
powerGraph.add(producerTile);
|
||||||
void test_balancedPower(){
|
powerGraph.add(directConsumerTile);
|
||||||
PowerGraph powerGraph = new PowerGraph();
|
|
||||||
|
|
||||||
// Create one water extractor (5.4 power/Second = 0.09/tick)
|
assumeTrue(MathUtils.isEqual(producedPower, powerGraph.getPowerProduced()));
|
||||||
Tile waterExtractorTile = createFakeTile(0, 0, (Floor)Blocks.sand, ProductionBlocks.waterExtractor);
|
assumeTrue(MathUtils.isEqual(requiredPower, powerGraph.getPowerNeeded()));
|
||||||
powerGraph.add(waterExtractorTile);
|
|
||||||
|
|
||||||
// Create 20 small solar panels (20*0.27=5.4 power/second = 0.09/tick)
|
// Update and check for the expected power satisfaction of the consumer
|
||||||
List<Tile> solarPanelTiles = new LinkedList<>();
|
powerGraph.update();
|
||||||
for(int counter = 0; counter < 20; counter++){
|
assertEquals(expectedSatisfaction, directConsumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match");
|
||||||
Tile solarPanelTile = createFakeTile( 2 + counter / 2, counter % 2, (Floor)Blocks.sand, PowerBlocks.solarPanel);
|
|
||||||
powerGraph.add(solarPanelTile);
|
|
||||||
solarPanelTiles.add(solarPanelTile);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
float powerNeeded = powerGraph.getPowerNeeded();
|
/** Tests the satisfaction of a single buffered consumer after a single update of the power graph which contains a single producer. */
|
||||||
float powerProduced = powerGraph.getPowerProduced();
|
@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
|
PowerGraph powerGraph = new PowerGraph();
|
||||||
// OR their implementation is corrupt.
|
powerGraph.add(producerTile);
|
||||||
// TODO: Create fake blocks which are independent of such changes
|
powerGraph.add(bufferedConsumerTile);
|
||||||
float epsilon = 0.00001f;
|
|
||||||
assertEquals(powerNeeded, 0.09f, epsilon);
|
|
||||||
assertEquals(powerProduced, 0.09f, epsilon);
|
|
||||||
// Note: The assertions above induce that powerNeeded = powerProduced (with floating point inaccuracy)
|
|
||||||
|
|
||||||
// Distribute power and make sure the water extractor is powered
|
assumeTrue(MathUtils.isEqual(producedPower, powerGraph.getPowerProduced()));
|
||||||
powerGraph.distributePower(powerNeeded, powerProduced);
|
//assumeTrue(MathUtils.isEqual(Math.min(maxBuffer, powerPerTick), powerGraph.getPowerNeeded()));
|
||||||
assertEquals(waterExtractorTile.entity.power.satisfaction, 1.0f, epsilon);
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user