malt.ee

Cracking Pokémon GO's RNG

how to turn 64 bits into an entire pokémon

Every Pokémon in Pokémon GO comes with a 64 bit encounter ID. A long sequence of digits that may look arbitrary, but can be used to accurately predict a Pokémon’s stats without ever asking the server for it. Add your player ID, and you can even predict whether it will be shiny for you!

Traditionally, most MMO games spread their players across different servers to manage load. Pokémon GO, however, is not a traditional MMO. All players share one map: the real world, which also happens to be pretty big. This introduces a big challenge: how do you make sure millions of players see the same Pokémon, with the same attributes, at the same time?

The answer is to make randomness repeatable. A pesudorandom number generator takes a seed number and produces more random-looking numbers from it. Take the same seed and follow the same steps and you’ll always get the same results.

I’ve spent the last months investigating the mechanics behind wild Pokémon generation. I now feel confident enough to share what I was able to uncover here.

Pokémon come in three parts

  1. Nearby

    PokéStopsIn-game locations representing real-world point of interests in view distance may show which Pokémon are around them, these are called Nearby Pokémon. Data is very limited, just containing their encounter ID and basic information about the species.
  1. Wild

    Wild Pokémon are what appear on the in-game map when the player gets within 40 meters around them. In addition to the encounter ID and species data, these also reveal their exact spawnpoint, giving their exact location and disappear time.

  1. Encounter

    Click on a wild Pokémon and you enter the encounter. This gives all data about that Pokémon: IVsIndividual values; the strength of this Pokémon, level, shininess, and so on.

Spawnpoints are just level 20 S2 cellsS2 cells partition the globe into a grid-like hierarchy of cells. Level 20 cells are about 9x9 meters each where Pokémon are spawning at the same time each hour. Which cells become spawnpoints is determined by signals like player activity or their proximity to PokéStops.*

Everything is deterministic

In 2021, James Prompanya gave some insight into how the Pokémon GO backend works.*

A diagram of Pokémon GO's Google Cloud infrastructure

When walking around, the client polls the server every 10 seconds for all objects that are within its view. This request goes to the Spatial Query Backend, where persistent in-game objects live in, like PokéStops. Results are cached based on level 15 S2 cells.

When asked about how Pokémon catching works, however, he mentions this:

For catching Pokémon, that actually doesn’t go to the spatial query backend.*

So that’s interesting: Pokémon seem to not be stored persistently. We know that encounters give all data about a wild Pokémon. And encounter payloads only consist of the encounter ID and spawnpoint ID, meaning that a complete Pokémon must be identifiable from these two values. It has long been theorized that they actually act as a seed, rather than a key for a lookup table, which the interview kind of confirms. But reversing a random number generator can be very challenging, so no one was ever able to find out how exactly.

Getting there

Analyzing this behavior would take significant statistical expertise, which I don’t posess. But there’s this new magic black box: You give it a set of inputs and outputs, and it will churn on it like no human ever would. So I originally handed this problem to GPT 5.6 Sol, and lo-and-behold: After a day of hard churning, it did end up finding how to derive IVs from an encounter ID.

This is a big mystery solvled after it being only a theory for over 10 years. But there’s more: After many months of further analyzing game mechanics, here’s everthing I was able to uncover.

Predicting encounters

Let’s start with how a Pokémon’s stats are generated. Those that you would otherwise only get from an encounter.

  1. Construct a java random number generator with this seed:

    encounter ID XOR 0x18ADE5CD737B
  2. Consume 2 rolls

  3. The next roll determines if that Pokémon becomes a Ditto, at 2% odds and given Ditto can disguise as the given species

  4. The next 4 rolls determine the IVs and level, and apply a higher floor for weather-boosted spawns

  5. Consume one Gaussian roll, then determine size from the next roll, based on specific cutoffs

  6. Determine the quick and charge move by indexing the available move pools with two more rolls

A sample implementation may look like this:

salt = 0x18ADE5CD737B
rng = java.Random(encounter_id xor salt)

rng.next_int()
rng.next_int()

ditto_roll + rng.next_float()
is_ditto = is_ditto_species and ditto_roll < 0.02

if weather_boosted:
    attack  = rng.next_int(12) + 4
    defense = rng.next_int(12) + 4
    stamina = rng.next_int(12) + 4
    level   = rng.next_int(30) + 6
else:
    attack  = rng.next_int(16)
    defense = rng.next_int(16)
    stamina = rng.next_int(16)
    level   = rng.next_int(30) + 1

rng.next_gaussian()
size_roll = rng.next_double()

if size_roll < 0.004:
    size = "XXS"
elif size_roll < 0.029:
    size = "XS"
elif size_roll < 0.971:
    size = "M"
elif size_roll < 0.996:
    size = "XL"
else:
    size = "XXL"

# overwrite size for pumpkaboo and gourgeist here

fast_move = fast_move_pool[rng.next_int(len(fast_move_pool))]
charge_move = charged_move_pool[rng.next_int(len(charged_move_pool))]

Shininess and backgrounds ✨

Encounter a Pokémon, and it might turn out shinyIt has a different color. These are very rare and unique per player, making them highly saught after.

