Pokemon Nerd Dan

in which I blather on about Pokemon Go, including occasional bouts of code

In the course of trying to (nearly) automate downloading matchup information from PvPoke I had to decipher what, exactly, the path parameters in the URL meant.

To step back: let's say you go to the Battle page. You choose Multi Battle, and then Jungle Cup. You choose Vigoroth as the attacking Pokemon, and — because you're finicky — even enter the specific IVs and level of your Vigoroth, the one you'll be using in your tournament. You click “battle”, et voila: you have the results.

But! Look at the URL. It's changed! For me, it's now


That's a lot of numbers! It's nice, though, because I knew I could change the URL itself to change the battle. In the past I've done that to avoid going through menus, but now my use case is that I want my code to launch the specific URL I want. You may have a similar use case! So let's decypher the parameters.

Some are easy enough to figure out:

https://pvpoke.com/battle/multi/(CP limit)/(cup name)/(Pokemon)-(level)-(attack IV)-(defense IV)-(stamina IV)-4-4-1/11/0-1-3/2-1/

For the others I could guess or get them through trial and error, but PvPoke is open source! So let's look at the source. In particular, the .htaccess, which has:

RewriteRule battle/multi/(\d+)/([a-zA-Z0-9-]+)/([a-zA-Z_\d\.-]+)/([\d-]+)/([\da-zA-Z_-]+)/([\d-]+).*$ battle.php?mode=multi&cp=$1&cup=$2&p1=$3&s=$4&m1=$5&cms=$6 [L,QSA]

So now I know that elsewhere in the code:

  • 4-4-1 after my Pokemon is part of p1
  • 11 is called s
  • 0-1-3 is called m1 (I had suspected it was the moveset, so this tracks)
  • 2-1 is something called cms

I'll spare you the minutiae of tracking everything else down (odd to stop now, I know), but it's all in Interface.js.

The format

https://pvpoke.com/battle/multi/(CP limit)/(cup name)/<P1>/<S>/<M1>/<CMS>/



atkmod and defmod are in-battle modifiers, like from Power Up Punch or Ominous Wind. 4 means no modifier.

baitshields is whether to bait shields, ie with Thunder Punch instead of Wild Charge. 1 for yes, 0 for no.



Shields! This isn't actually one number but two digits: the first is how many shields you use, the second is your opponent. So 01 means you don't use any shields and your opponent uses 1.

Anything above 2 is treated as 0. I was hoping I could sim something where I had 9 shields, but alas. Empoleon_Dynamite is too smart for me.



Three numbers: fast move, charge move 1, charge move 2.

Of the available moves for your Pokemon, this is their index in the html form. This comes from the PvPoke gamemaster.json file, which I'm a big fan of because it's taking the gamemaster file scraped from the game itself and parsing it into something more usable.

Of note: the available moves are alphabetized, and the fast moves start at index 0 while the charge starts at 1. This is because a charge move of 0 means no move.

And so, since Viggy can learn "BODY_SLAM", "BRICK_BREAK", "BULLDOZE", since mine has Body Slam and Bulldoze the charge moves are 1 and 3.



Two numbers: the first is how many fast moves your opponents know. The second is whether they bait shields.

Here I thought I had found a bug, because you can sim against an entire cup and set your opponents' known charge moves to 0! Spoiler alert: you do really well when your opponent never uses a charge move.

Feeling like I had really hackerman'd my way into the mainframe, I opened an issue on the PvPoke Github. It turns out that simming against 0 charge moves is intended: this way you can see if your opponent can farm you for energy.

In conclusion

This is just for one specific battle type (multi battle vs a cup). The .htaccess file I linked to above has others, but since I don't care about them at the moment you'll have to write your own blog post.

Also: PvPoke has a Patreon. Considering this website is basically the de facto authority when it comes to Pokemon Go PVP and is referenced in most if not all discussions, the fact that as of this writing only 52 people support the website financially is an absolute crime.

The finale of the trilogy.

Because hey, if you can be CP 10 at level 1.5... can you be CP 10 at level 2? 3? A million?

