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
anAbility
. - Pokémon are
FROM
aGeneration
. - Pokémon
CAN_KNOW
a variety ofMoves
. I have only loadedCAN_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 everyGeneration
. - Each Pokémon can have one or two
Types
, shown by theHAS_TYPE
relationship. - Pokémon can also be weak or strong against attacks of different
Types
. For eachType
, every Pokémon has anAGAINST
relationship which has a multiplier property to indicate how much damage a Pokémon takes from that moveType
. 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 multiplierWEAK_AGAINST
= 2x multiplierNORMAL_VULNERABILITY_AGAINST
= 1x multiplierRESISTANT_TO
= 0.5x multiplierVERY_RESISTANT_TO
= 0.25x multiplierIMMUNE_TO
= 0x multiplier
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 Abilities
– Grassy 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 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) RETURN *
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 MATCH (p:Pokemon)-[r:IMMUNE_TO|RESISTANT_TO|VERY_RESISTANT_TO]->(t:Type) 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'}) RETURN path
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!]
Take the Class