Pokégraph: Gotta Graph ‘Em All!

Graphs are everywhere, including Pokémon.

Gotta graph ‘em all!

By now, anyone who’s read a few of my blog posts will know that I can get really geeky about the things I’m into. Anything worth doing is worth geeking out over! One of the main outlets for my geeky inclinations is gaming – both video games and tabletop games.

These days I’m rarely more than a few feet away from my Nintendo Switch and I play board games, card games and role playing games with friends at least once or twice a week. I’ve even organised lunch-time Mario Kart 8 tournaments between the Neo4j European offices!

One of my favourite crossovers between the worlds of video games and tabletop gaming has always been Pokémon. I originally got into Pokémon as a way of bonding with my younger brother, who was just a little boy when Pokémon was first created. We would spend hours together playing the Pokémon card game, battling Pokémon on our Game Boys, and visiting the Pokémon Center in New York City where I lived at the time.

A couple of decades have since passed – my brother is now all grown up, and my beard is mostly gray. I still love Pokémon, though! This Christmas I got both Pokémon Sword and Shield for the Nintendo Switch, which quickly turned into an all-consuming obsession.

You know what happened next, right? I started seeing graphs in my Pokémon game!

This is your brain on Pokémon.

I really wanted to see what a Pokégraph could look like, so I grabbed some data I found on the internet, loaded it into Neo4j, and started to see what I might do with it. The graph Pokémodel I created looks like this:

CALL db.schema()

You can see that there are several node types, or labels, in my graph:

Generation: A node representing each Generation of Pokémon. More than 800 Pokémon have been introduced over eight Generations. These 8 Generations group the Pokémon from each of the regions in the main video game series – Kanto, Johto, Hoenn, Sinnoh, Unova, Alola and Galar.

Ability: All of the abilities that Pokémon can have.

Type: These are the different types, which exist in the Pokémon universe such as Water, Fire, Grass, etc. Types can be used to classify individual Pokémon as well as their Moves. Pokémon can also be weak or resistant to Moves of certain Types.

Move: All of the moves, which Pokémon can know or learn are included in our graph. Moves are usually attacks, but there are also status Moves (improving attack or defense stats, for example), healing Moves, etc. You can see that Move nodes can have a HAS_TYPE relationship to a Type node.

Pokémon: Our graph includes all of the Pokémon entries from the National Pokédex, for all 8 generations. Pokémon nodes contain a number of base stats like Attack, Defence, etc. for each Pokémon. The stat that we’re most interested in is base_total which sums up all of a Pokémon’s stats and can be used as a general indicator of how powerful a particular Pokémon is.

Pokémon can be related to other node types in a variety of ways:

    • Pokémon can have an EVOLVES_FROM relationship to other Pokémon, so that we can view the chain of evolutions which some Pokémon can follow.
    • Pokémon CAN_HAVE an Ability.
    • Pokémon are FROM a Generation.
    • Pokémon CAN_KNOW a variety of Moves. I have only loaded CAN_KNOW Move relationships for Pokémon, which are in Pokémon Sword and Shield (Generation 8) as that’s what I’m currently playing, although the graph contains Pokémon from every Generation.
    • Each Pokémon can have one or two Types, shown by the HAS_TYPE relationship.
    • Pokémon can also be weak or strong against attacks of different Types. For each Type, every Pokémon has an AGAINST relationship which has a multiplier property to indicate how much damage a Pokémon takes from that move Type. A multiplier can be 4x, 2x, 1x, 0.5x, 0.25x, or 0x. Lower multiplier values mean less damage is taken, and of course higher multiplier values mean more damage is taken.
    • I have also created specialised relationships between Pokémon and Types, one for each possible multiplier value. These relationships are:
        • VERY_WEAK_AGAINST = 4x multiplier
        • WEAK_AGAINST = 2x multiplier
        • NORMAL_VULNERABILITY_AGAINST = 1x multiplier
        • RESISTANT_TO = 0.5x multiplier
        • VERY_RESISTANT_TO = 0.25x multiplier
        • IMMUNE_TO = 0x multiplier
TeamMembers: These are specific ‘instances’ of Pokémon. This is the real lineup I’m using in my Pokémon Shield game right now! Instead of linking to the complete set of Abilities a Pokémon CAN_HAVE, and all the Moves it CAN_KNOW, a TeamMember is related to the specific Ability and Moves it actually has in my game.

Let’s bring this to life and look at the graph surrounding a particular Pokémon – Grookey, the adorable Chimp Pokémon that I chose to start my game in Pokémon Shield.

// View Grookey’s Generation, Pokemon Type, and Abilities
MATCH path = (p:Pokemon {name: 'Grookey'})-[:FROM|HAS_TYPE|EVOLVES_FROM|CAN_HAVE]-() RETURN path

We can see that Grookey is a Generation 8 Pokémon, from the region of Galar introduced in Pokémon Sword and Shield. It is a Grass Type Pokémon, and it can have one of two different AbilitiesGrassy Surge or Overgrow. Grookey can evolve into another Pokémon, Thwackey.