Here you go:

Level Number of Pokemon List of Pokemon
1 102 Azurill, Baltoy, Barboach, Beldum, Bidoof, Bronzor, Budew, Burmy (Plant), Burmy (Sandy), Burmy (Trash), Carvanha, Cascoon, Caterpie, Charmander, Cherubi, Chikorita, Chimchar, Cleffa, Combee, Croagunk, Cyndaquil, Diglett, Diglett (Alolan), Ditto, Duskull, Ekans, Electrike, Feebas, Finneon, Glameow, Gulpin, Happiny, Hoothoot, Hoppip, Igglybuff, Jigglypuff, Kakuna, Kirlia, Kricketot, Ledyba, Lotad, Luvdisc, Magikarp, Makuhita, Mareep, Marill, Meditite, Meowth, Meowth (Alolan), Metapod, Nidoran♀, Nidoran♂, Nincada, Nosepass, Pichu, Pidgey, Pikachu, Poliwag, Poochyena, Ralts, Rattata, Rattata (Alolan), Remoraid, Riolu, Seedot, Seel, Sentret, Shedinja, Shieldon, Shinx, Shroomish, Shuckle, Silcoon, Skiploom, Skitty, Slakoth, Slugma, Smeargle, Snorunt, Spearow, Spheal, Spinarak, Squirtle, Starly, Sunkern, Surskit, Swablu, Swinub, Taillow, Togepi, Tyrogue, Vulpix, Vulpix (Alolan), Weedle, Whismur, Wingull, Wobbuffet, Wooper, Wurmple, Wynaut, Zigzagoon, Zubat
1.5 23 Azurill, Burmy (Plant), Burmy (Sandy), Burmy (Trash), Caterpie, Combee, Feebas, Happiny, Igglybuff, Kakuna, Kricketot, Magikarp, Marill, Metapod, Pichu, Shedinja, Shuckle, Smeargle, Sunkern, Tyrogue, Weedle, Wynaut, Zigzagoon
2 6 Azurill, Feebas, Happiny, Magikarp, Shedinja, Shuckle
2.5 3 Feebas, Magikarp, Shedinja
3 1 Shedinja
3.5 1 Shedinja
4 1 Shedinja
4.5 1 Shedinja

