The Faster You Mine, the Faster It Tips Over: A Game Economy as a Dynamic System
Money from mining buys drills, and drills mine more ore, so a player’s output grows like compound interest. Every price involved can look reasonable on its own and still add up to a positive feedback loop that traces a curve nobody designed. One way to get a grip on this is to treat the economy as a dynamic system: separate stocks from flows, describe change as “next = now + inflow - outflow”, and then look at feedback, delay and how sensitive the result is to each parameter.
Borrowing these tools comes with caveats. A game economy obeys no conservation law, since its rules can create and destroy resources out of nothing. Players are not particles; they change strategy as they go. A simulation tests assumptions; it cannot prove that real players will behave as the model does.
Stocks, flows and a difference equation
Chapter 4 of Game Mechanics: Advanced Game Design breaks an internal economy into resources, entities and four mechanics that act on resources: sources, drains, converters and traders. For analysis it also helps to separate two kinds of quantity:
- A stock is how much there is at a moment: ore in the warehouse, drills on the ship.
- A flow is how much per unit of time: 10 units mined per hour, 2 units of upkeep per hour.
Stocks change only through flows, which as a difference equation is:
stock[t+1] = stock[t] + production[t] - upkeep[t] - spending[t]
production depends on the number of drills, the number of drills depends on past spending, and there is the loop. The setup: each drill mines 10 units an hour, a drill costs 1,000, and a player who reinvests everything buys another as soon as they can afford it. In the continuous approximation the drill count grows by 10 / 1000 = 1% an hour and doubles roughly every ln 2 / 0.01 ≈ 69 hours.
Positive feedback, negative feedback and delay
Each run below lasts 500 hours of the same space-mining game, adding upkeep, rising prices and build time one at a time. Run on Python 3.13.16:
import math
import random
import sys
# One tick = one in-game hour. A drill digs ORE_PER_DRILL ore per tick.
ORE_PER_DRILL = 10.0
DRILL_PRICE = 1_000.0 # in ore
UPKEEP = 2.0 # ore per drill per tick
def run(ticks, reinvest, upkeep=0.0, price_growth=1.0, build_delay=0, eff=1.0):
"""stock[t+1] = stock[t] + production - upkeep - spending"""
stock, drills, price, queue = 0.0, 1, DRILL_PRICE, []
for t in range(ticks):
drills += sum(1 for ready in queue if ready == t)
queue = [r for r in queue if r > t]
stock += drills * ORE_PER_DRILL * eff - drills * upkeep
while reinvest and stock >= price:
stock -= price
if build_delay:
queue.append(t + build_delay)
else:
drills += 1
price *= price_growth
return stock, drills + len(queue)
print("== 1. stock and flow over 500 ticks ==")
for label, kw in (("never upgrade", dict(reinvest=False)),
("reinvest everything", dict(reinvest=True)),
("reinvest + upkeep 2/tick", dict(reinvest=True, upkeep=UPKEEP)),
("+ price x1.15 per drill", dict(reinvest=True, upkeep=UPKEEP, price_growth=1.15)),
("+ 48-tick build time", dict(reinvest=True, upkeep=UPKEEP, price_growth=1.15, build_delay=48))):
s, d = run(500, **kw)
print(f"{label:26} drills {d:6d} ore in stock {s:9.0f}")
print(f"growth rate with reinvest: ln2 / (ore per drill / price) ~ doubling every "
f"{math.log(2) / (ORE_PER_DRILL / DRILL_PRICE):.0f} ticks")
== 1. stock and flow over 500 ticks ==
never upgrade drills 1 ore in stock 5000
reinvest everything drills 82 ore in stock 620
reinvest + upkeep 2/tick drills 30 ore in stock 864
+ price x1.15 per drill drills 9 ore in stock 2265
+ 48-tick build time drills 7 ore in stock 2222
growth rate with reinvest: ln2 / (ore per drill / price) ~ doubling every 69 ticks
A player who never upgrades ends with 5,000 units, growing linearly. One who reinvests everything ends with 82 drills, so 82 times the hourly output. Output turning into capacity that produces more output is positive feedback, and given more time the gap only widens.
The next three rows add the usual brakes. Upkeep is an outflow proportional to the stock of drills: at 2 per drill per hour, net output per drill drops from 10 to 8 and the drill count from 82 to 30. A 20% cut looks modest, but it comes out of the growth rate, where compounding magnifies it.
A price that rises with every purchase lowers the return on each extra drill. At 15% more per drill the player ends with only 9, and actually holds more ore, because the next drill is already out of reach. Growth goes from exponential to something closer to logarithmic. The book calls this negative feedback; it holds growth down.
Build time adds a delay between paying and producing. The money is gone, but the drill starts digging 48 hours later, which costs 2 drills over 500 hours. Delay doesn’t necessarily slow a system down much, but it can make it oscillate, as the market model further down shows.
Which parameter matters most
Suppose the goal is to make players wait longer for their fiftieth drill. Nudging one parameter at a time and timing the result shows which ones carry the most weight:
print("\n== 2. sensitivity: ticks until 50 drills ==")
def ticks_to(target, **kw):
for t in range(1, 20_000):
if run(t, reinvest=True, **kw)[1] >= target:
return t
return None
base = dict(upkeep=UPKEEP, price_growth=1.05)
print(f"baseline {ticks_to(50, **base)}")
print(f"mining efficiency +10% {ticks_to(50, **base, eff=1.10)}")
print(f"mining efficiency -10% {ticks_to(50, **base, eff=0.90)}")
print(f"upkeep 2 -> 4 per tick {ticks_to(50, upkeep=4.0, price_growth=1.05)}")
print(f"price growth 1.05 -> 1.07 {ticks_to(50, upkeep=UPKEEP, price_growth=1.07)}")
== 2. sensitivity: ticks until 50 drills ==
baseline 1153
mining efficiency +10% 1025
mining efficiency -10% 1317
upkeep 2 -> 4 per tick 1537
price growth 1.05 -> 1.07 1781
A 10% change in mining efficiency moves the time to target by 11% to 14%, and not symmetrically: losing 10% hurts more than gaining 10% helps. Moving the price growth from 5% to 7% looks like a small tweak, yet the time goes up by 54%, because it is the base of an exponent and touches every drill bought. Parameters like that need the most care when tuning.
This only moves one parameter at a time. When parameters interact, say efficiency and upkeep changing together, you need a two-dimensional sweep or something more systematic, which is beyond this post.
Equilibrium and stability in a market with a lag
So far each player has their own economy. Once players trade with each other, price becomes a quantity that moves over time as well. Economics has a model for a market with a lag, the cobweb model: producers must decide how much to make before they know the price, so they go by last period’s. Take demand for ore as price = 100 - q, and suppose miners set this round’s output from last round’s price, q = slope · price[t-1]:
print("\n== 3. a market with lag: miners plan output from last round's price (cobweb) ==")
def cobweb(supply_slope, rounds=8, demand_a=100.0, demand_b=1.0):
"""demand: price = a - b*q. supply: q[t] = slope * price[t-1]."""
price, out = 60.0, []
for _ in range(rounds):
q = supply_slope * price
price = max(0.0, demand_a - demand_b * q)
out.append(round(price, 1))
return out
for slope in (0.5, 1.0, 1.5):
print(f"supply slope {slope}: prices {cobweb(slope)}")
== 3. a market with lag: miners plan output from last round's price (cobweb) ==
supply slope 0.5: prices [70.0, 65.0, 67.5, 66.2, 66.9, 66.6, 66.7, 66.6]
supply slope 1.0: prices [40.0, 60.0, 40.0, 60.0, 40.0, 60.0, 40.0, 60.0]
supply slope 1.5: prices [10.0, 85.0, 0.0, 100.0, 0.0, 100.0, 0.0, 100.0]
All three have an equilibrium; what differs is what happens after a disturbance. When miners respond gently (0.5), the price swings back and forth by less each time and settles at 66.7. When they respond exactly as strongly as demand (1.0), it swings between 40 and 60 for ever. When they overreact (1.5), the swings grow until they hit the 0 and 100 bounds, and from then on the price bounces between the two.
Rearranged, price[t] = 100 - slope · price[t-1], so each round’s deviation is multiplied by -slope and shrinks only if its magnitude is below 1. The Wikipedia article states the same condition in terms of the relative slopes of the supply and demand curves. In a game, if players really do decide today’s mining from yesterday’s price, or price data arrives late (a market screen updated daily), or upgrades take a long time to come online, the system is more prone to swing. Shorter delays, limits on how much output can change per period, or NPCs that buy and sell when the price strays too far all help to steady it.
Monte Carlo: strategy, luck and catching up
A deterministic model shows the trend. A real player’s position also depends on luck and on strategy. Chapter 8 of the book runs hundreds of simulated games with artificial players to compare strategies, and the same idea works here: 2,000 players, 30 days, four hours a day, and a 1% chance per dig of rare ore worth 300 (the same 1% as in A 1% Drop Rate and 100 Tries Without Luck), comparing three strategies:
print("\n== 4. 2,000 players, 30 days of 4 one-hour sessions, random drops ==")
def season(strategy, rng, days=30):
ore, credits, drills = 0, 0.0, 1
for _ in range(days * 4):
for _ in range(drills * 10): # 10 digs per drill per session
ore += 1
if rng.random() < 0.01: # 1% rare find, worth 300 credits
credits += 300
credits += ore * 1.0 # sell common ore
ore = 0
price = 1_000 * 1.15 ** (drills - 1)
credits -= drills * 2 * 1.0 # upkeep in credits
if strategy == "reinvest" and credits >= price:
credits, drills = credits - price, drills + 1
elif strategy == "keep 2,000 buffer" and credits - 2_000 >= price:
credits, drills = credits - price, drills + 1
worth = credits + sum(1_000 * 1.15 ** i for i in range(drills - 1)) * 0.5 # resale at half
return worth, drills
rng = random.Random(20261007)
for strategy in ("never upgrade", "reinvest", "keep 2,000 buffer"):
res = sorted(season(strategy, rng) for _ in range(2_000))
w = [r[0] for r in res]
d = sorted(r[1] for r in res)
print(f"{strategy:18} worth P10 {w[200]:8.0f} median {w[1000]:8.0f} P90 {w[1800]:8.0f}"
f" drills median {d[1000]} (P10 {d[200]}, P90 {d[1800]})")
late = sorted(season("reinvest", rng, days=15)[0] for _ in range(2_000))
print(f"joined on day 15, reinvest: median worth on day 30 {late[1000]:8.0f}")
print(f"\nPython {sys.version.split()[0]}")
== 4. 2,000 players, 30 days of 4 one-hour sessions, random drops ==
never upgrade worth P10 3360 median 4560 P90 6060 drills median 1 (P10 1, P90 1)
reinvest worth P10 6867 median 11288 P90 16261 drills median 11 (P10 8, P90 13)
keep 2,000 buffer worth P10 2948 median 5119 P90 9077 drills median 5 (P10 2, P90 8)
joined on day 15, reinvest: median worth on day 30 2507
Net worth here is cash plus drills valued at half their purchase price. That is an assumption, and a different valuation would change the numbers. On that basis, reinvesting everything gives a median 2.5 times that of never upgrading, and even its P10 beats the non-upgrader’s P90, so in this model strategy outweighs luck.
Keeping a 2,000 buffer leaves P10 below that of a player who never upgrades. The buffer delays every purchase, so later drills have not paid for themselves by the end, and with drills valued at half price and upkeep deducted, net worth comes out lower than doing nothing.
Within a single strategy, luck still separates P10 and P90 by 5 drills and more than a factor of two in net worth. A few early rare finds bring the first upgrade forward, and the feedback loop amplifies the head start.
A player who joins on day 15 and plays the best strategy until day 30 ends with a median net worth of 22% of an early starter’s. They played half the season but end with well under half the net worth, because compounding keeps widening the gap. Latecomers only catch up with something extra, such as a newcomer boost or costs that rise with a player’s wealth.
The book makes a related point with its SimWar example: two artificial players using the same strategy and finishing evenly does not show that the game is balanced. A simulation is only as credible as its strategies are close to real players, and real players change strategy once they see the results.
Economies on the record
CCP, which runs EVE Online, publishes a Monthly Economic Report covering ISK faucets and sinks, money supply and velocity, and production and destruction by region. The November 2024 report came out on 12 December, with a correction noting that the faucet and sink totals had been miscalculated. The report is a regular reconciliation of the game’s stocks and flows, the same accounting as the difference equation above.
Diablo III’s auction houses are a public record of a trading mechanic. Blizzard announced on 17 September 2013 that both the gold and real-money auction houses would close, and they closed on 18 March 2014. Wikipedia quotes the then game director Jay Wilson saying in March 2013 that the auction house “really hurt” the game. It was a trader running alongside in-game drops; I’m recording the public timeline here, not guessing at its effect on the economy.
That brings the series to an end. It went from space and time to a single random roll and a roll with memory; Pity Is Not a Higher Drop Rate priced one player’s path to a single item, and this post looks at what the whole system does once many such interactions pile up.
Notes
- Separating stocks from flows and writing next = now + inflow - outflow makes the links and loops between numbers visible. Income that buys capacity gives near-exponential positive feedback; upkeep and rising prices are negative feedback; build time adds delay.
- Changing one parameter at a time shows how sensitive the result is to it. A base of an exponent, like price growth, can swing the outcome even with a small change.
- With a lagged market, the strength of the response decides whether prices settle, oscillate for ever or diverge; shorter delays and caps on per-period change help.
- Compare strategies by quantiles and catch-up time: here a player starting on day 15 ends with 22% of an early starter’s median net worth. Economies are not conserved and players adapt, so check any conclusion against real data.
Further reading
- Ernest Adams and Joris Dormans, Game Mechanics: Advanced Game Design, chapters 4, 6 and 8: the four internal-economy mechanics, feedback structures and randomness, and artificial players for simulation and balancing. A game design textbook and the main framework for this post.
- Wikipedia, Cobweb model: Kaldor’s 1934 formulation, the lag assumption and the convergence condition. The original sources are in the article’s references.
- CCP Games, Monthly Economic Report - November 2024: EVE Online’s official economic report, with faucets and sinks and the correction notice. Data published by the operator.
- Wikipedia, Diablo III: the dates the auction houses’ closure was announced and carried out, and Jay Wilson’s remarks. The original reports are in the article’s references.
Ideas and technical judgement by Sheng; drafted with Claude · examples run on Python 3.13.16.