1
0
mirror of https://github.com/Anuken/Mindustry.git synced 2024-11-10 15:05:23 +03:00

Merge pull request #7 from Timmeey86/baltitenger

Adapted ItemLiquidGenerator to new power system
This commit is contained in:
Baltazár Radics 2018-11-28 17:14:19 +01:00 committed by GitHub
commit 630693d64e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 388 additions and 141 deletions

View File

@ -4,6 +4,7 @@ import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.content.fx.BlockFx;
import io.anuke.mindustry.game.ContentList;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.power.*;
public class PowerBlocks extends BlockList implements ContentList{
@ -19,17 +20,17 @@ public class PowerBlocks extends BlockList implements ContentList{
thermalGenerator = new LiquidHeatGenerator("thermal-generator"){{
maxLiquidGenerate = 4f;
// TODO: Adapt to new power system
powerProduction = -1;
powerPerLiquid = 0.1f;
// TODO: Balance
powerProduction = 0.17f;
liquidPowerMultiplier = 0.1f;
generateEffect = BlockFx.redgeneratespark;
size = 2;
}};
turbineGenerator = new TurbineGenerator("turbine-generator"){{
// TODO: Adapt to new power system
// TODO: Balance
powerProduction = 0.28f;
powerPerLiquid = 0.1f;
liquidPowerMultiplier = 0.3f;
itemDuration = 30f;
consumes.liquid(Liquids.water, 0.05f);
size = 2;
@ -41,14 +42,26 @@ public class PowerBlocks extends BlockList implements ContentList{
itemDuration = 220f;
}};
solarPanel = new PowerGenerator("solar-panel"){{
powerProduction = 0.0045f;
}};
// TODO: Maybe reintroduce a class for the initial production efficiency
solarPanel = new PowerGenerator("solar-panel"){
{
powerProduction = 0.0045f;
}
@Override
public void update(Tile tile){
tile.<GeneratorEntity>entity().productionEfficiency = 1.0f;
}
};
largeSolarPanel = new PowerGenerator("solar-panel-large"){{
powerProduction = 0.055f;
size = 3;
}};
largeSolarPanel = new PowerGenerator("solar-panel-large"){
{
powerProduction = 0.055f;
}
@Override
public void update(Tile tile){
tile.<GeneratorEntity>entity().productionEfficiency = 1.0f;
}
};
thoriumReactor = new NuclearReactor("thorium-reactor"){{
size = 3;

View File

@ -337,9 +337,8 @@ public class Block extends BaseBlock {
}
public void setBars(){
if(consumes.has(ConsumePower.class)){
if(consumes.has(ConsumePower.class))
bars.add(new BlockBar(BarType.power, true, tile -> tile.entity.power.satisfaction));
}
if(hasLiquids)
bars.add(new BlockBar(BarType.liquid, true, tile -> tile.entity.liquids.total() / liquidCapacity));
if(hasItems)

View File

@ -14,7 +14,7 @@ import static io.anuke.mindustry.Vars.tilesize;
public abstract class ItemLiquidGenerator extends ItemGenerator{
protected float minLiquidEfficiency = 0.2f;
protected float powerPerLiquid = 0.13f;
protected float liquidPowerMultiplier = 1.3f; // A liquid with 100% flammability will be 30% more efficient than an item with 100% flammability.
/**Maximum liquid used per frame.*/
protected float maxLiquidGenerate = 0.4f;
@ -45,44 +45,48 @@ public abstract class ItemLiquidGenerator extends ItemGenerator{
}
}
// Note: Do not use this delta when calculating the amount of power or the power efficiency, but use it for resource consumption if necessary.
// Power amount is delta'd by PowerGraph class already.
float calculationDelta = entity.delta();
if(!entity.cons.valid()){
entity.productionEfficiency = 0.0f;
return;
}
//liquid takes priority over solids
if(liquid != null && entity.liquids.get(liquid) >= 0.001f && entity.cons.valid()){
float powerPerLiquid = getLiquidEfficiency(liquid) * this.powerPerLiquid;
float used = Math.min(entity.liquids.get(liquid), maxLiquidGenerate * entity.delta());
// TODO: Adapt to new power system
//used = Math.min(used, (powerCapacity - entity.power.amount) / powerPerLiquid);
if(liquid != null && entity.liquids.get(liquid) >= 0.001f){
float baseLiquidEfficiency = getLiquidEfficiency(liquid) * this.liquidPowerMultiplier;
float maximumPossible = maxLiquidGenerate * calculationDelta;
float used = Math.min(entity.liquids.get(liquid) * calculationDelta, maximumPossible);
entity.liquids.remove(liquid, used);
// TODO: Adapt to new power system
//entity.power.amount += used * powerPerLiquid;
// Note: 1 Item with 100% Flammability = 100% efficiency. This means 100% is not max but rather a reference point for this generator.
entity.productionEfficiency = baseLiquidEfficiency * used / maximumPossible;
if(used > 0.001f && Mathf.chance(0.05 * entity.delta())){
Effects.effect(generateEffect, tile.drawx() + Mathf.range(3f), tile.drawy() + Mathf.range(3f));
}
}else if(entity.cons.valid()){
// TODO: Adapt to new power system
//float maxPower = Math.min(powerCapacity - entity.power.amount, powerOutput * entity.delta()) * entity.efficiency;
}else{
if(entity.generateTime <= 0f && entity.items.total() > 0){
Effects.effect(generateEffect, tile.worldx() + Mathf.range(3f), tile.worldy() + Mathf.range(3f));
Item item = entity.items.take();
// TODO: Adapt to new power system
//entity.efficiency = getItemEfficiency(item);
entity.productionEfficiency = getItemEfficiency(item);
entity.explosiveness = item.explosiveness;
entity.generateTime = 1f;
}
if(entity.generateTime > 0f){
entity.generateTime -= 1f / itemDuration * entity.delta();
// TODO: Adapt to new power system
//entity.power.amount += maxPower;
entity.generateTime = Mathf.clamp(entity.generateTime);
if(Mathf.chance(entity.delta() * 0.06 * Mathf.clamp(entity.explosiveness - 0.25f))){
entity.damage(Mathf.random(8f));
Effects.effect(explodeEffect, tile.worldx() + Mathf.range(size * tilesize / 2f), tile.worldy() + Mathf.range(size * tilesize / 2f));
}
}else{
entity.productionEfficiency = 0.0f;
}
}
}

View File

@ -13,7 +13,7 @@ import io.anuke.ucore.util.Mathf;
public abstract class LiquidGenerator extends PowerGenerator{
protected float minEfficiency = 0.2f;
protected float powerPerLiquid;
protected float liquidPowerMultiplier;
/**Maximum liquid used per frame.*/
protected float maxLiquidGenerate;
protected Effect generateEffect = BlockFx.generatespark;
@ -50,8 +50,9 @@ public abstract class LiquidGenerator extends PowerGenerator{
public void update(Tile tile){
TileEntity entity = tile.entity();
// TODO Code duplication with ItemLiquidGenerator
if(entity.liquids.get(entity.liquids.current()) >= 0.001f){
float powerPerLiquid = getEfficiency(entity.liquids.current()) * this.powerPerLiquid;
//float powerPerLiquid = getEfficiency(entity.liquids.current()) * this.powerPerLiquid;
float used = Math.min(entity.liquids.currentAmount(), maxLiquidGenerate * entity.delta());
// TODO Adapt to new power system
//used = Math.min(used, (powerCapacity - entity.power.amount) / powerPerLiquid);

View File

@ -14,9 +14,9 @@ public class LiquidHeatGenerator extends LiquidGenerator{
public void setStats(){
super.setStats();
// TODO Verify for new power system
stats.remove(BlockStat.basePowerGeneration);
stats.add(BlockStat.basePowerGeneration, maxLiquidGenerate * powerPerLiquid * 60f, StatUnit.powerSecond);
// TODO Adapt to new new power system. Maybe this override can be removed.
//stats.add(BlockStat.basePowerGeneration, <Do something with maxLiquidGenerate, basePowerGeneration and liquidPowerMultiplier> * 60f, StatUnit.powerSecond);
}
@Override

View File

@ -1,5 +1,7 @@
package io.anuke.mindustry.world.blocks.power;
import io.anuke.mindustry.world.BarType;
import io.anuke.mindustry.world.meta.BlockBar;
import io.anuke.mindustry.world.meta.StatUnit;
import io.anuke.ucore.util.EnumSet;
@ -10,7 +12,12 @@ import io.anuke.mindustry.world.meta.BlockStat;
public class PowerGenerator extends PowerDistributor{
/** The amount of power produced per tick. */
public float powerProduction;
protected float powerProduction;
/** The maximum possible efficiency for this generator. Supply values larger than 1.0f if more than 100% is possible.
* This could be the case when e.g. an item with 100% flammability is the reference point, but a more effective liquid
* can be supplied as an alternative.
*/
protected float maxEfficiency = 1.0f;
public BlockStat generationType = BlockStat.basePowerGeneration;
public PowerGenerator(String name){
@ -40,8 +47,16 @@ public class PowerGenerator extends PowerDistributor{
return new GeneratorEntity();
}
@Override
public void setBars(){
super.setBars();
if(hasPower){
bars.add(new BlockBar(BarType.power, true, tile -> tile.<GeneratorEntity>entity().productionEfficiency / maxEfficiency));
}
}
public static class GeneratorEntity extends TileEntity{
public float generateTime;
public float productionEfficiency = 1;
public float productionEfficiency = 0.0f;
}
}

View File

@ -121,7 +121,7 @@ public class PowerGraph{
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 maximumRate = consumePower.requestedPower(consumer.block(), consumer.entity()) * coverage * consumer.entity.delta();
float missingAmount = consumePower.powerCapacity * (1 - consumer.entity.power.satisfaction);
consumer.entity.power.satisfaction += Math.min(missingAmount, maximumRate) / consumePower.powerCapacity;
}else{

View File

@ -86,7 +86,9 @@ public class ConsumePower extends Consume{
* @return The amount of power which is requested per tick.
*/
public float requestedPower(Block block, TileEntity entity){
// TODO Is it possible to make the block not consume power while items/liquids are missing?
// TODO Make the block not consume power on the following conditions, either here or in PowerGraph:
// - Other consumers are not valid, e.g. additional input items/liquids are missing
// - Buffer is full
return powerPerTick;
}

View File

@ -1,65 +0,0 @@
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;
}
}
}

View File

@ -1,15 +1,17 @@
import com.badlogic.gdx.Gdx;
package power;
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;
public static final float fakeDelta = 0.5f;
FakeThreadHandler(){
super();
Timers.setDeltaProvider(() -> 1.0f);
Timers.setDeltaProvider(() -> fakeDelta);
}
@Override
public long getFrameID(){

View File

@ -0,0 +1,142 @@
package power;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.Items;
import io.anuke.mindustry.content.Liquids;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.type.Liquid;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.power.BurnerGenerator;
import io.anuke.mindustry.world.blocks.power.ItemGenerator;
import io.anuke.mindustry.world.blocks.power.ItemLiquidGenerator;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
/**
* This class tests ItemLiquidGenerators. Currently, testing is only performed on the BurnerGenerator subclass,
* which means only power calculations based on flammability are tested.
* All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well.
* Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame.
* Both of these constraints are handled by FakeThreadHandler within PowerTestFixture.
* Any power amount (produced, consumed, buffered) should be affected by FakeThreadHandler.fakeDelta but satisfaction should not!
*/
public class ItemLiquidGeneratorTests extends PowerTestFixture{
private ItemLiquidGenerator generator;
private Tile tile;
private ItemGenerator.ItemGeneratorEntity entity;
private final float fakeLiquidPowerMultiplier = 2.0f;
private final float fakeItemDuration = 60f; // 60 ticks
private final float maximumLiquidUsage = 0.5f;
@BeforeEach
public void createBurnerGenerator(){
// Use a burner generator instead of a custom ItemLiquidGenerator subclass since we would implement abstract methods the same way.
generator = new BurnerGenerator("fakegen"){{
powerProduction = 0.1f;
itemDuration = 60f;
liquidPowerMultiplier = fakeLiquidPowerMultiplier;
itemDuration = fakeItemDuration;
maxLiquidGenerate = maximumLiquidUsage;
}};
tile = createFakeTile(0, 0, generator);
entity = tile.entity();
}
/** Tests the consumption and efficiency when being supplied with liquids. */
@TestFactory
DynamicTest[] testLiquidConsumption(){
return new DynamicTest[]{
dynamicTest("01", () -> test_liquidConsumption(Liquids.oil, 0.0f, "No liquids provided")),
dynamicTest("02", () -> test_liquidConsumption(Liquids.oil, maximumLiquidUsage / 4.0f, "Low oil provided")),
dynamicTest("03", () -> test_liquidConsumption(Liquids.oil, maximumLiquidUsage * 1.0f, "Sufficient oil provided")),
dynamicTest("04", () -> test_liquidConsumption(Liquids.oil, maximumLiquidUsage * 2.0f, "Excess oil provided"))
// Note: The generator will decline any other liquid since it's not flammable
};
}
void test_liquidConsumption(Liquid liquid, float availableLiquidAmount, String parameterDescription){
final float baseEfficiency = fakeLiquidPowerMultiplier * liquid.flammability;
final float expectedEfficiency = Math.min(1.0f, availableLiquidAmount / maximumLiquidUsage) * baseEfficiency;
final float expectedConsumptionPerTick = Math.min(maximumLiquidUsage, availableLiquidAmount);
final float expectedRemainingLiquidAmount = Math.max(0.0f, availableLiquidAmount - expectedConsumptionPerTick * FakeThreadHandler.fakeDelta);
assertTrue(generator.acceptLiquid(tile, null, liquid, availableLiquidAmount), parameterDescription + ": Liquids which will be declined by the generator don't need to be tested - The code won't be called for those cases.");
// Reset liquids since BeforeEach will not be called between dynamic tests
for(Liquid tmpLiquid : Vars.content.liquids()){
entity.liquids.reset(tmpLiquid, 0.0f);
}
entity.liquids.add(liquid, availableLiquidAmount);
entity.cons.update(tile.entity);
assertTrue(entity.cons.valid());
// Perform an update on the generator once - This should use up any resource up to the maximum liquid usage
generator.update(tile);
assertEquals(expectedRemainingLiquidAmount, entity.liquids.get(liquid), parameterDescription + ": Remaining liquid amount mismatch.");
assertEquals(expectedEfficiency, entity.productionEfficiency, parameterDescription + ": Efficiency mismatch.");
}
/** Tests the consumption and efficiency when being supplied with items. */
@TestFactory
DynamicTest[] testItemConsumption(){
return new DynamicTest[]{
dynamicTest("01", () -> test_itemConsumption(Items.coal, 0, "No items provided")),
dynamicTest("02", () -> test_itemConsumption(Items.coal, 1, "Sufficient coal provided")),
dynamicTest("03", () -> test_itemConsumption(Items.coal, 10, "Excess coal provided")),
dynamicTest("04", () -> test_itemConsumption(Items.blastCompound, 1, "Blast compound provided")),
//dynamicTest("03", () -> test_itemConsumption(Items.plastanium, 1, "Plastanium provided")), // Not accepted by generator due to low flammability
dynamicTest("05", () -> test_itemConsumption(Items.biomatter, 1, "Biomatter provided")),
dynamicTest("06", () -> test_itemConsumption(Items.pyratite, 1, "Pyratite provided"))
};
}
void test_itemConsumption(Item item, int amount, String parameterDescription){
final float expectedEfficiency = Math.min(1.0f, amount > 0 ? item.flammability : 0f);
final float expectedRemainingItemAmount = Math.max(0, amount - 1);
assertTrue(generator.acceptItem(item, tile, null), parameterDescription + ": Items which will be declined by the generator don't need to be tested - The code won't be called for those cases.");
// Clean up manually since BeforeEach will not be called between dynamic tests
entity.items.clear();
entity.generateTime = 0.0f;
entity.productionEfficiency = 0.0f;
if(amount > 0){
entity.items.add(item, amount);
}
entity.cons.update(tile.entity);
assertTrue(entity.cons.valid());
// Perform an update on the generator once - This should use up one or zero items - dependent on if the item is accepted and available or not.
generator.update(tile);
assertEquals(expectedRemainingItemAmount, entity.items.get(item), parameterDescription + ": Remaining item amount mismatch.");
assertEquals(expectedEfficiency, entity.productionEfficiency, parameterDescription + ": Efficiency mismatch.");
}
/** Makes sure the efficiency stays equal during the item duration. */
@Test
void test_efficiencyConstantDuringItemDuration(){
// Burn a single coal and test for the duration
entity.items.add(Items.coal, 1);
entity.cons.update(tile.entity);
generator.update(tile);
float expectedEfficiency = entity.productionEfficiency;
float currentDuration = 0.0f;
while((currentDuration += FakeThreadHandler.fakeDelta) <= fakeItemDuration){
generator.update(tile);
assertEquals(expectedEfficiency, entity.productionEfficiency, "Duration: " + String.valueOf(currentDuration));
}
generator.update(tile);
assertEquals(0.0f, entity.productionEfficiency, "Duration: " + String.valueOf(currentDuration));
}
}

View File

@ -0,0 +1,104 @@
package power;
import com.badlogic.gdx.math.MathUtils;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.blocks.Blocks;
import io.anuke.mindustry.core.ContentLoader;
import io.anuke.mindustry.core.World;
import io.anuke.mindustry.entities.TileEntity;
import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.PowerBlock;
import io.anuke.mindustry.world.blocks.power.Battery;
import io.anuke.mindustry.world.blocks.power.PowerGenerator;
import io.anuke.mindustry.world.modules.ConsumeModule;
import io.anuke.mindustry.world.modules.ItemModule;
import io.anuke.mindustry.world.modules.LiquidModule;
import io.anuke.mindustry.world.modules.PowerModule;
import io.anuke.ucore.entities.Entities;
import org.junit.jupiter.api.BeforeAll;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static io.anuke.mindustry.Vars.world;
/** This class provides objects commonly used by power related unit tests.
* For now, this is a helper with static methods, but this might change.
*
* Note: All tests which subclass this will run with a fixed delta of 0.5!
* */
public class PowerTestFixture{
public static final float smallRoundingTolerance = MathUtils.FLOAT_ROUNDING_ERROR;
public static final float mediumRoundingTolerance = MathUtils.FLOAT_ROUNDING_ERROR * 10;
public static final float highRoundingTolerance = MathUtils.FLOAT_ROUNDING_ERROR * 100;
@BeforeAll
static void initializeDependencies(){
Vars.content = new ContentLoader();
Vars.content.load();
Vars.threads = new FakeThreadHandler();
}
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 PowerBlock("fakedirectconsumer"){{
consumes.powerDirect(powerPerTick, minimumSatisfaction);
}};
}
protected static Block createFakeBufferedConsumer(float capacity, float ticksToFill){
return new PowerBlock("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);
// Using the Tile(int, int, byte, byte) constructor would require us to register any fake block or tile we create
// Since this part shall not be part of the test and would require more work anyway, we manually set the block and floor
// through reflections and then simulate part of what the changed() method does.
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);
// Simulate the "changed" method. Calling it through reflections would require half the game to be initialized.
tile.entity = block.newEntity().init(tile, false);
tile.entity.cons = new ConsumeModule();
if(block.hasItems) tile.entity.items = new ItemModule();
if(block.hasLiquids) tile.entity.liquids = new LiquidModule();
if(block.hasPower){
tile.entity.power = new PowerModule();
tile.entity.power.graph.add(tile);
}
return tile;
}catch(Exception ex){
return null;
}
}
}

View File

@ -1,28 +1,26 @@
package power;
import com.badlogic.gdx.math.MathUtils;
import io.anuke.mindustry.Vars;
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.Tile;
import io.anuke.mindustry.world.blocks.power.PowerGenerator;
import io.anuke.mindustry.world.blocks.power.PowerGraph;
import io.anuke.mindustry.world.consumers.ConsumePower;
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.jupiter.params.ParameterizedTest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
/**
* Tests code related to the power system in general, but not specific blocks.
* All tests are run with a fixed delta of 0.5 so delta considerations can be tested as well.
* Additionally, each PowerGraph::update() call will have its own thread frame, i.e. the method will never be called twice within the same frame.
* Both of these constraints are handled by FakeThreadHandler within PowerTestFixture.
* Any power amount (produced, consumed, buffered) should be affected by FakeThreadHandler.fakeDelta but satisfaction should not!
*/
public class PowerTests extends PowerTestFixture{
@BeforeAll
static void initializeDependencies(){
Vars.content = new ContentLoader();
Vars.content.load();
Vars.threads = new FakeThreadHandler();
}
@BeforeEach
void initTest(){
}
@ -39,6 +37,7 @@ public class PowerTests extends PowerTestFixture{
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.
// Additional Note: If you don't see any labels in front of the values supplied as function parameters, use a better IDE like IntelliJ IDEA.
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)")),
@ -50,14 +49,15 @@ public class PowerTests extends PowerTestFixture{
}
void test_directConsumptionCalculation(float producedPower, float requiredPower, float expectedSatisfaction, String parameterDescription){
Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 1.0f;
Tile directConsumerTile = createFakeTile(0, 1, createFakeDirectConsumer(requiredPower, 0.6f));
PowerGraph powerGraph = new PowerGraph();
powerGraph.add(producerTile);
powerGraph.add(directConsumerTile);
assumeTrue(MathUtils.isEqual(producedPower, powerGraph.getPowerProduced()));
assumeTrue(MathUtils.isEqual(requiredPower, powerGraph.getPowerNeeded()));
assertEquals(producedPower * FakeThreadHandler.fakeDelta, powerGraph.getPowerProduced(), MathUtils.FLOAT_ROUNDING_ERROR);
assertEquals(requiredPower * FakeThreadHandler.fakeDelta, powerGraph.getPowerNeeded(), MathUtils.FLOAT_ROUNDING_ERROR);
// Update and check for the expected power satisfaction of the consumer
powerGraph.update();
@ -69,33 +69,36 @@ public class PowerTests extends PowerTestFixture{
DynamicTest[] testBufferedConsumption(){
return new DynamicTest[]{
// Note: powerPerTick may not be 0 in any of the test cases. This would equal a "ticksToFill" of infinite.
// Note: Due to a fixed delta of 0.5, only half of what is defined here will in fact be produced/consumed. Keep this in mind when defining expectedSatisfaction!
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"))
dynamicTest("04", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 1.0f, 0.0f, 0.5f, "Empty Buffer, Stable Power, One tick to fill")),
dynamicTest("05", () -> test_bufferedConsumptionCalculation(2.0f, 1.0f, 2.0f, 0.0f, 1.0f, "Empty Buffer, Stable Power, One delta to fill")),
dynamicTest("06", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.1f, 0.0f, 0.05f, "Empty Buffer, Stable Power, multiple ticks to fill")),
dynamicTest("07", () -> test_bufferedConsumptionCalculation(1.2f, 0.5f, 1.0f, 0.0f, 1.0f, "Empty Buffer, Power excess, one delta to fill")),
dynamicTest("08", () -> test_bufferedConsumptionCalculation(1.0f, 0.5f, 0.1f, 0.0f, 0.1f, "Empty Buffer, Power excess, multiple ticks to fill")),
dynamicTest("09", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 2.0f, 0.0f, 0.5f, "Empty Buffer, Power shortage, one delta to fill")),
dynamicTest("10", () -> test_bufferedConsumptionCalculation(0.5f, 1.0f, 0.1f, 0.0f, 0.05f, "Empty Buffer, Power shortage, multiple ticks to fill")),
dynamicTest("11", () -> test_bufferedConsumptionCalculation(0.0f, 1.0f, 0.1f, 0.5f, 0.5f, "Unchanged buffer with no power produced")),
dynamicTest("12", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.1f, 1.0f, 1.0f, "Unchanged buffer when already full")),
dynamicTest("13", () -> test_bufferedConsumptionCalculation(0.2f, 1.0f, 0.5f, 0.5f, 0.6f, "Half buffer, power shortage")),
dynamicTest("14", () -> test_bufferedConsumptionCalculation(1.0f, 1.0f, 0.5f, 0.9f, 1.0f, "Buffer does not get exceeded")),
dynamicTest("15", () -> test_bufferedConsumptionCalculation(2.0f, 1.0f, 1.0f, 0.5f, 1.0f, "Half buffer, filled with excess"))
};
}
void test_bufferedConsumptionCalculation(float producedPower, float maxBuffer, float powerPerTick, float initialSatisfaction, float expectedSatisfaction, String parameterDescription){
void test_bufferedConsumptionCalculation(float producedPower, float maxBuffer, float powerConsumedPerTick, 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));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 1.0f;
Tile bufferedConsumerTile = createFakeTile(0, 1, createFakeBufferedConsumer(maxBuffer, maxBuffer > 0.0f ? maxBuffer/powerConsumedPerTick : 1.0f));
bufferedConsumerTile.entity.power.satisfaction = initialSatisfaction;
PowerGraph powerGraph = new PowerGraph();
powerGraph.add(producerTile);
powerGraph.add(bufferedConsumerTile);
assumeTrue(MathUtils.isEqual(producedPower, powerGraph.getPowerProduced()));
//assumeTrue(MathUtils.isEqual(Math.min(maxBuffer, powerPerTick), powerGraph.getPowerNeeded()));
assertEquals(producedPower * FakeThreadHandler.fakeDelta, powerGraph.getPowerProduced(), MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Produced power did not match");
assertEquals(Math.min(maxBuffer, powerConsumedPerTick * FakeThreadHandler.fakeDelta), powerGraph.getPowerNeeded(), MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": ConsumedPower did not match");
// Update and check for the expected power satisfaction of the consumer
powerGraph.update();
@ -108,14 +111,15 @@ public class PowerTests extends PowerTestFixture{
@TestFactory
DynamicTest[] testDirectConsumptionWithBattery(){
return new DynamicTest[]{
dynamicTest("01", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 0.0f, 10.0f, 0.0f, "Empty battery, no consumer")),
dynamicTest("02", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 90.0f, 100.0f, 0.0f, "Battery full after update, no consumer")),
// Note: expectedBatteryCapacity is currently adjusted to a delta of 0.5! (FakeThreadHandler sets it to that)
dynamicTest("01", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 0.0f, 5.0f, 0.0f, "Empty battery, no consumer")),
dynamicTest("02", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 94.999f, 99.999f, 0.0f, "Battery almost full after update, no consumer")),
dynamicTest("03", () -> test_directConsumptionWithBattery(10.0f, 0.0f, 100.0f, 100.0f, 0.0f, "Full battery, no consumer")),
dynamicTest("04", () -> test_directConsumptionWithBattery(0.0f, 0.0f, 0.0f, 0.0f, 0.0f, "No producer, no consumer, empty battery")),
dynamicTest("05", () -> test_directConsumptionWithBattery(0.0f, 0.0f, 100.0f, 100.0f, 0.0f, "No producer, no consumer, full battery")),
dynamicTest("06", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 0.0f, 0.0f, 0.0f, "No producer, empty battery")),
dynamicTest("07", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 100.0f, 90.0f, 1.0f, "No producer, full battery")),
dynamicTest("08", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 5.0f, 0.0f, 0.5f, "No producer, low battery")),
dynamicTest("07", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 100.0f, 95.0f, 1.0f, "No producer, full battery")),
dynamicTest("08", () -> test_directConsumptionWithBattery(0.0f, 10.0f, 2.5f, 0.0f, 0.5f, "No producer, low battery")),
dynamicTest("09", () -> test_directConsumptionWithBattery(5.0f, 10.0f, 5.0f, 0.0f, 1.0f, "Producer + Battery = Consumed")),
};
}
@ -124,6 +128,7 @@ public class PowerTests extends PowerTestFixture{
if(producedPower > 0.0f){
Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(producedPower));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 1.0f;
powerGraph.add(producerTile);
}
Tile directConsumerTile = null;
@ -138,10 +143,35 @@ public class PowerTests extends PowerTestFixture{
powerGraph.add(batteryTile);
powerGraph.update();
assertEquals(expectedBatteryCapacity, batteryTile.entity.power.satisfaction * maxCapacity, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Expected battery capacity did not match");
assertEquals(expectedBatteryCapacity / maxCapacity, batteryTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Expected battery satisfaction did not match");
if(directConsumerTile != null){
assertEquals(expectedSatisfaction, directConsumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR, parameterDescription + ": Satisfaction of direct consumer did not match");
}
}
/** Makes sure a direct consumer stops working after power production is set to zero. */
@Test
void testDirectConsumptionStopsWithNoPower(){
Tile producerTile = createFakeTile(0, 0, createFakeProducerBlock(10.0f));
producerTile.<PowerGenerator.GeneratorEntity>entity().productionEfficiency = 1.0f;
Tile consumerTile = createFakeTile(0, 1, createFakeDirectConsumer(5.0f, 0.6f));
PowerGraph powerGraph = new PowerGraph();
powerGraph.add(producerTile);
powerGraph.add(consumerTile);
powerGraph.update();
assertEquals(1.0f, consumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR);
powerGraph.remove(producerTile);
powerGraph.add(consumerTile);
powerGraph.update();
assertEquals(0.0f, consumerTile.entity.power.satisfaction, MathUtils.FLOAT_ROUNDING_ERROR);
if(consumerTile.block().consumes.has(ConsumePower.class)){
ConsumePower consumePower = consumerTile.block().consumes.get(ConsumePower.class);
assertFalse(consumePower.valid(consumerTile.block(), consumerTile.entity()));
}
}
}
}