Table of Contents
- General Formula for Cost Curves
- Linear sequence formulas
- Exponential sequence formulas
- Flat cost sequence formula
- Practical Example - Sinks
- Practical Example - Leaderboards
- Test equations
This series is intended to help you think through how to set up and balance game mechanics and progression. To give you a bit of context, I typically support games (and do not make them) focusing on balance, equation structure, and the implementation of mechanics. So the goal output of my scripts are typically csv files or INSERT commands for SQL tables.
If you'd like to try and run elements of the Python code the main packages that I rely on are Pandas, Numpy, and Matplotlib. I'm running Python 3.6ish at the moment.
2. General Formula for Cost Curves
I recommend linear sequences or exponential sequences. They resolve clearly across the three equations that we will typically rely upon to calculate costs for players.
- What is the cost of a specific level (100) for a player
- What is the total cost of progressing to level 100 for a player (typically more useful for modeling and leaderboards)
- What is the cost for advancing from level 100 to level 125. This allows you to save compute and lets players use a slider or "+5"/"max" buttons without loops
That generally means relying on linear and exponential sequences. Those resolve really nicely on all three formula sets.
3. Linear: Cost = a*x + b
#Define Linear Summation Equations #Linear Summations are where you have Lvl 1 = 1, Lvl 2 = 2, etc. def sequenceLinearCost(a,b,x): return a*x+b def sequenceLinearSum(a,b,x): return ((x)/2)*(a*(x+1)+2*b) def sequenceLinearDifference(a,b,x,r): #x=currentlvl,r=resources,a&b constants #Example: Given you have upgraded to lvl 5, how many more upgrades can you buy with 500 gold and the cost curve a*x+b xs = sequenceLinearSum(a,b,x); qa = a /2 qb = (2*b+a)/2 qc = -(xs + r) return (-1*qb+(qb**2-4*qa*qc)**0.5)/(2*qa)
Note that the
sequenceLinearDifference(a,b,x,r) function is effectively using the Quadratic Formula to come to a solution.
One nice thing to note is that as that as the curve progresses you can roughly assume that "Every time a player increases their sum investment by 300% they will double (see +100%) to the bonus they are receiving. As it is a sequence and not a curve the ratios start below 4.0 and approach it with time. Examples below:
Cost/Cost = 3.66
Cost/Cost = 3.81
Cost/Cost = 3.88
Cost/Cost = 3.99
Part of the reason for the error rate is that a sequence does not have the same cost progression rate as a curve so they don't match 1:1 across all ranges.
4. Exponential: Cost = a*b^x
#Exponential Equations def sequenceExponentialCost(a,b,x): return a*(b**x) def sequenceExponentialSum(a,b,x): return a*(1-b**(x+1))/(1-b)-a def sequenceExponentialDifference(a,b,x,r): return math.log(b**(x+1)-(r*(1-b)/a))/math.log(b)-1
5. Flat: Cost = a
You may never use this one, but just know it's there. I think a use case for this would be "Players get 1 Soul Coin per day, they can spend it on 1 of 10 options. Each level in that option gives diminishing benefit gains"
# Flat Costs - every level costs the same # If you have flat cost the bonus will generally be sublinear def FlatCost(a,x): return a def FlatSum(a,x): return a*x def FlatDifference(a,x,r): return a+r/x
Of the above I tend to prefer linear progression. It is easier to manage mentally in that cost is linear and sum cost is quadratic.
6. Practical Example - Resource Sinks
A common use case for the Difference equations will be the above example. A player wants to invest some amount of money in a sink and you want to be able to define how much they can spend without loops.
First let's define the cost curve...
#Define Current Player Variables BaseCost = 5 #Gold LevelCost = 10 #Gold CurrentLevel = 3 CurrentGold = 45+55+65+75+85
For lazy testing I find it's easiest to design the gold total as a sum of each level cost. In this case the equation is "Cost = 10(Level) + 5" and the starting level is 3 so the next five levels cost 45,55,65,75,85 each.
Now let's define our buttons...
#Define Max Level and then Floor it - this impacts the top selectable value on the bar MaxLevel = math.floor(sequenceLinearDifference(LevelCost,BaseCost,CurrentLevel,CurrentGold)) #+5 Button: Active/Inactive and cost Plus5ButtonActive = (MaxLevel>=5) #Cost is the difference between the sum cost of the new level and old level Plus5ButtonCost = sequenceLinearSum(LevelCost,BaseCost,CurrentLevel+5)-sequenceLinearSum(LevelCost,BaseCost,CurrentLevel) #+10 Button: Active/Inactive and cost Plus10ButtonActive = (MaxLevel>=10) Plus10ButtonCost = sequenceLinearSum(LevelCost,BaseCost,CurrentLevel+10)-sequenceLinearSum(LevelCost,BaseCost,CurrentLevel) #+Max Button: Active/Inactive and cost MaxLevelButtonActive = (MaxLevel>=1) MaxLevelButtonCost = sequenceLinearSum(LevelCost,BaseCost,MaxLevel)-sequenceLinearSum(LevelCost,BaseCost,CurrentLevel) ``
MaxLevel will return the max level achievable given the player's current achievement and gold reserves, in this case 8. Note that we floor the function.
The ButtonXActive functions define whether or not the buttons are selectable. The PlusXButtonCost functions define the sum cost to the player should they select that button.
With these equations you can provide a nice UI for players without sacrificing too much compute.
6. Practical Example - Leaderboards
Let's see who has spent the most gold on sinks. For a single sink you can compare using something like current level. However, once you start to compare against multiple sinks it gets a bit tricker. That said, if you apply the sum equations you get totals to rank against quite easily.
PlayerSink1Level = 10 PlayerSink2Level = 20 PlayerSink3Level = 30 #Define Cost Functions a1 = 10 b1 = 5 a2 = 13 b2 = 15 a3 = 7 b3 = 13 #Linear Totals TotalSpendLinear = sequenceLinearSum(a1,b1,PlayerSink1Level)+sequenceLinearSum(a2,b2,PlayerSink2Level)+sequenceLinearSum(a3,b3,PlayerSink3Level)
With this method we can look across multiple categories to see who has invested the most. Any situation where you have multiple categories of play where level progression is inconsistent are good use cases for this approach.
7. Test Equations
I typically stress about whether I've written equations right, so in an ideal world I'll have test equations to test with. Below are a sampling of the tests I've written.
#Test Cases print("Linear Sequence Sum") print(sequenceLinearSum(2,1,4)) #24 print(sequenceLinearSum(1,2,5)) #25 print(sequenceLinearSum(2,1,5)) #35 print(sequenceLinearSum(1,2,6)) #33 print("Exponential Sequence Sum") print(sequenceExponentialSum(1,2,1)) #2 print(sequenceExponentialSum(2,2,1)) #4 print(sequenceExponentialSum(1,2,2)) #6 print(sequenceExponentialSum(1,2,3)) #14 print(sequenceExponentialSum(2,2,3)) #28 print("Flat Sums") print(FlatSum(1,2)) #2 print(FlatSum(16,16)) #256 print(FlatSum(4,4)) #16 print(FlatSum(8,4)) #32 print("Exponential Difference") print(sequenceExponentialDifference(1,2,0,2)) # 1.0, 2**1 = 2 print(sequenceExponentialDifference(1,2,1,4)) # 2.0, 2**2 = 4 print(sequenceExponentialDifference(1,2,1,12)) # 3.0, 2**2 + 2**3 = 12 print(sequenceExponentialDifference(2,2,1,56)) # 4.0 2*(2**2+2**3+2**4) print(sequenceExponentialDifference(6,2,1,24)) # 2.0 6*(2**2) print(sequenceExponentialDifference(1,3,2,108)) # 4.0 3**3+3**4 print(sequenceExponentialDifference(2,3,2,54)) # 3.0 2*3**3 print("Linear Difference") print(sequenceLinearDifference(2,0,4,10)) # 5.0 print(sequenceLinearDifference(2,0,4,22)) # 6.0 print(sequenceLinearDifference(2,2,4,12)) # 5.0 print(sequenceLinearDifference(4,3,4,23)) # 5.0 print(sequenceLinearDifference(8,0,3,120)) # 6.0
8. Edge Cases - Rounding
One edge case to think about when doing summations and differences is that you may hit some edge cases for rounding. One example of the above is that the equation for sequenceExponentialDifference does not always kick out the target integer - see this example below.
sequenceExponentialDifference(1,3,2,108) = 3.999999999999999
The hacky way to address this is just to multiply, ceiling and then divide the function before returning, perhaps something along the lines of the following:
def sequenceExponentialDifference(a,b,x,r): return math.ceil(10000*(math.log(b**(x+1)-(r*(1-b)/a))/math.log(b)-1))/10000
Depending on the libraries available to you there may be a better way to address.
Hopefully this helps serve as a bit of a starter pack on math equations. In future posts we will apply them to create cost curves and start to model out progression.
If there are specific topics you would like covered please let me know. If you have specific questions on your curves I'm happy to take a look.