1
\$\begingroup\$

I'm writing a basic program that calculates the total base resources needed to craft items in Minecraft. It's intended to be modular for support for modded versions of Minecraft (new items or different recipe's).

To start, I created an Item class that will function as a base resource. A base resource is something I have determined to be harvestable in Minecraft (such as planks or coal) or something that is easily craftable and does not warrant its own recipe (such as an iron ingot, easily obtained by smelting iron ore).

public class Item {

String name;
int count;
boolean isPopulated;
boolean isItem;

public Item() {
    this.name = "";
    this.count = 1;
    this.isPopulated = false;
    this.isItem = true;
}

public Item(String name) {
    this.name = name;
    this.count = 1;
    this.isPopulated = false;
    this.isItem = true;
}

public Item(int count) {
    this.name = "";
    this.count = count;
    this.isPopulated = false;
    this.isItem = true;
}

public Item(int count, String name) {
    this.name = name;
    this.count = count;
    this.isPopulated = false;
    this.isItem = true;
}

public String getName() {
    return this.name;
}

public int getCount() {
    return this.count;
}

public void setName(String name) {
    this.name = name;
}

public void setcount(int count) {
    this.count = count;
}
}

Here's an example of an Item class:

public class IronIngot extends Item{
    public IronIngot(int count) {
        super(count);
        this.name = "Iron Ingot";
    }
}

Next is the tricky part (at least for me): I created a Recipe class that has a list of items (or other recipe's) that are used to craft a craftable item. I was able to use some form of recursion to account for craftable items whose recipe has craftable items in them

public class Recipe extends Item {

    List<Item> components;
    List<Item> materials;

    public Recipe() {
        this.components = new ArrayList<>();
        this.materials = new ArrayList<>();
        this.isItem = false;
    }

    public Recipe(int count) {
        super(count);
        this.components = new ArrayList<>();
        this.materials = new ArrayList<>();
        this.isItem = false;
    }

    public void printRecipe() {

        System.out.println("Recipe for " + this.name + ":");
        this.components.forEach((item) -> {
            System.out.println("\t" + item.name + ": " + item.count);
        });
        System.out.println();
    }

    public void printMaterials() {
        populateMaterials();
        System.out.println("Materials needed for " + this.name + ":");
        this.materials.forEach((item) -> {
            System.out.println("\t" + item.name + ": " + item.count);
        });
        System.out.println();
    }

    public void populateMaterials() {
        this.components.forEach((item) -> {
            if (!item.isPopulated) {
                if (item.isItem) {
                    this.addMaterial(item);
                } else {
                    Recipe tempRecipe = (Recipe) item;
                }
            }
        });
    }

    public void addMaterial(Item item) {
        this.materials.forEach((material) -> {
            if (material.name.equals(item.name)) {
                item.isPopulated = true;
                material.count += item.count;

            }
        });
        if (!item.isPopulated) {
            item.isPopulated = true;
            this.materials.add(item);
        }
    }

    public void addMaterials(List<Item> materials) {
        materials.forEach((material) -> {

            if (!material.isItem) {
                Recipe tempRecipe = (Recipe) material;
                this.addMaterials(tempRecipe.getMaterials());
            } else {
                this.addMaterial((Item) material);
            }
        });
    }

    public List getItems() {
        return this.components;
    }

    public List getMaterials() {
        populateMaterials();
        this.materials.forEach((material) -> {
            material.isPopulated = false;
        });
        return this.materials;
    }

}

Here's an example of a recipe class:

public class CopperCoilBlock extends Recipe{
    public CopperCoilBlock(){
        this.name = "Copper Coil Block";
        this.components.add(new LVWireCoil(8));
        this.components.add(new IronIngot(1));
    }

    public CopperCoilBlock(int count){
        super(count);
        this.name = "Copper Coil Block";
        this.components.add(new LVWireCoil(8));
        this.components.add(new IronIngot(1));
    }
}

My problem is this: how do I account for the resource cost of crafting recipe's that yield multiple items? For example, to make a Power cell(low) I need to have a copper coil block. To make a copper coil block I need 8 LV Wire coils and 1 iron ingot. The recipe for LV wire coils is that 4 copper wires and 1 stick yields 4 LV wire coils. This means that to make a copper coil block I need 8 copper wires and 2 sticks. My initial thought was to solve this using the count integer present in every item but I don't know how to implement it or if this is even the best process. Here is what the LVWireCoil class looks like:

public class LVWireCoil extends Recipe {
    public LVWireCoil(int count) {
        super(4);
        this.name = "LV Wire Coil";
        this.components.add(new CopperWire(4));
        this.components.add(new Sticks(1));

    }
}

Also, I have quite a bit to learn with java so any constructive criticism (code conventions, logic, etc) would be welcome!

\$\endgroup\$
2
\$\begingroup\$

Here are my comments:

Item common super class of itself and Recipe

I understand this design decision stems from the requirement that Recipes are Items of other Recipes. and this is the cause of the confusing isItem property of the Item class. It would be more clear if Item and Recipee would have a common super class, say RecipeComponent and then you can test if a component is an Item using instanceof

Recipe is a tree

if we rethink the design of Recipe we see that it is a tree structure, where the leaves (nodes w/o children) are Items (that are not built from other items) and Recipes are the 'junctions' (nodes with children). So now you can build a Recipe as tree of sub-Recipes and Items. the advantage of this is that with trees you have many established algorithms for traversal and manipulation.

count property

It took me a while to understand the count property of Item. IIUC, the count is the number of items in the Recipe. if that is so, the count is a property (= instance variable) of the recipee, not the item.

so this is how I think your model should look like:

Component class:
holds the properties that are define a thing that is used in recipes. currently, you have one such property - name.

Recipe class:
holds a map where key is component name and value is count of components in recipe.

RecipeCatalog class:
holds a map where kay is recipe name and value is an instance of that recipe.

Traversal of Recipe tree:
iterate over the components map. for each item in the map, if it's a Recipe, get its instance from the catalog and traverse it (in recursion fashion).

how to build recipe instances and the catalog

this can be done in the code like you did. but a more convonient way is to have the details in a text file in a standard format. (for example, json or xml) then you can use many of the available libraries to load the text into memry as instances of Recipe and build the catalog. This is also useful for support of mods (you can have different text files for mods)

multiple constructors

This is a technical tip: Item class has multiple constructors with duplicate assignment of instance variables. yuo can call constructor from another constructor to have the assignment written once:

// constructor with no args assigns default values
public Item() {
    this.name = "";
    this.count = 1;
    this.isPopulated = false;
    this.isItem = true;
}

// constructor with args only needs to overide default values
public Item(String name) {
    this();
    this.name = name;
}
\$\endgroup\$
  • \$\begingroup\$ The only problem I have with Recipe becoming a tree is that there are instances where the child has multiple parents. For example, an Iron ingot is used to craft an iron sword (iron ingot and sticks would be leaves, the iron sword would be the branch) but it could also be used to craft a bucket (in this case, iron ingot has two parents, the sword and the bucket). The recipecomponent class is a great idea and will clean up quite a bit. After thinking through my constructors, I realized that I really only needed 2. one with a name and one with a name and count. Thanks for your input! \$\endgroup\$ – Hobopowers Aug 13 '18 at 20:35
  • \$\begingroup\$ if Iron ingot is used in two recipipes, then two separate instances of the item will be created \$\endgroup\$ – Sharon Ben Asher Aug 14 '18 at 8:23
2
\$\begingroup\$

General recomendations

  1. Prefer immutable classes without add(), set(), etc. It leads to simple programs without errors.
  2. Avoid the inheritance. Honestly I thick there are only few cases when it's appropriate.
  3. Use streams. They help to do lazy traversing or many collection operations.
  4. Also I'm big fan of Kotlin language so I recomend you to try it (but this does not relate to the topic).

Here is my prototype:

public class Temp {

    public static void main(String[] args){
        Item log = new Item("log", Collections.emptyList());
        Item wood = new Item("wood", Arrays.asList(new Component(log, 1f/4f)));
        Item stick = new Item("stick", Arrays.asList(new Component(wood, 1f/4f)));
        Item stone = new Item("stone", Collections.emptyList());
        Item axe = new Item("axe", Arrays.asList(new Component(stick, 2f), new Component(stone, 3f)));

        List<Component> axeComponents = getSimpleComponentsOf(new Component(axe, 1f)).collect(Collectors.toList());

        System.out.println(StringUtils.join(axeComponents.stream().map(Component::toString).collect(Collectors.toList()), ", "));
    }


    public static Stream<Component> getSimpleComponentsOf(Component component){
        if (component.item.components.isEmpty()) return Stream.of(component);
        return component.item.components.stream()
                .map(it -> new Component(it.item, it.number * component.number))
                .flatMap(it -> getSimpleComponentsOf(it));
    }

    static class Item {
        public final String name;
        public final List<Component> components;

        Item(String name, List<Component> components) {
            this.name = name;
            this.components = components;
        }
    }

    static class Component {

        public final Item item;
        public final float number;

        Component(Item item, float number){
            this.item = item;
            this.number = number;
        }

        @Override
        public String toString() {
            return "Component(item = " + item.name + ", number = " + number + ")";
        }
    }
}

Output:

Component(item = log, number = 0.125), Component(item = stone, number = 3.0)
\$\endgroup\$
  • \$\begingroup\$ why isn't the code in kotlin? \$\endgroup\$ – Sharon Ben Asher Aug 14 '18 at 8:21
  • \$\begingroup\$ we are in the business of teaching good practices. perhaps there is a better name for the class than Temp? \$\endgroup\$ – Sharon Ben Asher Aug 14 '18 at 8:26
  • \$\begingroup\$ 1. Kotlin code isn't appropriate because of question tag. But if you're interested in kotlin - let me know, I can rewrite code this evening. 2. I had no time to create new project with dozen files. Thought it's obvious but you're right. You are welcome to edit the answer. \$\endgroup\$ – llama Aug 14 '18 at 9:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.