Given IVs are directly derived from the encounter ID, you might think that shininess is determined in a similar fashion. And you’d be right. Given the encounter ID, the player’s internal account ID, and the species’ shiny odds, shininess is perfectly predictable:

  1. Construct a java random number generator with the seed being constructed like this:

    • Encode the encounter ID as 8 bytes, little-endian
    • Append the player ID, encoded as ASCII
    • SHA-1 hash that byte sequence
    • Take the first 8 bytes of the hash, interpreted as little-endian 64-bit integer
  2. First roll determines shininess, given the shiny odds

  3. Second roll determines whether that Pokémon has a backgroundSpecial collector cosmetic, similar to shinies, given the background odds

  4. Pokémon disguised as Ditto will always be overwritten as non-shiny, and an actual shiny Ditto is rolled separately

payload = encounter_id.to_bytes(8, "little") + player_id.encode("ascii")
digest = hashlib.sha1(payload).digest()
seed = int.from_bytes(digest[:8], "little")
rng = JavaRandom(seed)

# odds differ per species, most have 1/512
shiny = rng.next_float() < 1/512

# odds vary, 1/25 are confirmed for community day events
has_background = rng.next_float() < 1/25

# dittos get special treatment
if is_ditto:
    shiny = rng.next_float() < 1/64

Spawn pools, nests, and predicting the species

Predicting a Pokémon species is where it gets more complicated. Here’s what I was able to find:

Takeaways

  1. For nestsParks, spawning a select species more frequently, lower 25% of species rolls will always be overwritten with the nesting species

  2. Before drawing a species, it’s first determined which pool it’s drawn from

  3. There’s a regionalExclusive to specific parts of the world pool, making up 5% of all draws

  4. If there’s an active event, the event pool takes up 60% of spawns

  5. Pool boundaries may vary, or there might be special pools active. For example, I was able to detect a 2% special pool only consisting of Minior

General pools

General pools are where a normal, non-event species is drawn from. Exact details remain unclear, but here’s the main points I was able to confirm:

  1. They’re based on level 16 S2 cells

  2. There’s 4 different geographical classifications that each influence species weights:

    • Landform: Plains, hills, mountains, or tablelands
    • Landcover: Forests, shrublands, croplands, grasslands, non-vegetated, snow, or settlements
    • Climate: Polar, cool, warm, boreal, tropical, or subtropical
    • Features: River, lake, ocean, city, beach, or desert
  3. Weather conditions at spawn time affects species weights

  4. As of October 2026, there’s no evidence for hemisphere or time of day to be of influence

And again, here’s the general implementation for how to predict a species:

# generate species roll
species_rng = java.Random(encounter_id)
species_roll = species_rng.next_double()

# generate spawn pool roll
pool_salt = 0x44444A87
pool_rng = java.Random(java.Long.hashCode(encounter_id) * 31 + pool_salt)
pool_roll = pool_rng.next_double()

# overwrite bottom 25% of rolls with nesting species
if nest and species_roll < 0.25:
    species = NESTING_SPECIES
    return

# decide what pool to use
if pool_roll >= 0.95:
    pool = REGIONAL_POOL
elif ongoing_event and pool_roll >= 0.35:
    pool = EVENT_POOL
else:
    pool = GENERAL_POOL

# read from a given spawn pool, for example:
if roll < 0.80:
	species = BULBASAUR
elif roll < 0.99:
	species = IVYSAUR
else:
	species = VENUSAUR

Implications, what this gets us, and what we’ve learned

  1. Shinies are now worthless

Shinies are now perfectly predictable, allowing maps and feeds to show exactly which Pokémon will be shiny for you. This takes away a lot of value from them.

A map showing shiny Pokémon

  1. Nearby, wild, and encountered Pokémon are now (almost) the same

The limited data provided through nearby and wild Pokémon can now be used to perfectly derive the full data-breadth an encounter would give you. This makes finding specific Pokémon much easier and effective, given you use modified game clients.

  1. Predicting weather boost changes

For every full hour, the in-game weather might change, re-rolling all stats of Pokémon that are affected by that weather. Now, you can perfectly predict the post-roll stats of these Pokémon.

  1. Perfectly analyzing nests

To know the nesting species, you would previously have to look at the most common spawn within nest boundaries, producing many false-positives. Just looking at an encounter ID can now determine wheter a spawn comes from a nest. You wouldn’t even have to know the exact park boundaries anymore.

  1. Knowing about Ditto before catching it

Ditto are only revealed upon catching. Previously, it was possible to detect some Ditto under specific conditions, but now it’s possible to reliably detect all Ditto, without having to catch them.

  1. Parks don’t spawn low-Pokédex Pokémon

Spawn pools are ordered by the species’ Pokédex number. Since nests always overwrite low rolls with the nesting species, the lower 25% of pool entries always get overwritten by the nesting species.

This can be easily observed in the screenshot below. At the time of taking it, Ponyta spawns occupy rolls below 0.15. It makes them common everywhere, but completely within nest boundaries.

A map showing Ponyta around a nest, but not within it

Investigating this was a lot of fun, and I’ll continue researching more hidden game mechanics. In the mean-time, feel free to check out Demap to check out how I implemented these findings into my mapping project.


that's it.

Normally I would link to other posts here, but this is the only one I've made so far! Thanks for reading