Minecraft Crafting Tree in Neo4j – Part 2

In this series of posts, I’m going to try to represent the Minecraft crafting tree in Neo4j. In Part 1 we looked briefly at one possible data model and then produced two CSV files ready for import into Neo4j Desktop.

In this post we’re going to do a load of those two CSVs and start exploring the graph that we get as a result.

You can access the CSVs here as part of this Gist.

Note: We’re going to create a database from scratch via those CSVs, then do a couple of data fixes to our database as part of this post.

If you don’t want the step-by-step, you can just directly download the minecraft.cypher file that represents the state of our database by the end of this post.

Data load

Given two CSVs structured as follows:

  • Items.csv – containing a single column of item names
  • Recipes.csv – with three columns
    • OutputItem
    • Qty
    • InputItem

We can do a quick load into Neo4j to see how we’re set.

First, drop the two files into the import folder then do a quick sanity check on both:

LOAD CSV WITH HEADERS FROM 'file:///Items.csv' AS row
RETURN row
LOAD CSV WITH HEADERS FROM 'file:///Recipes.csv' AS row
RETURN row

Our data model will be as we described earlier:

  • Nodes will be labelled with ‘Resource’ for now to cover all bases, but later we’ll maybe refine this out
  • name is the only property of a node so far
  • qty is the only property of a relationship so far
LOAD CSV WITH HEADERS FROM 'file:///Items.csv' AS row
MERGE (r: Resource { name: row.Item })

Added 187 labels, created 187 nodes, set 187 properties, completed after 193 ms.
LOAD CSV WITH HEADERS FROM 'file:///Recipes.csv' AS row
MATCH (rInput: Resource { name: row.InputItem })
MATCH (rOutput: Resource { name: row.OutputItem })
MERGE (rOutput)-[:REQUIRES { qty: toInteger(row.Qty) }]->(rInput)

Set 219 properties, created 219 relationships, completed after 330 ms.
CREATE CONSTRAINT ON (resource: Resource) ASSERT resource.name IS UNIQUE
Added 1 constraint, completed after 79 ms.

Running some queries

What all goes into a Wood Sword?

MATCH path= (r:Resource { name: 'Wood Sword' })-[:REQUIRES*]->(:Resource)
RETURN path

What’s the most convoluted item to craft?

MATCH path = (s:Resource)-[:REQUIRES*]->(e:Resource)
RETURN s, max(length(path))
ORDER BY max(length(path)) DESC, s.name DESC
LIMIT 1

Huh – really?

MATCH path= (r:Resource { name: 'Carrot on a Stick' })-[:REQUIRES*]->(:Resource)
RETURN path

Just graph the whole thing – what requires what?

MATCH path = (:Resource)-[:REQUIRES*]->(:Resource)
RETURN path

One last problem – bricks

Our import isn’t perfect – in fact just by looking around the graph interactively we can see some trouble.

Let’s look at the Brick node:

MATCH path = (:Resource { name: 'Brick' })-[:REQUIRES*]->(:Resource)
RETURN path

Displaying 4 nodes, 3 relationships.

From our Crafting page, we see that Bricks only require Clay Bricks to make:

But we can also make bricks directly from Clay using a furnace:

Surely that can’t be right? From the official documentation you can make Brick (as an item) from Clay in a furnace but you then combine four of those brick materials into a Bricks block.

So our source website isn’t entirely accurate, because its listing for Brick should be Bricks. We can fix up our graph for this instance by using the Clay Brick item when we mean the brick material and Brick when we mean the Brick block. We’ll then rename Brick to match the documentation and force it to be plural.

// First make the Flower Pot be constructed of Clay Brick
MATCH (pot: Resource { name: 'Flower Pot' })-[rel:REQUIRES]->(brick: Resource { name: 'Brick' })
MATCH (clayBrick: Resource { name: 'Clay Brick' })
MERGE (pot)-[:REQUIRES { qty: rel.qty }]->(clayBrick)
DELETE rel
RETURN pot, brick, clayBrick
// Now stop Bricks blocks being made of Clay and Fuel, and point those relationships over to the Clay Brick instead
MATCH (brick: Resource { name: 'Brick' })-[rel:REQUIRES]->(clay: Resource { name: 'Clay' })
MATCH (clayBrick: Resource { name: 'Clay Brick' })
MERGE (clayBrick)-[:REQUIRES { qty: rel.qty }]->(clay)
DELETE rel
RETURN brick, clay, clayBrick
MATCH (brick: Resource { name: 'Brick' })-[rel:REQUIRES]->(fuel: Resource { name: 'Fuel' })
MATCH (clayBrick: Resource { name: 'Clay Brick' })
MERGE (clayBrick)-[:REQUIRES { qty: rel.qty }]->(fuel)
DELETE rel
RETURN brick, fuel, clayBrick

Download the sample database so far

I have exported the state of the database so far to this Gist – it’s pure Cypher, so just run it on a clean database in Neo4j Desktop (or whatever version you’re using).

Download minecraft.cypher.

Next steps

We’ve got something that’s sort-of useful now, but we haven’t yet managed to answer the question of ‘how much Wood does it take to make a Wood Sword‘, or anything similar. We’re going to find that tricky because we’ve forgotten to import some key information from the original data source, and to tidily do that our current representation is going to cause us trouble.

Next time we’ll look at the representation problem, we’ll refactor our graph to make Recipe a first-class citizen and we’ll see what impact that has on our ability to query.

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.