Fix out of order bug (#21)

This commit is contained in:
Joshua Ball authored and GitHub committed 2024-12-09 13:16:47 -06:00
1 parent b069425613
commit e852c84cef
12 files changed
+1108 -228

No files matched your search

+9
View File
@@ -3,3 +3,12 @@
plugins {
id 'com.gtnewhorizons.gtnhconvention'
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
}
test {
useJUnitPlatform()
}
@@ -0,0 +1,455 @@
package me.towdium.jecalculation.data.structure;
import static me.towdium.jecalculation.utils.Utilities.stream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import me.towdium.jecalculation.polyfill.MethodsReturnNonnullByDefault;
import me.towdium.jecalculation.utils.Utilities;
import me.towdium.jecalculation.utils.wrappers.Pair;
// positive => generate; negative => require
@MethodsReturnNonnullByDefault
@ParametersAreNonnullByDefault
public abstract class AbstractCostListService<LabelT, RecipeT, CostListT> {
private final Dependencies<LabelT, RecipeT> d;
private final CostLists<LabelT, CostListT> costLists;
private final Class<CostListT> costListClass;
AbstractCostListService(Dependencies<LabelT, RecipeT> dependencies, CostLists<LabelT, CostListT> costLists,
Class<CostListT> costListClass) {
this.d = dependencies;
this.costLists = costLists;
this.costListClass = costListClass;
}
public CostListT newNegatedCostList(List<LabelT> labels) {
List<LabelT> negativeLabels = labels.stream()
.filter(d::isNotEmptyLabel)
.map(i -> d.multiplyLabel(d.copyLabel(i), -1))
.collect(Collectors.toList());
return costLists.newCostList(negativeLabels);
}
public CostListT newPosNegCostList(List<LabelT> positive, List<LabelT> negative) {
CostListT ret = newNegatedCostList(positive);
multiply(ret, -1);
mergeInplace(ret, newNegatedCostList(negative), false);
return ret;
}
public CostListT strictMergeCostList(CostListT a, CostListT b) {
return mergeCostLists(a, b, false);
}
public List<LabelT> getLabels(CostListT costList) {
return costLists.getLabels(costList);
}
public Calculation<LabelT> calculate(CostListT costList) {
class ProcedureStep {
CostListT stillNeeded; // mostly negative until the last step. may have some positive if there are excess
// outputs
RecipeT recipe;
long multiplier;
CostListT multipliedRecipeOutputs; // recipe.outputs * multiplier; 1st item is the main output
}
ArrayList<ProcedureStep> procedure = new ArrayList<>();
ArrayList<LabelT> catalysts = new ArrayList<>();
return new Calculation<LabelT>() {
private Iterator<RecipeT> iterator = d.recipeIterator();
private int index;
{
HashSet<CostListT> set = new HashSet<>();
set.add(costList);
// reset index & iterator
reset();
Pair<RecipeT, Long> next = find();
int count = 0;
while (next != null) {
ProcedureStep procedureStep = new ProcedureStep();
procedureStep.recipe = next.one;
procedureStep.multiplier = next.two;
CostListT original = getCurrent();
List<LabelT> outL = d.getRecipeOutput(next.one)
.stream()
.filter(d::isNotEmptyLabel)
.collect(Collectors.toList());
CostListT outC = newNegatedCostList(outL);
multiply(outC, -next.two);
procedureStep.multipliedRecipeOutputs = outC;
List<LabelT> inL = d.getRecipeInput(next.one)
.stream()
.filter(d::isNotEmptyLabel)
.collect(Collectors.toList());
CostListT inC = newNegatedCostList(inL);
multiply(inC, next.two);
CostListT result = mergeCostLists(original, outC, false);
mergeInplace(result, inC, false);
procedureStep.stillNeeded = result;
if (!set.contains(result)) {
set.add(result);
procedure.add(procedureStep);
addCatalyst(d.getRecipeCatalyst(next.one));
reset();
}
next = find();
if (count++ > 1000) {
d.addMaxLoopChatMessage();
break;
}
}
}
@Override
public List<LabelT> getCatalysts() {
return catalysts;
}
@Override
public List<LabelT> getInputs() {
return getLabels(getCurrent()).stream()
.filter(i -> d.getLabelAmount(i) < 0)
.map(i -> d.multiplyLabel(d.copyLabel(i), -1))
.collect(Collectors.toList());
}
@Override
public List<LabelT> getOutputs(List<LabelT> ignore) {
return getLabels(getCurrent()).stream()
.map(i -> d.multiplyLabel(d.copyLabel(i), -1))
.map(
i -> ignore.stream()
.flatMap(j -> stream(d.mergeLabels(i, j)))
.findFirst()
.orElse(i))
.filter(i -> d.isNotEmptyLabel(i) && d.getLabelAmount(i) < 0)
.map(i -> d.multiplyLabel(i, -1))
.collect(Collectors.toList());
}
@Override
public List<LabelT> getSteps(List<LabelT> givenInventory) {
// First we try running a simulated inventory through the procedure backwards, greedily preferring steps
// that keep a small inventory, although we prohibit steps that are impossible and merge steps that use
// identical recipes whenever possible. There's no backtracking, so it's posssible to get stuck in a
// corner, in which case we fall back to the simpler solution below. As long as the number of labels in
// the simulated inventory stays relatively bounded, this algorithm is close to quadratic (like the
// fallback). It could in theory bump up to cubic if there were lots of excess outputs of many distinct
// labels, but that doesn't seem to be a problem in practice.
{
List<LabelT> startingInventory = new ArrayList<>(givenInventory);
startingInventory.addAll(getInputs());
CostListT inventory = costLists.newCostList(startingInventory);
List<ProcedureStep> remainingProcedureSteps = new ArrayList<>(procedure);
List<Pair<RecipeT, Long>> optimizedSteps = new ArrayList<>();
while (!remainingProcedureSteps.isEmpty()) {
final RecipeT preferredRecipe = optimizedSteps.isEmpty() ? null
: optimizedSteps.get(optimizedSteps.size() - 1).one;
Integer indexOfBestStep = null;
CostListT inventoryAfterBestStep = null;
int sizeOfInventoryAfterBestStep = Integer.MAX_VALUE;
for (int i = remainingProcedureSteps.size() - 1; i >= 0; i--) {
ProcedureStep step = remainingProcedureSteps.get(i);
CostListT candidateInventory = mergeCostLists(
inventory,
recipeAsCostList(step.recipe, step.multiplier),
false);
if (!isAllPositive(candidateInventory)) {
// Don't give the user an impossible plan.
continue;
}
int inventorySize = estimatedNumSlotsTakenBy(candidateInventory);
if (indexOfBestStep == null) {
// First encounter of an option with a non-negative inventory
indexOfBestStep = i;
inventoryAfterBestStep = candidateInventory;
sizeOfInventoryAfterBestStep = inventorySize;
continue;
}
ProcedureStep bestStep = remainingProcedureSteps.get(indexOfBestStep);
if (step.recipe.equals(preferredRecipe) && !bestStep.recipe.equals(preferredRecipe)) {
// First encounter of an option that uses the same recipe as our most recent step; stop
// here
indexOfBestStep = i;
inventoryAfterBestStep = candidateInventory;
break;
}
if (inventorySize < sizeOfInventoryAfterBestStep) {
// No matching recipe found yet, but this step gives the smallest inventory
indexOfBestStep = i;
inventoryAfterBestStep = candidateInventory;
sizeOfInventoryAfterBestStep = inventorySize;
}
}
if (indexOfBestStep == null) {
// Stuck in a corner; give up
optimizedSteps = null;
break;
}
// Removal is linear time in the worst case, but
// 1. Java's LinkedList would require a re-traversal anyway
// 2. Most of the time, indexOfBestStep will be near the end.
// 3. Array copies are fast.
// 4. We just finished a linear scan, so this doesn't worsen our complexity.
ProcedureStep bestStep = remainingProcedureSteps.remove((int) indexOfBestStep);
inventory = inventoryAfterBestStep;
if (!optimizedSteps.isEmpty() && bestStep.recipe.equals(preferredRecipe)) {
// merge with previous step
Pair<RecipeT, Long> latest = optimizedSteps.get(optimizedSteps.size() - 1);
latest.two = latest.two + bestStep.multiplier;
} else {
// add new step
optimizedSteps.add(new Pair<>(bestStep.recipe, bestStep.multiplier));
}
}
if (optimizedSteps != null) {
List<LabelT> retLabels = new ArrayList<>(optimizedSteps.size());
for (Pair<RecipeT, Long> pair : optimizedSteps) {
List<LabelT> outL = d.getRecipeOutput(pair.one)
.stream()
.filter(d::isNotEmptyLabel)
.collect(Collectors.toList());
CostListT outC = newNegatedCostList(outL);
multiply(outC, -pair.two);
retLabels.add(
costLists.getLabels(outC)
.get(0));
}
return retLabels;
}
}
// If we reach here, then the above approach got trapped in a corner where all paths forward led to
// negative items in the simulated inventory. Here we fall back to a straightforward merge, which
// occasionally gives steps out of order, but 99% of the time gives a right answer.
{
List<LabelT> ret = procedure.stream()
.map(
i -> costLists.getLabels(i.multipliedRecipeOutputs)
.get(0))
.collect(Collectors.toList());
Collections.reverse(ret);
CostListT cl = multiply(newNegatedCostList(ret), -1);
CostListT temp = newNegatedCostList(new ArrayList<>());
mergeInplace(temp, cl, false);
return costLists.getLabels(temp);
}
}
private boolean isAllPositive(CostListT candidateInventory) {
for (LabelT label : costLists.getLabels(candidateInventory)) {
if (d.getLabelAmount(label) < 0) {
return false;
}
}
return true;
}
private void reset() {
index = 0;
iterator = d.recipeIterator();
}
/**
* Find next recipe and its amount
*
* @return pair of the next recipe and its amount
*/
@Nullable
private Pair<RecipeT, Long> find() {
List<LabelT> labels = getLabels(getCurrent());
for (; index < labels.size(); index++) {
LabelT label = labels.get(index);
// Only negative label is required to calculate
if (d.getLabelAmount(label) >= 0) continue;
// Find the recipe for the label.
// Reset or not reset the iterator is a question
while (iterator.hasNext()) {
RecipeT r = iterator.next();
if (d.recipeOutputMatches(r, label)
.isPresent()) {
return new Pair<>(r, d.multiplier(r, label));
}
}
iterator = d.recipeIterator();
}
return null;
}
private void addCatalyst(List<LabelT> labels) {
labels.stream()
.filter(d::isNotEmptyLabel)
.forEach(
i -> catalysts.stream()
.filter(j -> d.labelMatches(j, i))
.findAny()
.map(j -> d.setLabelAmount(j, Math.max(d.getLabelAmount(i), d.getLabelAmount(j))))
.orElseGet(Utilities.fake(() -> catalysts.add(i))));
}
private CostListT getCurrent() {
return procedure.isEmpty() ? costList : procedure.get(procedure.size() - 1).stillNeeded;
}
};
};
interface Dependencies<LabelT, RecipeT> {
// Labels
LabelT copyLabel(LabelT label);
LabelT getEmptyLabel();
long getLabelAmount(LabelT label);
boolean isNotEmptyLabel(LabelT label);
boolean labelMatches(LabelT self, LabelT that);
Optional<LabelT> mergeLabels(LabelT a, LabelT b);
LabelT multiplyLabel(LabelT label, float i);
LabelT setLabelAmount(LabelT label, long amount);
// Recipes
List<LabelT> getRecipeCatalyst(RecipeT recipe);
List<LabelT> getRecipeInput(RecipeT recipe);
List<LabelT> getRecipeOutput(RecipeT recipe);
Optional<LabelT> recipeOutputMatches(RecipeT recipe, LabelT label);
long multiplier(RecipeT recipe, LabelT label);
Iterator<RecipeT> recipeIterator();
// Utilities
void addMaxLoopChatMessage();
}
interface CostLists<LabelT, CostListT> {
CostListT newCostList(List<LabelT> labels);
List<LabelT> getLabels(CostListT self);
void setLabels(CostListT self, List<LabelT> labels);
}
private CostListT mergeCostLists(CostListT a, CostListT b, boolean strict) {
CostListT ret = copyCostList(a);
mergeInplace(ret, b, strict);
return ret;
}
/**
* Merge self to this
*
* @param that cost list to merge
* @param strict if true, only merge same label
*/
private void mergeInplace(CostListT self, CostListT that, boolean strict) {
List<LabelT> thisLabels = getLabels(self);
getLabels(that).forEach(i -> thisLabels.add(d.copyLabel(i)));
for (int i = 0; i < thisLabels.size(); i++) {
for (int j = i + 1; j < thisLabels.size(); j++) {
if (strict) {
LabelT a = thisLabels.get(i);
LabelT b = thisLabels.get(j);
if (d.labelMatches(a, b)) {
thisLabels.set(i, d.setLabelAmount(a, Math.addExact(d.getLabelAmount(a), d.getLabelAmount(b))));
thisLabels.set(j, d.getEmptyLabel());
}
} else {
Optional<LabelT> l = d.mergeLabels(thisLabels.get(i), thisLabels.get(j));
if (l.isPresent()) {
thisLabels.set(i, l.get());
thisLabels.set(j, d.getEmptyLabel());
}
}
}
}
costLists.setLabels(
self,
thisLabels.stream()
.filter(d::isNotEmptyLabel)
.collect(Collectors.toList()));
}
private CostListT multiply(CostListT self, long i) {
costLists.setLabels(
self,
costLists.getLabels(self)
.stream()
.map(j -> d.multiplyLabel(j, i))
.collect(Collectors.toList()));
return self;
}
boolean costListEquals(CostListT self, Object obj) {
if (costListClass.isInstance(obj)) {
CostListT c = (CostListT) obj;
CostListT m = multiply(copyCostList(c), -1);
return getLabels(mergeCostLists(self, m, true)).isEmpty();
} else return false;
}
protected CostListT copyCostList(CostListT from) {
CostListT ret = newNegatedCostList(Collections.emptyList());
costLists.setLabels(
ret,
costLists.getLabels(from)
.stream()
.map(d::copyLabel)
.collect(Collectors.toList()));
return ret;
}
private CostListT recipeAsCostList(RecipeT recipe, long multiplier) {
// todo: unify with body of calculate()
List<LabelT> outL = d.getRecipeOutput(recipe)
.stream()
.filter(d::isNotEmptyLabel)
.collect(Collectors.toList());
CostListT outC = newNegatedCostList(outL);
multiply(outC, -multiplier);
List<LabelT> inL = d.getRecipeInput(recipe)
.stream()
.filter(d::isNotEmptyLabel)
.collect(Collectors.toList());
CostListT inC = newNegatedCostList(inL);
multiply(inC, multiplier);
return mergeCostLists(inC, outC, false);
}
private int estimatedNumSlotsTakenBy(CostListT costLisT) {
int total = 0;
for (LabelT label : costLists.getLabels(costLisT)) {
// Assuming 64 is not valid for snowballs and unstackables, but good enough for an estimate
total += (int) Math.ceil(d.getLabelAmount(label) / 64.0);
}
return total;
}
}
@@ -0,0 +1,19 @@
package me.towdium.jecalculation.data.structure;
import java.util.Collections;
import java.util.List;
public interface Calculation<LabelT> {
List<LabelT> getCatalysts();
List<LabelT> getInputs();
List<LabelT> getOutputs(List<LabelT> ignore);
default List<LabelT> getSteps() {
return getSteps(Collections.emptyList());
}
List<LabelT> getSteps(List<LabelT> startingInventory);
}
@@ -1,22 +1,11 @@
package me.towdium.jecalculation.data.structure;
import static me.towdium.jecalculation.utils.Utilities.stream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.annotation.Nullable;
import javax.annotation.ParametersAreNonnullByDefault;
import me.towdium.jecalculation.data.Controller;
import me.towdium.jecalculation.data.label.ILabel;
import me.towdium.jecalculation.polyfill.MethodsReturnNonnullByDefault;
import me.towdium.jecalculation.utils.Utilities;
import me.towdium.jecalculation.utils.wrappers.Pair;
// positive => generate; negative => require
@MethodsReturnNonnullByDefault
@@ -25,97 +14,23 @@ public class CostList {
List<ILabel> labels;
public CostList() {
labels = new ArrayList<>();
}
public CostList(List<ILabel> labels) {
this.labels = labels.stream()
.filter(i -> i != ILabel.EMPTY)
.map(
i -> i.copy()
.multiply(-1))
.collect(Collectors.toList());
}
public CostList(List<ILabel> positive, List<ILabel> negative) {
this(positive);
multiply(-1);
mergeInplace(new CostList(negative), false);
}
public static CostList merge(CostList a, CostList b, boolean strict) {
CostList ret = a.copy();
ret.mergeInplace(b, strict);
return ret;
}
/**
* Merge that to this
*
* @param that cost list to merge
* @param strict if true, only merge same label
*/
public void mergeInplace(CostList that, boolean strict) {
that.labels.forEach(i -> this.labels.add(i.copy()));
for (int i = 0; i < this.labels.size(); i++) {
for (int j = i + 1; j < this.labels.size(); j++) {
if (strict) {
ILabel a = this.labels.get(i);
ILabel b = this.labels.get(j);
if (a.matches(b)) {
this.labels.set(i, a.setAmount(Math.addExact(a.getAmount(), b.getAmount())));
this.labels.set(j, ILabel.EMPTY);
}
} else {
Optional<ILabel> l = ILabel.MERGER.merge(this.labels.get(i), this.labels.get(j));
if (l.isPresent()) {
this.labels.set(i, l.get());
this.labels.set(j, ILabel.EMPTY);
}
}
}
}
this.labels = this.labels.stream()
.filter(i -> i != ILabel.EMPTY)
.collect(Collectors.toList());
}
public CostList multiply(long i) {
labels = labels.stream()
.map(j -> j.multiply(i))
.collect(Collectors.toList());
return this;
// External code should probably be calling MainCostListService.INSTANCE.newPosCostList()
// instead of calling this directly
CostList(List<ILabel> labels) {
this.labels = labels;
}
@Override
public boolean equals(Object obj) {
if (obj instanceof CostList) {
CostList c = (CostList) obj;
CostList m = c.copy()
.multiply(-1);
return CostList.merge(this, m, true).labels.isEmpty();
} else return false;
}
public CostList copy() {
CostList ret = new CostList();
ret.labels = labels.stream()
.map(ILabel::copy)
.collect(Collectors.toList());
return ret;
}
public boolean isEmpty() {
return labels.isEmpty();
return MainCostListService.INSTANCE.costListEquals(this, obj);
}
public List<ILabel> getLabels() {
return labels;
}
public Calculator calculate() {
return new Calculator();
public Calculation<ILabel> calculate() {
return MainCostListService.INSTANCE.calculate(this);
}
@Override
@@ -124,133 +39,4 @@ public class CostList {
for (ILabel i : labels) hash ^= i.hashCode();
return hash;
}
public class Calculator {
ArrayList<Pair<CostList, CostList>> procedure = new ArrayList<>();
ArrayList<ILabel> catalysts = new ArrayList<>();
Recipes.RecipeIterator iterator = Controller.recipeIterator();
private int index;
public Calculator() throws ArithmeticException {
HashSet<CostList> set = new HashSet<>();
set.add(CostList.this);
// reset index & iterator
reset();
Pair<Recipe, Long> next = find();
int count = 0;
while (next != null) {
CostList original = getCurrent();
List<ILabel> outL = next.one.getOutput()
.stream()
.filter(i -> i != ILabel.EMPTY)
.collect(Collectors.toList());
CostList outC = new CostList(outL);
outC.multiply(-next.two);
List<ILabel> inL = next.one.getInput()
.stream()
.filter(i -> i != ILabel.EMPTY)
.collect(Collectors.toList());
CostList inC = new CostList(inL);
inC.multiply(next.two);
CostList result = CostList.merge(original, outC, false);
result.mergeInplace(inC, false);
if (!set.contains(result)) {
set.add(result);
procedure.add(new Pair<>(result, outC));
addCatalyst(next.one.getCatalyst());
reset();
}
next = find();
if (count++ > 1000) {
Utilities.addChatMessage(Utilities.ChatMessage.MAX_LOOP);
break;
}
}
}
private void reset() {
index = 0;
iterator = Controller.recipeIterator();
}
/**
* Find next recipe and its amount
*
* @return pair of the next recipe and its amount
*/
@Nullable
private Pair<Recipe, Long> find() {
List<ILabel> labels = getCurrent().labels;
for (; index < labels.size(); index++) {
ILabel label = labels.get(index);
// Only negative label is required to calculate
if (label.getAmount() >= 0) continue;
// Find the recipe for the label.
// Reset or not reset the iterator is a question
while (iterator.hasNext()) {
Recipe r = iterator.next();
if (r.matches(label)
.isPresent()) return new Pair<>(r, r.multiplier(label));
}
iterator = Controller.recipeIterator();
}
return null;
}
private void addCatalyst(List<ILabel> labels) {
labels.stream()
.filter(i -> i != ILabel.EMPTY)
.forEach(
i -> catalysts.stream()
.filter(j -> j.matches(i))
.findAny()
.map(j -> j.setAmount(Math.max(i.getAmount(), j.getAmount())))
.orElseGet(Utilities.fake(() -> catalysts.add(i))));
}
private CostList getCurrent() {
return procedure.isEmpty() ? CostList.this : procedure.get(procedure.size() - 1).one;
}
public List<ILabel> getCatalysts() {
return catalysts;
}
public List<ILabel> getInputs() {
return getCurrent().labels.stream()
.filter(i -> i.getAmount() < 0)
.map(
i -> i.copy()
.multiply(-1))
.collect(Collectors.toList());
}
public List<ILabel> getOutputs(List<ILabel> ignore) {
return getCurrent().labels.stream()
.map(
i -> i.copy()
.multiply(-1))
.map(
i -> ignore.stream()
.flatMap(j -> stream(ILabel.MERGER.merge(i, j)))
.findFirst()
.orElse(i))
.filter(i -> i != ILabel.EMPTY && i.getAmount() < 0)
.map(i -> i.multiply(-1))
.collect(Collectors.toList());
}
public List<ILabel> getSteps() {
List<ILabel> ret = procedure.stream()
.map(i -> i.two.labels.get(0))
.collect(Collectors.toList());
Collections.reverse(ret);
CostList cl = new CostList(ret).multiply(-1);
CostList temp = new CostList();
temp.mergeInplace(cl, false);
return temp.labels;
}
}
}
@@ -0,0 +1,117 @@
package me.towdium.jecalculation.data.structure;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import me.towdium.jecalculation.data.Controller;
import me.towdium.jecalculation.data.label.ILabel;
import me.towdium.jecalculation.utils.Utilities;
public class MainCostListService extends AbstractCostListService<ILabel, Recipe, CostList> {
private static final Dependencies<ILabel, Recipe> DEFAULT_DEPENDENCIES = new Dependencies<ILabel, Recipe>() {
@Override
public ILabel copyLabel(ILabel label) {
return label.copy();
}
@Override
public ILabel getEmptyLabel() {
return ILabel.EMPTY;
}
@Override
public long getLabelAmount(ILabel label) {
return label.getAmount();
}
@Override
public boolean isNotEmptyLabel(ILabel label) {
return label != ILabel.EMPTY;
}
@Override
public boolean labelMatches(ILabel self, ILabel that) {
return self.matches(that);
}
@Override
public Optional<ILabel> mergeLabels(ILabel a, ILabel b) {
return ILabel.MERGER.merge(a, b);
}
@Override
public ILabel multiplyLabel(ILabel label, float i) {
return label.multiply(i);
}
@Override
public ILabel setLabelAmount(ILabel label, long amount) {
return label.setAmount(amount);
}
@Override
public List<ILabel> getRecipeCatalyst(Recipe recipe) {
return recipe.getCatalyst();
}
@Override
public List<ILabel> getRecipeInput(Recipe recipe) {
return recipe.getInput();
}
@Override
public List<ILabel> getRecipeOutput(Recipe recipe) {
return recipe.getOutput();
}
@Override
public Optional<ILabel> recipeOutputMatches(Recipe recipe, ILabel label) {
return recipe.matches(label);
}
@Override
public long multiplier(Recipe recipe, ILabel label) {
return recipe.multiplier(label);
}
@Override
public Iterator<Recipe> recipeIterator() {
return Controller.recipeIterator();
}
@Override
public void addMaxLoopChatMessage() {
Utilities.addChatMessage(Utilities.ChatMessage.MAX_LOOP);
}
};
private static final CostLists<ILabel, CostList> DEFAULT_COST_LISTS = new CostLists<ILabel, CostList>() {
@Override
public CostList newCostList(List<ILabel> labels) {
return new CostList(labels);
}
@Override
public List<ILabel> getLabels(CostList costList) {
return costList.getLabels();
}
@Override
public void setLabels(CostList self, List<ILabel> labels) {
self.labels = labels;
}
};
// This MUST be defined after DEFAULT_DEPENDENCIES and DEFAULT_COST_LISTS.
// Otherwise, you will get a NullPointerException!
public static MainCostListService INSTANCE = new MainCostListService();
private MainCostListService() {
super(DEFAULT_DEPENDENCIES, DEFAULT_COST_LISTS, CostList.class);
}
}
@@ -153,6 +153,11 @@ public class Recipe {
.findAny();
}
@Override
public String toString() {
return "Recipe{" + "output=" + (output.size() == 0 ? "{}" : output.get(0)) + '}';
}
public long multiplier(ILabel label) {
return output.stream()
.filter(
@@ -22,8 +22,9 @@ import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import me.towdium.jecalculation.data.Controller;
import me.towdium.jecalculation.data.label.ILabel;
import me.towdium.jecalculation.data.structure.Calculation;
import me.towdium.jecalculation.data.structure.CostList;
import me.towdium.jecalculation.data.structure.CostList.Calculator;
import me.towdium.jecalculation.data.structure.MainCostListService;
import me.towdium.jecalculation.data.structure.RecordCraft;
import me.towdium.jecalculation.data.structure.RecordGroupCraft;
import me.towdium.jecalculation.gui.JecaGui;
@@ -55,7 +56,7 @@ import me.towdium.jecalculation.utils.wrappers.Pair;
@SideOnly(Side.CLIENT)
public class GuiCraft extends Gui {
Calculator calculator = null;
Calculation<ILabel> calculator = null;
RecordCraft record;
RecordGroupCraft groupCraft;
long currentAmount = 1;
@@ -211,7 +212,8 @@ public class GuiCraft extends Gui {
long i = s.isEmpty() ? 1 : Long.parseLong(amount.getText());
amount.setColor(JecaGui.COLOR_TEXT_WHITE);
List<ILabel> dest = groupCraft.getCraftList();
CostList list = record.inventory ? new CostList(getInventory(), dest) : new CostList(dest);
CostList list = record.inventory ? MainCostListService.INSTANCE.newPosNegCostList(getInventory(), dest)
: MainCostListService.INSTANCE.newNegatedCostList(dest);
calculator = list.calculate();
} catch (NumberFormatException | ArithmeticException e) {
amount.setColor(JecaGui.COLOR_TEXT_RED);
@@ -246,7 +248,7 @@ public class GuiCraft extends Gui {
result.setLabels(calculator.getCatalysts());
break;
case STEPS:
result.setLabels(calculator.getSteps());
result.setLabels(calculator.getSteps(getInventory()));
break;
}
}
@@ -22,6 +22,7 @@ import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import me.towdium.jecalculation.data.label.ILabel;
import me.towdium.jecalculation.data.structure.CostList;
import me.towdium.jecalculation.data.structure.MainCostListService;
import me.towdium.jecalculation.data.structure.Recipe;
import me.towdium.jecalculation.gui.JecaGui;
import me.towdium.jecalculation.gui.guis.GuiRecipe;
@@ -83,17 +84,20 @@ public class JecaOverlayHandler implements IOverlayHandler {
dst.computeIfAbsent(type, i -> new ArrayList<>())
.stream()
.filter(p -> {
CostList cl = new CostList(list);
CostList cl = MainCostListService.INSTANCE.newNegatedCostList(list);
if (p.three.equals(cl)) {
ILabel.MERGER.merge(p.one, fin)
.ifPresent(i -> p.one = i);
p.two = CostList.merge(p.two, cl, true);
p.two = MainCostListService.INSTANCE.strictMergeCostList(p.two, cl);
return true;
} else return false;
})
.findAny()
.orElseGet(() -> {
Trio<ILabel, CostList, CostList> ret = new Trio<>(fin, new CostList(list), new CostList(list));
Trio<ILabel, CostList, CostList> ret = new Trio<>(
fin,
MainCostListService.INSTANCE.newNegatedCostList(list),
MainCostListService.INSTANCE.newNegatedCostList(list));
dst.get(type)
.add(ret);
return ret;
@@ -0,0 +1,225 @@
package me.towdium.jecalculation.data.structure;
import static me.towdium.jecalculation.data.structure.TestRcp.rcp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
public abstract class AbstractCostListServiceTest {
private static TestLbl EMPTY = new TestLbl("THE_EMPTY_LABEL", 0);
Calculation<TestLbl> calculation;
private List<TestLbl> inventory = Collections.emptyList();
private final List<TestRcp> recipes = new ArrayList<>();
private boolean maxLoopTriggered = false;
private AbstractCostListService.Dependencies<TestLbl, TestRcp> dependencies = new AbstractCostListService.Dependencies<TestLbl, TestRcp>() {
@Override
public TestLbl copyLabel(TestLbl label) {
return label.clone();
}
@Override
public TestLbl getEmptyLabel() {
return EMPTY;
}
@Override
public long getLabelAmount(TestLbl label) {
return label.amount;
}
@Override
public boolean isNotEmptyLabel(TestLbl label) {
return !"THE_EMPTY_LABEL".equals(label.name);
}
@Override
public boolean labelMatches(TestLbl self, TestLbl that) {
return self.name.equals(that.name);
}
@Override
public Optional<TestLbl> mergeLabels(TestLbl a, TestLbl b) {
// For some reason this is way more complicated in ILabel, involving multiplying by 100
// adding 99, and then dividing by 100. Not sure why. Obviously something to do with
// percents, but weirdly, this is what happens when isPercent() returns false.
// I think my naive implementation is still valid for the tests at least.
if (!a.name.equals(b.name)) {
return Optional.empty();
}
long sum = a.amount + b.amount;
if (sum == 0) {
return Optional.of(EMPTY);
}
return Optional.of(new TestLbl(a.name, sum));
}
@Override
public TestLbl multiplyLabel(TestLbl label, float i) {
float amount = i * label.amount;
if (amount > Long.MAX_VALUE) throw new ArithmeticException("Multiply overflow");
return setLabelAmount(label, (long) amount);
}
@Override
public TestLbl setLabelAmount(TestLbl label, long amount) {
if (amount == 0) return EMPTY; // consistent with actual implementation, but this feels like a bug in
// waiting to me, since it never actually mutates the input!
label.amount = amount;
return label;
}
@Override
public List<TestLbl> getRecipeCatalyst(TestRcp recipe) {
return recipe.catalysts;
}
@Override
public List<TestLbl> getRecipeInput(TestRcp recipe) {
return recipe.inputs;
}
@Override
public List<TestLbl> getRecipeOutput(TestRcp recipe) {
return recipe.outputs;
}
@Override
public Optional<TestLbl> recipeOutputMatches(TestRcp recipe, TestLbl label) {
for (TestLbl output : recipe.outputs) {
if (mergeLabels(label, output).isPresent()) {
return Optional.of(output);
}
}
return Optional.empty();
}
@Override
public long multiplier(TestRcp recipe, TestLbl label) {
for (TestLbl output : recipe.outputs) {
if (mergeLabels(label, output).isPresent()) {
long amountA = Math.multiplyExact(label.amount, 100L);
long amountB = Math.multiplyExact(output.amount, 100L);
return (amountB + Math.abs(amountA) - 1) / amountB;
}
}
return 0L;
}
@Override
public Iterator<TestRcp> recipeIterator() {
return recipes.iterator();
}
@Override
public void addMaxLoopChatMessage() {
maxLoopTriggered = true;
}
};
private AbstractCostListService.CostLists<TestLbl, List<TestLbl>> costLists = new AbstractCostListService.CostLists<TestLbl, List<TestLbl>>() {
@Override
public List<TestLbl> newCostList(List<TestLbl> labels) {
return labels;
}
@Override
public List<TestLbl> getLabels(List<TestLbl> self) {
return self;
}
@Override
public void setLabels(List<TestLbl> self, List<TestLbl> labels) {
self.clear();
self.addAll(labels);
}
};
private CostListService service = new CostListService();
private class CostListService extends AbstractCostListService<TestLbl, TestRcp, List<TestLbl>> {
CostListService() {
super(dependencies, costLists, (Class) List.class);
}
}
void inventory(TestLbl... inventory) {
inventory(Arrays.asList(inventory));
}
void inventory(List<TestLbl> inventory) {
this.inventory = inventory;
}
void recipe(List<TestLbl> outputs, List<TestLbl> catalysts, List<TestLbl> inputs) {
recipes.add(rcp(outputs, catalysts, inputs));
}
void request(TestLbl label) {
calculation = service.calculate(service.newPosNegCostList(inventory, Collections.singletonList(label)));
}
void assertInputs(TestLbl... inputs) {
assertInputs(Arrays.asList(inputs));
}
void assertInputs(List<TestLbl> inputs) {
List<TestLbl> actualInputs = calculation.getInputs();
if (!inputs.equals(actualInputs)) {
throw new AssertionError("expectedInputs = " + inputs + ", actualInputs = " + actualInputs);
}
}
void assertExcessOutputs(TestLbl... outputs) {
assertExcessOutputs(Arrays.asList(outputs));
}
void assertExcessOutputs(List<TestLbl> outputs) {
List<TestLbl> actualOutputs = calculation.getOutputs(inventory);
if (!outputs.equals(actualOutputs)) {
throw new AssertionError("expectedOutputs = " + outputs + ", actualOutputs = " + actualOutputs);
}
}
void assertCatalysts(TestLbl... outputs) {
assertCatalysts(Arrays.asList(outputs));
}
void assertCatalysts(List<TestLbl> catalysts) {
List<TestLbl> actualCatalysts = calculation.getCatalysts();
if (!catalysts.equals(actualCatalysts)) {
throw new AssertionError("expectedCatalysts = " + catalysts + ", actualCatalysts = " + actualCatalysts);
}
}
void assertSteps(TestLbl... steps) {
assertSteps(Arrays.asList(steps));
}
void assertSteps(List<TestLbl> steps) {
List<TestLbl> actualSteps = calculation.getSteps();
if (!steps.equals(actualSteps)) {
throw new AssertionError("expectedSteps = " + steps + ", actualSteps = " + actualSteps);
}
}
void assertMaxLoopTriggered() {
assert maxLoopTriggered;
}
void printCalculation() {
System.out.println("inputs: " + calculation.getInputs());
System.out.println("excess outputs: " + calculation.getOutputs(inventory));
System.out.println("catalysts: " + calculation.getCatalysts());
System.out.println("steps: " + calculation.getSteps());
}
}
@@ -0,0 +1,173 @@
package me.towdium.jecalculation.data.structure;
import static me.towdium.jecalculation.data.structure.TestLbl.lbl;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.Test;
public class CostListServiceTest extends AbstractCostListServiceTest {
@Test
void oneCobblestone() {
request(lbl("cobblestone"));
assertInputs(lbl("cobblestone"));
assertExcessOutputs();
assertCatalysts();
assertSteps();
}
@Test
public void threeStone() {
recipe(lst(lbl("stone")), lst(lbl("furnace")), lst(lbl("cobblestone")));
inventory(lbl("stone"));
request(lbl("stone", 3));
assertInputs(lbl("cobblestone", 2));
assertExcessOutputs();
assertCatalysts(lbl("furnace"));
assertSteps(lbl("stone", 2));
}
@Test
void tntPure() {
recipe(
Arrays.asList(lbl("tnt")),
Arrays.asList(lbl("crafting-table")),
Arrays.asList(lbl("gunpowder", 5), lbl("sand", 4)));
request(lbl("tnt"));
assertInputs(lbl("gunpowder", 5), lbl("sand", 4));
assertExcessOutputs(Collections.emptyList());
assertCatalysts(lbl("crafting-table"));
assertSteps(lbl("tnt"));
}
@Test
void tntPartialInventory() {
recipe(
Arrays.asList(lbl("tnt")),
Arrays.asList(lbl("crafting-table")),
Arrays.asList(lbl("gunpowder", 5), lbl("sand", 4)));
inventory(lbl("sand"));
request(lbl("tnt"));
// The reason the sand is listed before the gunpowder, instead of the order it is in the recipe, is because
// adding
// 3 sand to your inventory when you've already got one won't increase the size of your inventory, while
// adding 5 gunpowder when you have none actually will.
assertInputs(lbl("sand", 3), lbl("gunpowder", 5));
assertExcessOutputs(Collections.emptyList());
assertCatalysts(lbl("crafting-table"));
assertSteps(lbl("tnt"));
}
@Test
void minimalInventory1() {
// This request requires 14 iron blocks and 1000 stone.
// We should make the iron blocks before making the stone, to minimize the amount of inventory space we take up.
recipe(lst(lbl("iron-block")), WORKBENCH, lst(lbl("iron-ingot", 9)));
recipe(lst(lbl("stone")), lst(lbl("furnace")), lst(lbl("cobblestone")));
recipe(lst(lbl("mega-block")), lst(), lst(lbl("stone", 1000), lbl("iron-block", 14)));
request(lbl("mega-block"));
assertInputs(lbl("cobblestone", 1000), lbl("iron-ingot", 126));
assertExcessOutputs();
assertCatalysts(lbl("furnace"), lbl("crafting-table"));
assertSteps(lbl("iron-block", 14), lbl("stone", 1000), lbl("mega-block"));
}
@Test
void minimalInventory2() {
// Identical to minimalInventory1, except that the mega-block recipe requests iron-blocks before stone. The
// Calculator should choose to make stone before iron-blocks regardless
// of which order they are specified in the recipe.
recipe(lst(lbl("iron-block")), WORKBENCH, lst(lbl("iron-ingot", 9)));
recipe(lst(lbl("stone")), lst(lbl("furnace")), lst(lbl("cobblestone")));
recipe(lst(lbl("mega-block")), lst(), lst(lbl("iron-block", 14), lbl("stone", 1000)));
request(lbl("mega-block"));
assertInputs(lbl("iron-ingot", 126), lbl("cobblestone", 1000));
assertExcessOutputs();
assertCatalysts(lbl("crafting-table"), lbl("furnace"));
assertSteps(lbl("iron-block", 14), lbl("stone", 1000), lbl("mega-block"));
}
@Test
void basicLoop() {
recipe(lst(lbl("stone")), lst(lbl("furnace")), lst(lbl("cobblestone")));
recipe(lst(lbl("cobblestone")), lst(lbl("hammer")), lst(lbl("stone")));
request(lbl("cobblestone", 1));
assertExcessOutputs();
assert calculation.getSteps()
.size() <= 2000;
}
@Test
void infiniteLoop() {
recipe(lst(lbl("stone")), lst(lbl("furnace")), lst(lbl("cobblestone")));
recipe(lst(lbl("cobblestone", 100)), lst(lbl("hammer")), lst(lbl("stone", 101)));
request(lbl("cobblestone", 1));
assertCatalysts(lbl("hammer"), lbl("furnace"));
assert calculation.getSteps()
.size() <= 2000;
}
@Test
void surplus() {
recipe(lst(lbl("motor")), WORKBENCH, lst(lbl("iron-rod", 2), lbl("magnetic-iron-rod")));
recipe(lst(lbl("iron-rod", 64), lbl("iron-dust", 128)), lst(lbl("lathe")), lst(lbl("iron-ingot", 64)));
recipe(lst(lbl("magnetic-iron-rod", 64)), lst(lbl("magnetizer")), lst(lbl("iron-rod", 64)));
request(lbl("motor"));
assertInputs(lbl("iron-ingot", 128));
assertExcessOutputs(lbl("iron-rod", 62), lbl("magnetic-iron-rod", 63), lbl("iron-dust", 256));
assertCatalysts(lbl("crafting-table"), lbl("lathe"), lbl("magnetizer"));
assertSteps(lbl("iron-rod", 128), lbl("magnetic-iron-rod", 64), lbl("motor"));
}
// The next two tests demonstrate the problem with trying to merge repeated crafting steps in a naive
// post-processing step. If the steps are merged in one direction, one of the tests fails. If the steps are merged
// in the other direction, the other test fails.
@Test
void wiresAndCables1() {
recipe(lst(lbl("superWireAndCable")), WORKBENCH, lst(lbl("cable"), lbl("wire")));
recipe(lst(lbl("wire", 2)), WORKBENCH, lst(lbl("tin-ingot")));
recipe(lst(lbl("cable")), WORKBENCH, lst(lbl("wire")));
request(lbl("superWireAndCable"));
assertInputs(lbl("tin-ingot", 1));
assertExcessOutputs();
assertCatalysts(WORKBENCH);
assertSteps(lbl("wire", 2), lbl("cable"), lbl("superWireAndCable"));
}
@Test
void wiresAndCables2() {
recipe(lst(lbl("tv")), WORKBENCH, lst(lbl("cable"), lbl("antenna")));
recipe(lst(lbl("cable")), WORKBENCH, lst(lbl("wire")));
recipe(lst(lbl("wire", 2)), WORKBENCH, lst(lbl("tin-ingot")));
recipe(lst(lbl("antenna")), WORKBENCH, lst(lbl("cable")));
request(lbl("tv"));
assertInputs(lbl("tin-ingot", 1));
assertExcessOutputs();
assertCatalysts(WORKBENCH);
// Cables are made from wires, not the other way around, so it is important that wires appear before cables.
assertSteps(lbl("wire", 2), lbl("cable", 2), lbl("antenna"), lbl("tv"));
}
private static List<TestLbl> WORKBENCH = lst(lbl("crafting-table"));
private static <T> List<T> lst(T... args) {
return Arrays.asList(args);
}
}
@@ -0,0 +1,45 @@
package me.towdium.jecalculation.data.structure;
import java.util.Objects;
class TestLbl implements Cloneable {
final String name;
long amount;
TestLbl(String name, long amount) {
this.name = name;
this.amount = amount;
}
@Override
protected TestLbl clone() {
return new TestLbl(this.name, this.amount);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TestLbl)) return false;
TestLbl testLbl = (TestLbl) o;
return amount == testLbl.amount && Objects.equals(name, testLbl.name);
}
@Override
public int hashCode() {
return Objects.hash(name, amount);
}
@Override
public String toString() {
return "{" + amount + " " + name + "}";
}
static TestLbl lbl(String name) {
return lbl(name, 1);
}
static TestLbl lbl(String name, int count) {
return new TestLbl(name, count);
}
}
@@ -0,0 +1,40 @@
package me.towdium.jecalculation.data.structure;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
class TestRcp {
final List<TestLbl> outputs;
final List<TestLbl> catalysts;
final List<TestLbl> inputs;
TestRcp(List<TestLbl> outputs, List<TestLbl> catalysts, List<TestLbl> inputs) {
this.outputs = Collections.unmodifiableList(outputs);
this.catalysts = Collections.unmodifiableList(catalysts);
this.inputs = Collections.unmodifiableList(inputs);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof TestRcp)) return false;
TestRcp testRcp = (TestRcp) o;
return Objects.equals(outputs, testRcp.outputs) && Objects.equals(catalysts, testRcp.catalysts)
&& Objects.equals(inputs, testRcp.inputs);
}
@Override
public int hashCode() {
return Objects.hash(outputs, catalysts, inputs);
}
static TestRcp rcp(List<TestLbl> outputs, List<TestLbl> catalysts, List<TestLbl> inputs) {
return new TestRcp(outputs, catalysts, inputs);
}
static TestRcp rcp(List<TestLbl> outputs, List<TestLbl> inputs) {
return new TestRcp(outputs, Collections.emptyList(), inputs);
}
}