// View Grookey’s Type strengths and weaknesses
MATCH path = (p:Pokemon {name: 'Grookey'})-[]-(:Type) RETURN path

We can see Grookey HAS_NORMAL_VULNERABILITY (1x multiplier) to most Types of . This Pokémon is WEAK_AGAINST (2x multiplier) Moves with the Bug, Ice, Poison, Fire, and Flying Type. Grookey is RESISTANT_TO (0.5x multiplier) Moves with the Ground, Water, Electric and Grass Type.

// View Grookey’s available Moves
MATCH path = (p:Pokemon {name: 'Grookey'})-[:CAN_KNOW]->(:Move) RETURN path

We can also see that Grookey CAN_KNOW or learn 52 different Moves! Not bad for a starter Pokémon.

We can take a look at the graph around the version of this Pokémon I have on my team – who I named Monchhichi.

// View Monchhichi’s evolution path, Types, Moves and AbilityMATCH path = (:Type)<-[:HAS_TYPE]-(p:Pokemon)<-[:IS_POKEMON]-(tm:TeamMember {name: ‘Monchhichi’)-[:HAS]->(:Ability),
path2 = (tm)-[:KNOWS]->(:Move)-[:HAS_TYPE]->(:Type)
OPTIONAL MATCH path3 = (p)-[:EVOLVES_FROM*]->(:Pokemon)-[:HAS_TYPE]-(:Type)

Monchhichi is a Rillaboom that I evolved all the way from when I first received him as a Grookey, and nurturing him through his time as a Thwackey. He’s a Grass Type Pokémon, just like the Grookey he grew up from, which means he has all the same weaknesses and resistances. My Monchhichi knows four Moves – three Grass Type Moves (Drum Beating, Grass Pledge and Frenzy Plant) and one Normal Type Move (False Swipe ). He has the Overgrow Ability .

The graph for my whole team is a bit more complex!

// View my whole teams’s evolution path, Types, Moves and Ability

MATCH path = (:Type)<-[:HAS_TYPE]-(p:Pokemon)<-[:IS_POKEMON]-(tm:TeamMember)-[:HAS]->(:Ability), path2 = ™-[:KNOWS]->(:Move)-[:HAS_TYPE]->(:Type) OPTIONAL MATCH path3 = (p)-[:EVOLVES_FROM*]->(:Pokemon)-[:HAS_TYPE]-(:Type) RETURN *

Viewing the data this way lets me see some things about my team very easily. For example, it’s easy to see that very few Moves (blue nodes) are known by more than one Pokemon (yellow nodes) on my team – Goldappletun shares one Move (Dragon Pulse) with Eternatus, and that’s all. We can also see that some Types (purple nodes) of Moves are better represented – my team knows quite a few Grass, Flying, Fire and Psychic Moves but Normal, Fairy, Steel, Poison and Ground Type Moves are not well represented. I might need to diversify the Moves my team knows!

Now that we understand how the data in our graph looks we can start to do some more interesting things with it. For example, can I tell what types of Moves my team are weak or strong against? Easily!

// View my whole team’s Type weaknesses
MATCH (:TeamMember)-[:IS_POKEMON]->(p:Pokemon)<-[a:AGAINST]-(t:Type)
RETURN t.name as Type, sum(a.multiplier) as Score order by Score desc

My team is obviously very strong against Grass Type Moves. The total sum of multipliers AGAINST different Grass Type Moves for the whole team is 2 – an average multiplier of 0.33x per TeamMember! That’s the good news.

The bad news is that my team is really weak against Ice (multiplier sum of 10.5, 1.75x average) and Rock (multiplier sum of 10, 1.66x average) Type Moves. Maybe I want to add some Pokémon to my team who are resistant to these Types of Moves!

// Find how many Pokémon are IMMUNE_TO, RESISTANT_TO or 
// VERY_RESISTANT_TO Ice and Rock Type Moves
WHERE t.name in ['Ice', 'Rock']
RETURN t.name as Type, type(r) as Rating, count(p) as Pokemon

There are no Pokémon who are immune to Moves of the Ice or Rock Types. There are, however, 10 Pokémon who are VERY_RESISTANT_TO (0.25x multiplier) Ice Moves and 4 who are VERY_RESISTANT to Rock Moves.

Unfortunately, there are no Pokémon who are VERY_RESISTANT to both Ice and Rock Type Moves. The following query returns no results:

// Find Pokémon who are VERY_RESISTANT_TO both Ice and Rock Type Moves
MATCH path = (:Type {name: 'Ice'})<-[:VERY_RESISTANT_TO]-(:Pokemon)-[:VERY_RESISTANT_TO]->(:Type {name: 'Rock'})

There are, however, plenty of Pokémon who are RESISTANT_TO (0.5x multiplier) both Ice and Rock Type Moves. Maybe I should add one of the powerful ones to my team!

// Find the Pokemon with the highest base_stat who are RESISTANT_TO both Ice and Rock Type Moves
MATCH (:Type {name: 'Ice'})<-[:RESISTANT_TO]-(p:Pokemon)-[:RESISTANT_TO]->(:Type {name: 'Rock'})
RETURN p.name AS Pokemon, p.base_total AS Power ORDER BY Power DESC LIMIT 10

