The perfect experience / level curve
by CyberShadow on May.16, 2010, under Code
I was recently tasked to create a required-experience-per-level table for a game – that is, create a table which indicates how many experience points the player must earn to progress to a certain experience level. The requirement to reach the next level must continuously increase (the “double derivate” must be non-negative). Of course, it is easy to come up with a function to satisfy that requirement alone, but what about generating a level curve that’s also aesthetic? I mean, it’s much nicer to look forward to reaching 15000 points than, say, 16384.
So, I wrote a small program to attempt to generate experience ranks with the following properties:
- Continuous acceleration
- Smoothness (calculated as “roughness”, using the sum of squares of the “triple-derivate”)
- Roundness (100 is more round than 150, 150 is more round than 160, 160 is more round than 165 etc.)
- Emphasis on the roundness of the last level
The qualifying factor here is smoothness vs. roundness. Here are two sample outputs for 19 levels (actual output multiplied by 100):
| Level | Smoother | Rounder |
|---|---|---|
| 1 | 0 | 0 |
| 2 | 100 | 100 |
| 3 | 400 | 300 |
| 4 | 1000 | 600 |
| 5 | 2000 | 1000 |
| 6 | 3200 | 2100 |
| 7 | 5000 | 3500 |
| 8 | 7500 | 5000 |
| 9 | 11000 | 7000 |
| 10 | 15000 | 10000 |
| 11 | 20000 | 15000 |
| 12 | 26000 | 22000 |
| 13 | 33000 | 30500 |
| 14 | 41000 | 40000 |
| 15 | 50000 | 50000 |
| 16 | 60000 | 61000 |
| 17 | 71500 | 73000 |
| 18 | 85000 | 86000 |
| 19 | 100000 | 100000 |
The code:
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>
#define N 19
#define FACTOR_RANK_ROUNDNESS 50
#define FACTOR_LAST_RANK_ROUNDNESS (FACTOR_RANK_ROUNDNESS*10)
#define MAX_MUTATIONS 3
#define ITERATIONS 50000000
int roundness(int n)
{
if (n == 0)
return -1000;
while (n && n%10==0)
n = n/10;
if (n==1)
return 2;
int r = 2;
if (n%10 == 5)
r++;
while (n)
n = n/10, r-=2;
return r;
}
int sqr(int x) { return x*x; }
struct P
{
int dd[N];
int d[N];
int n[N];
__forceinline int fitnessA()
{
int total = 0;
for (int i=1; i<N-1; i++)
total += roundness(n[i]) * FACTOR_RANK_ROUNDNESS;
total += roundness(n[N-1]) * FACTOR_LAST_RANK_ROUNDNESS;
return total;
}
__forceinline int fitnessB()
{
int total = 0;
for (int i=1; i<N; i++)
total -= sqr(abs(dd[i] - dd[i-1]));
return total;
}
int fitness()
{
return fitnessA() + fitnessB();
}
void populate()
{
for (int i=0; i<N; i++)
dd[i] = i;
}
void update()
{
d[0] = 0;
for (int i=1; i<N; i++)
d[i] = d[i-1] + dd[i];
n[0] = 0;
for (int i=1; i<N; i++)
n[i] = n[i-1] + d[i];
}
void mutate()
{
int mutations = 2 * (1 + rand() % MAX_MUTATIONS);
for (int m=0; m<mutations; m++)
{
int i = 1 + rand()%(N-1);
int n = dd[i];
if (n == 0)
dd[i]++;
else
dd[i] += (rand()%n) - (n/2);
//dd[i] = rand()%(n*2);
}
}
void print()
{
printf("%d,%d - [", fitnessA(), fitnessB());
for (int i=0; i<N-1; i++)
printf("%d,", n[i]);
printf("%d]\n", n[N-1]);
}
};
P p, n;
int main()
{
int fb, fp, fn;
fb = INT_MIN;
srand(time(NULL));
while (true)
{
p.populate();
p.update();
fp = p.fitness();
int iterations = 0;
while (iterations < ITERATIONS)
{
n = p;
n.mutate();
n.update();
fn = n.fitness();
if (fn > fp)
{
p = n;
fp = fn;
iterations = 0;
}
else
iterations++;
}
if (fp >= fb)
{
p.print();
fflush(stdout);
fb = fp;
}
}
}