Pokémon Experience


I have been experimenting with Plots.jl and I have also been binging videos of solo Pokémon playthroughs from Scott’s Thoughts. The different rate at which Pokémon gain experience is one of the largest differences in how long it will take to complete a playthrough. Pokémon are divided into six level-up groups: fluctuating, slow, medium slow, medium fast, fast, and erratic. The graphs below show how these groups vary as level increases.

plot of pokemon experience levels

While analyzing the math behind Pokémon is fun - and will likely result in more blog posts here or over at GottaCatchEmAll.jl - the rest of this post will focus on the Julia code to create the previous graphs.

First up, let’s define an enum to represent the different groups.

@enum Group begin
    Fluctuating
    Slow
    MediumSlow
    MediumFast
    Fast
    Erratic
end

The level-up groups are defined as functions of the target level. So we need a function which accepts the level and group and returns the required experience - exp(l::Int, g::Group)::Int. The obvious structure for exp is to branch on g. We could write that directly as a series of if, elseif, else statements. Or we could take advantage of Julia’s type system to handle the branching for us. Using value types we can define separate functions for each enum variant. This is potentially a performance challenge12. Tread lightly when using this in production, but irrelevant for this example.

Onward to defining exp. Here is the primary entry point allowing users to type exp(8, MediumFast). While not required this is a more approachable interface that hides the internal/Julia-specific implementation details.

exp(l::Int, g::Group) = exp(l, Val(g))

Each of these functions will have a similar type signature and will have to use max and floor to handle edge cases and converting to integers. Let’s define a macro to handle this repetition.

macro exp(group, eq)
    eval(quote
             exp(l::Int, ::Val{$group})::Int = max(0, floor($eq))
         end)
end

With the enum and macro we can now simply type the mathematical function definitions for each group. These were sourced from Bulbapedia and pokered.

@exp(Fast, 4l^3/5)
@exp(MediumFast, l^3)
@exp(MediumSlow, 6l^3/5 - 15l^2 + 100l - 140)
@exp(Slow, 5l^3/4)
@exp(Fluctuating, begin
         if l < 15
             l^3*(floor((l+1)/3) + 24)/50
         elseif l < 36
             l^3*(l+14)/50
         else
             l^3*(floor(l/2)+32)/50
         end
     end)
@exp(Erratic, begin
         if l < 50
             l^3*(100-l)/50
         elseif l < 68
             l^3*(150-l)/100
         elseif l < 98
             l^3*floor((1911-10l)/3)/500
         else
             l^3*(160-l)/100
         end
     end)

Now we define some colors to match this website. I opted to use hex notation and divide by 0xff to directly match the css statements. Least mental overhead while not the prettiest.

const darksienna = RGB(0x23/0xff, 0x0c/0xff, 0x0f/0xff)
const antiquewhitelight = RGB(0xfd/0xff, 0xf9/0xff, 0xf3/0xff)
const brass = RGB(0xb5/0xff, 0xa6/0xff, 0x42/0xff)
const cafenoir = RGB(0x4e/0xff, 0x38/0xff, 0x22/0xff)
const flame = RGB(0xd4/0xff, 0x51/0xff, 0x13/0xff)
const polishedpine = RGB(0x4e/0xff, 0xa6/0xff, 0x99/0xff)
const englishviolet = RGB(0x51/0xff, 0x35/0xff, 0x5a/0xff)
const puce = RGB(0xd4/0xff, 0x96/0xff, 0xa7/0xff)

const seriescolors = [brass,
                      englishviolet,
                      puce,
                      cafenoir,
                      flame,
                      polishedpine]

Plotting the total experience to reach a given level. Data is tracked as a single number and level is computed from total experience gained. Functional programming ftw! Bonus points for as a unicode character in the list comprehension and |> to reduce cognitive overhead of function composition.

function plotcumulative()
    p = plot(; xlims=(0, 100),
               fontfamily="monospace",
               title="Minimum Experience to Reach Level",
               xlabel="Level",
               ylabel="Experience Points",
               formatter=:plain,
               legend_foreground_color=nothing,
               size=(1080, 540))

    for (i, g) in Group |> instances |> enumerate
        plot!(p,
              [exp(l, g) for l ∈ 1:100];
              color=seriescolors[i],
              label="$g")
    end

    p
end

How much experience to get to the next level? Just take the difference.

function plotdelta()
    p = plot(; xlims=(0, 100),
               fontfamily="monospace",
               title="Experience to Next Level",
               xlabel="Level",
               ylabel="Experience Points",
               formatter=:plain,
               legend_foreground_color=nothing,
               size=(1080, 540))
    for (i, g) in Group |> instances |> enumerate
        plot!(p,
              [exp(l, g) - exp(l-1, g) for l ∈ 1:100];
              color=seriescolors[i],
              label="$g")
    end
    p
end

And finally, merge the two plots together into a single image.

function plotall()
    savefig(plot(plotcumulative(),
                 plotdelta();
                 layout=(2,1),
                 fontfamily="monospace",
                 bgcolor=antiquewhitelight,
                 fgcolor=darksienna,
                 size=(1080,1080)),
            "exp.png")
    nothing
end

The full source code can be found here: PokemonExperience.jl.


  1. The dangers of abusing multiple dispatch
  2. Value Type Performance