We can see Metagross is the Pokemon with the highest base_total – 700 – among all the Pokémon who are resistant to both Ice and Rock Type Moves. Close behind is Solgaleo with 680. These results are from the entire Pokedex, though, and these Pokemon may not be available in Sword and Shield.

Since we only loaded CAN_KNOW Move relationships for the Pokémon, which are in Sword and Shield, I can adjust my query to find the strongest Ice- and Rock-resistant Pokémon that CAN_KNOW at least one Move.

// Find the Pokemon in Sword and Shield with the highest base_stat who 
// are RESISTANT_TO both Ice and Rock Type Moves
MATCH (:Type {name: 'Ice'})<-[:RESISTANT_TO]-(p:Pokemon)-[:RESISTANT_TO]->(:Type {name: 'Rock'})
WHERE (p)-[:CAN_KNOW]->(:Move)
RETURN p.name AS Pokemon, p.base_total AS Power ORDER BY Power DESC LIMIT 1

The result of this query is ‘Klinklang’ – a Gear Pokémon who has a base_total of 520. Not as strong, but that will have to do until more Pokémon are available with the Sword and Shield Expansion Pass!

I might also want to design a team to battle a specific Gym Leader in Pokémon Shield. For example, Piers is the Dark Type Gym Leader. I need a query to find the strongest (highest base_total) Pokémon in Sword and Shield that has the Dark Type (and only the Dark Type), as Piers may play that Pokémon during our battle.

The query then finds the strongest Pokémon in Sword and Shield who CAN_KNOW Moves of Types, which this Dark Pokémon is VERY_WEAK_AGAINST or WEAK_AGAINST.

// Find the strongest Dark type (and only Dark) Pokemon in the // graph which is in Sword and Shield, and then find the strongest // Pokemon that can know a Move with a Type that the Dark Pokemon is // VERY_WEAK_AGAINST or WEAK_AGAINST MATCH (x:Pokemon)-[:HAS_TYPE]->(a:Type {name: 'Dark'}) WHERE (x)-[:CAN_KNOW]->(:Move) AND NOT (a)<-[:HAS_TYPE]-(x)-[:HAS_TYPE]->(:Type) WITH x ORDER by x.base_total DESC LIMIT 1 MATCH (x)-[:VERY_WEAK_AGAINST|WEAK_AGAINST]->(t:Type)<-[:HAS_TYPE]-(:Move)<-[:CAN_KNOW]-(p:Pokemon) RETURN DISTINCT(p.name) as Pokemon, x.name as Against, p.base_total as Power order by Power DESC limit 10

It looks like the strongest Dark Type Pokémon in Sword and Shield is Umbreon, who is WEAK_AGAINST Fairy, Bug and Fighting Type Moves.

// View Umbreon’s Type strengths and weaknesses
MATCH path = (p:Pokemon {name: 'Umbreon'})-[]-(:Type) RETURN path

I can see from my query results above that the strongest Pokémon who knows Moves of these Types is Tyranitar, followed by Zamazenta and Zacian. All of these are Legendary or psuedo-Legendary Pokémon, though. The strongest regular Pokémon in the query results is Charizard. I have a Charizard on my team, so Piers better watch out!

There’s so much more I could do with my Pokégraph. Using one of the Neo4j Community Detection Graph Algorithms I could group Pokémon together based on their Type weaknesses and strengths, or based on their available Moves. I could use one of the Similarity Graph Algorithms to find which Pokémon have the most Moves and Abilities in common. I could even use the Pokégraph to design the strongest team to battle one of the other Galar trainers – whether they specialise in Psychic, Ghost, Dragon or any other Type of Pokémon! Writing all those graph queries would eat into my Pokémon gaming time, though, and I’ve still got hundreds of Pokémon to add to my Pokédex! So, we’ll have to leave that for another time.

If you’re a fellow Pokémon-obsessed graphista then maybe we’ll see each other in the Wild Area, become rivals, and maybe even cook curry together. Until then, happy graphing and may your Pokéball throws always be successful!

[There’s a lot of data about Pokémon available on the internet. I’m very grateful to the following sources for the data I used in this post:

https://bulbapedia.bulbagarden.net/wiki/Pok%C3%A9mon https://github.com/veekun/pokedex https://github.com/yaylinda/serebii-parser https://www.kaggle.com/abcsds/pokemon/data https://www.kaggle.com/rounakbanik/pokemon

This is yet another example of a Knowledge Graph – this time, we’re graphing Pokémon domain knowledge. There’s so much more data we could add to this – working with Egg Groups for hatching Pokémon could be very interesting, for example. Or, we could include the areas and towns within each Region where a Pokémon could be found. Do you have any ideas for how to use a Pokémon knowledge graph? Let me know on Twitter or the Neo4j Community site!]

Want to take your Neo4j skills up a notch? Take our online training class, Neo4j in Production, and learn how to scale the world’s leading graph database to unprecedented levels.

Take the Class