(this is because Shedinja's base stats are 153/73/1. ONE. Chansey, meanwhile, has a base stamina of 487.)

After I published the heartwarming story of the Little Smeargle Who Couldn't, I realized I could easily figure out the complete list of Pokemon who could be CP 10 at level 1.5:

	fp, _ := os.Open("gamemaster.json")
	defer fp.Close()
	gm, _ := pokemongo.NewGamemaster(fp)

	ivs := pokemongo.Stats{
		Attack:  0,
		Defense: 0,
		HP:      0,
	level := 1.5
	for _, p := range gm.Pokemon {
		p.Level = level
		p.IVs = ivs
		if p.CP == 10 {

(error checking omitted for brevity)

The list:

  • Azurill
  • Burmy (Plant)
  • Burmy (Sandy)
  • Burmy (Trash)
  • Caterpie
  • Combee
  • Feebas
  • Happiny
  • Igglybuff
  • Kakuna
  • Kricketot
  • Magikarp
  • Marill
  • Metapod
  • Pichu
  • Shedinja
  • Shuckle
  • Smeargle
  • Sunkern
  • Tyrogue
  • Weedle
  • Wynaut
  • Zigzagoon

My son caught a Smeargle yesterday, and powered it up today. Ah, to be a child again, who

  • has the patience to use the snapshot feature, and
  • doesn't mind spending dust on something that's not for PVP

But I digress.

His Smeargle was CP 10, level 1. He powered it up and... it was still CP 10.

Wha happen?

The CP minimum! I knew that any value under 10 would be rounded up to 10. I just hadn't considered that you could be CP 10 at a level above 1!

His Smeargle is 5/0/2: clearly a god among men. Here's a table of CP from level 1 to 2.5:

level atk def hp atk * def^0.5 * sta ^ 0.51 divided by 10 CP
1 4.23 7.802 13 42.600531398094084 4.260 10
1.5 6.0811844310000005 11.216406839400001 20 91.08147384100222 9.108 10
2 7.48790415 13.81102321 24 136.32604170336802 13.326 13
2.5 8.669291355 15.990026277 28 183.43711153757977 18.344 18

So... yeah. Two levels at the minimum. You go, Smeargle.

1 The numbers are actually slightly off, as the HP had been truncated by the time I did the calculation. In my previous posts I would add debug statements to my code but this time I couldn't be bothered.

Using wobbotfet, one of the mods of my Discord noticed an oddity: the bot and web version give a different ranking for a Swampert.

Using 1/2/10, wobbotfet says it's rank 257. The website said 258.

The answer to that turned out to be that 1/2/10 and 1/2/11 actually have the exact same stat product, and the two sites round differently. Something I should look into, maybe, but not a huge deal.

Then, in chat, he checked 0/14/12 and 0/14/13. Same thing: they have the exact same stat product. Except:

0 14 12

CP: 1492
Level: 19
Attack: 121.11401120000001
Defense: 110.05071210000001
HP: 138
Product: 1.839358278542938e+06

0 14 13

CP: 1495
Level: 19
Attack: 121.11401120000001
Defense: 110.05071210000001
HP: 138
Product: 1.839358278542938e+06


They have the exact same stat product, but different CP. How?

The answer, my friends, is blo- er, rounding.

When calculating CP, the HP/Stamina isn't rounded: for a Swampert with 12 stamina, its stamina stat is 138.00009930000002. With 13 stamina it's 138.58237820000002.

They both round down to 138 when calculating the product: this is because that's how HP works in the game. You don't have 138.5 hit points. But when calculating the CP, the 0.58 does make a difference:

FLOOR(Stamina^0.5 * Attack * Def^0.5 / 10)

For our 12 stamina this becomes

(138.00009930000002^0.5 * 121.11401120000001 * 110.05071210000001^0.5) / 10
= (11.74734435095864 * 121.11401120000001 * 10.49050580763387) / 10
= 14925.55591752906766 / 10
= 1492

With one more stamina point it's

(138.58237820000002^0.5 * 121.11401120000001 * 110.05071210000001^0.5) / 10
= (11.77210169001270 * 121.11401120000001 * 10.49050580763387) / 10
= 14957.01128628144856 / 10
= 1495

And, just like that “where did the extra dollar go” riddle, I've turned one point into three.

Now that we've talked about ideal IV spreads and IV floors from the various means of Pokemon acquisition we can put them together and see how things conspire to become even more complicated.

All of the stats I'm using here came from the data/ directory of the api that powers wobbotfet, the Discord bot I wrote to rank your spreads. You can run best_spread.py in the root of that repo to see the results.

Today we're going to focus on Forretress, because the data works out a little bit better than Charizard.

In which Best Friends are not the best

The optimal Forretress for Great League is 0/9/15.

Having read my previous two posts, something about that should jump out at you.

The only way to have an IV of 0 is to catch it in the wild without weather boost.1

Many times this doesn't matter: you could trade with a Good Friend, for example, and have spread rank #2 (1/15/15). But it bears keeping in mind that the higher your friendship, the more IV spreads that are impossible to achieve via trade. The majority of the trades that I make to try to get a decent PVP Pokemon are with the people I interact with most: that is, Best Friends.

The perfect is the enemy of the good (friend)

The highest ranked Forretress you can get from a Best Friend trade is 5/15/13, rank 57. Now, this is still a good Pokemon. Its stat product is 98.75% of perfect. If you have this Pokemon, use it!2

Instead of urging you to eschew your friends and family and start a new life on the docks, constantly seeking out new trading partners with whom you have no emotional connection, what I'm trying to illustrate is when you should consider something good enough.

Let's switch Pokemon again, to an example given by Charm2Sul in my local Discord: they have a Skuntank that's 6/13/11, or rank #402 (97.24%). 401 out of the possible 4096 IV spreads are better than this one, which is 9.79%. So you might be tempted to keep looking instead of powering this one up: after all, it's like 1 in 10 that you find a better skunk!

But that's only wild catch. Here's the full breakdown:

  • 9.79%: Wild catch
  • 8.68%: Trade with Good Friend
  • 7.58%: Trade with Great Friend
  • 6.78%: Trade with Ultra Friend
  • 5.9%: Weather boosted catch
  • 4.73%: Trade with Best Friend
  • 0.46%: Hatched/Raid/Research
  • 0%: Lucky Trade

There are 401 better Skuntank than 6/13/11, but only 15.7% of those have their lowest IV as 5 or higher (ranks 52, 59, 60, 67, 72, 81, 128, 149, 150, 151 are the 10 best). Depending on what your local friendship situation is you may have a hard time doing better.

A skunk in the hand is worth two in the tall grass.

Ask a robot if you can do better

Much like determining IVs and calculating spread rank/stat products, this can and is facilitated by coding. You can ask wobbotfet !betterthan skuntank 6 13 11 to see what your chances are of getting a better spread from the different methods/floors.

There are always exceptions. No exceptions.

I should have covered this in the IV spread post. My apologies.

There are some Pokemon that either don't or barely reach 1500 CP at level 40 but are nevertheless strong enough to be meaningful in PVP. Medicham and Sableye max out at 1431 and 1476, respectively. Azumarill and Bastiodon barely make it.

PVP subverts the “bigger is better” IV rule, and these Pokemon in turn subvert that rule. If it maxes out at under 1500 then you do want as close to 15/15/15 as you can get.

For most Pokemon the IV floor is inversely proportional to your chance of getting a better one: floor goes up, chance goes down. For the Dust Busters the chance goes up: and a Lucky Trade would reduce the amount of your dust life savings you have to pour into the damn thing.

And then it starts getting weird

I was preparing a “weird esoterica” section but it rapidly started ballooning to the point where it detracted from the actual useful not-fun-edge-case information here. Expect that soon.

1 Or to trade with someone you've befriended but never interacted with, to be fair.

2 Except for how, in my experience, Forretress is only useful against grass, which Mantine also is and doesn't literally melt when faced with a fire type. I want to love my shiny golden orb, but I'm not sold.

Yet another building block in the PGo PVP series.

CP in Pokemon Go

CP itself has two functions in the game:

  • to be a reflection of a Pokemon's stats
  • to act as a cap for Great and Ultra league

That's it. Not just “as it relates to PVP”. That's it. CP is a summary of stats, not a stat itself that's ever used in the game.

The second point leads to an interesting nuance in CP-capped PVP.

The CP formula

CPM = (a number based on level from 0.094-0.79030001)
stamina = (base stamina + stamina IV) * CPM
attack = (base attack + attack IV) * CPM
defense = (base defense + defense IV) * CPM

CP = MAX(10, FLOOR(Stamina^0.5 * Attack * Def^0.5 / 10))

The key bit: you use the square root of stamina and defense when calculating CP, but the value of attack on its own. This means that a Pokemon's attack IV has a much larger impact on its CP than stamina or defense.

Bigger is better, unless...


See that line at the top about CPM? It's a multiplier that goes up with each level1. This means that in addition to attack, defense and HP/stamina, there's a fourth factor in how “good” a Pokemon is for PVP: how high of a level it can be and stay under the CP cap.

A case study: Charizard

Let's say you're preparing for the Silph Arena cup for the month when this was written (June 2019), which is the Rainbow Cup. PvPoke says the best overall (and lead) option is Charizard with Blast Burn.

“Great!”, you say. “I managed to get a perfect 15/15/15 one with Blast Burn on Community Day, and it's Great League eligible!”2

But not so fast! Let's look at the TDO (Total Damage Output) of that flappy boy:

  • IVs: 15/15/15
  • CP: 1486
  • Level: 18.0
  • Attack: 134.887571
  • Defense: 106.549846
  • HP: 113
  • Stat Product: 1624064.2406621396

1.6 million? That's a lot of... product? But consider this one (insert end-of-Clue title cards mentally)

  • IVs: 0/15/13
  • CP: 1500
  • Level: 19.5
  • Attack: 131.54500330559998
  • Defense: 110.89892655359999
  • HP: 117
  • Stat Product: 1706819.3602294535

Ignore the CP increase: it's a red herring3. You do take a 3 point hit to attack, but due to it being three levels higher the defense and HP are each 4 higher, giving you a stat product of 1.7 million. That's 100,000 higher! (100,000 what? uh, product!)

The whole paradigm done just shifted

attack is out, defense/stamina are in!

Learning this bit of mostly-inconsequential math singlehandedly turned Pokemon that are appraised as having attack be their best stat from being the ones I star/evolve to being fodder for Professor Willow's Pokemon Compost Machine.

Making robots do this math for you

The most popular site to check an IV spread is GO : Stadium (neé Pokemon PVP Club). I like mine better, because I made it. (It also does the calculations client-side, which means the burden of calculation isn't on my server, which is — along with a baby — what I gather caused the demise of Pokemon PVP Club)

I've also written a Discord bot, wobbotfet, which you can query with !rank charizard 0 15 13, or !vrank charizard 0 15 13 to get the detailed numbers of how it calculated the rankings. It's how I gathered the numbers for this post.

If you'd like to add wobbotfet to your Discord server, here's the URL to invite it. !rank help will tell you nearly everything you need to know, including a third command that I'm leaving for another post.

1 Really, each half level, because instead of numbering levels from 1-79 like decent people Niantic has made then 1, 1.5, 2, 2.5 ... 39.5, 40. Because, as far as I can tell, they want to make those of us who code in statically typed languages like civilized folk cast between int and float a bunch.

2 First off, stop gloating. Damn CD Pokemon released before we cared about sub-1500 CP, grumble grumble...

3 Like Communism.

I just added this to the custom stylesheet of my blog (/me/c/<blogname>):

table {
    width: 100%;
    border-collapse: collapse;

th, td {
    padding: 5px;

th {
    border-bottom: 1px solid #f2f2f2;

tr:nth-child(even) {
    background-color: #f2f2f2;

Lately I've been focusing my obsessive nature on Pokemon Go. Which, yes, is still a thing, and you can do more than catch another goddamn Pidgey.

Because, like the main games, you can chuck some balls at the various critters that pop up and call it a day — which is a completely valid way to play — or you can get deep into the mechanics. I've gone a step farther and written tooling to explore the mechanics. (Parents, next time you hear “teach your kids coding!”, remember they might turn out like me)


There's been plenty written about Individual Values in Pokemon Go, so I'll just link to Pokemon GO Hub and call it a day.

Except to add that the values of an IV — a number between 0 and 15 — can be expressed in four bits (0000 to 1111). Which is also known as a nybble, because it's smaller than a byte. Computers can occasionally be adorable.

IV floors

Something that I haven't really seen documented anywhere, at least in a concise manner, is that in certain circumstances a Pokemon's stats will meet one of a number of IV floors: that is, each one is guaranteed to be at least a certain number.

The most well known of these is a “lucky Pokemon”, which was the result of a Lucky Trade (whether by crazy random happenstance or through a trade with a Lucky Friend). Lucky Pokemon are guaranteed to have at least 12/12/12 IVs1.

But there's more! Here, have a table:

Minimum IV Condition
0 Wild catch
1 Trade with a Good Friend (one heart)
2 Trade with a Great Friend (two hearts)
3 Trade with an Ultra Friend (three hearts)
4 Weather Boosted
5 Trade with a Best Friend
10 Hatched/Raid/Research breakthrough
12 Lucky Trade

For PVE (gym battles) and raids, higher IVs are always better. For PVP, though, things get trickier, and as such trading with better friends can actually make it harder to get the stats you want. But more on that later.

1 I'll be using the standard format of attack/defense/stamina (or HP) for IVs