·5 min read

Pricing an economy off its difficulty model

Levels differ 13x in the thinking they demand, so a flat coin reward is wrong on nearly all of them. Tying payouts to the difficulty walk.

gamedevgame-economyfree-to-playdifficulty
The economy module the simulator sharesGist · IndieCoreDev/8f228b50bc004fc45e826368d0d1da22

Part 8 of 10 on building a word-block puzzle engine. Part 7: testing a level pack you did not write.

The first economy paid 20 coins for finishing a level. Every level. It survived about a week of my own play before it was obviously broken, and the reason is a number from part 3.

A level's decision load — the total count of real choices the board forces across a full solve — runs from 7.5 on the easiest tier to 96.7 on the hardest. A 13× spread. A flat rate pays the same for a two-minute board and a twenty-minute one, which teaches the player to grind the easy end of whatever is unlocked and never touch the interesting boards.

Reward follows the thinking, not the rating

The payout is tied to decision load rather than the 0–10 difficulty rating, and that distinction is deliberate. The rating is a percentile — a comparison against other boards. The decision load is an absolute count of work done. Coins should pay for work.

economy.js Open in gist
// A coin economy priced off the difficulty model, as data and pure functions.
//
// Nothing in here touches React, the store, or storage. That is not tidiness —
// it is what lets the balance SIMULATOR import the same module. A simulator
// that re-implements the economy tells you about a game you are not shipping,
// and it diverges on the first tuning pass, silently, because nothing connects
// them.
//
// WHERE THE NUMBERS COME FROM
//
// A level's difficulty is its DECISION LOAD: walk the board the way the
// difficulty model does — fill the topmost-leftmost empty cell, count how many
// pieces in hand could go there — and sum (choices - 1) over every step.
//
// It runs 7.5 on the easiest tier to 96.7 on the hardest. A 13x spread, which
// is why nothing here is a flat rate. The first version paid 20 coins for
// finishing a level, every level, and taught players to grind the easy end.

/** Board size bands. Pieces per board runs 4..30, median 15. */
export const BANDS = ['S', 'M', 'L'];
export const bandFor = (pieceCount) => (pieceCount <= 9 ? 'S' : pieceCount <= 17 ? 'M' : 'L');

// ---------------------------------------------------------------------------
// Earning
// ---------------------------------------------------------------------------

/**
 * What a first clear pays.
 *
 * Tied to the decision load rather than the 0-10 rating, and the distinction is
 * deliberate: the rating is a percentile — a comparison against other levels —
 * while the decision load is an absolute count of work done. Coins pay for work.
 *
 * Yields 8 coins on an Easy board, 35 on an Expert one. The floor exists so a
 * tiny 3x3 still feels like it paid something; the slope is what makes a hard
 * board worth choosing.
 */
export const CLEAR_BASE = 6;
export const CLEAR_PER_DECISION = 0.30;

export const clearReward = (decisionLoad) =>
  Math.round(CLEAR_BASE + CLEAR_PER_DECISION * decisionLoad);

/**
 * Star bonuses, paid as a DELTA against the highest tier ever banked for that
 * level. Replaying a 3-star level pays nothing; dragging a 1-star up to 3 pays
 * the difference once and never again.
 *
 * Without the delta, replaying your easiest completed level is the optimal way
 * to earn — a broken economy and a boring game.
 *
 * These are deliberately small. Simulation put star bonuses at 45% of ALL
 * income — the single largest faucet, larger than clears, ads or the daily
 * chest — while three stars is earned on about two thirds of levels. A generous
 * bonus here is a tax-free salary rather than a reward for excellence.
 *
 * The top delta is also the ceiling on what any single purchase can unlock, so
 * it sets the price floor: if a hint costs less than this, the optimal strategy
 * is to buy hints until three stars is guaranteed, forever, and the economy
 * becomes a vending machine.
 */
export const STAR_BONUS = { 0: 0, 1: 0, 2: 6, 3: 18 };

export const starDelta = (stars, tierAlreadyPaid) =>
  Math.max(0, (STAR_BONUS[stars] ?? 0) - (STAR_BONUS[tierAlreadyPaid] ?? 0));

/**
 * Rewarded-video multiplier on a first clear. Never offered on a replay.
 *
 * A multiplier rather than a flat coin amount, so the ad reward inherits the
 * difficulty scaling for free: doubling a 35-coin Expert clear is worth
 * watching, doubling an 8-coin Easy one is not, and the player self-selects.
 * A flat "watch for 25 coins" inverts that — farm the easiest level, watch,
 * repeat.
 */
export const DOUBLER = 2;
export const PEAK_DOUBLER = 3;

/**
 * Daily login chest: a 7-day cycle that grows a little each time it completes,
 * then STOPS.
 *
 * The escalation is capped because the version this replaced multiplied without
 * limit — by the eighth cycle its day-7 chest paid 225 coins, more than three
 * level clears, for opening the app. Retention rewards should be worth showing
 * up for and never worth more than playing.
 */
export const DAILY_CHEST = [20, 22, 25, 28, 32, 40, 70];
export const CHEST_GROWTH = 0.2;
export const CHEST_MAX_CYCLES = 2;

export const dailyChest = (totalDaysClaimed = 0) => {
  const cycle = Math.min(CHEST_MAX_CYCLES, Math.floor(totalDaysClaimed / DAILY_CHEST.length));
  const day = totalDaysClaimed % DAILY_CHEST.length;
  return Math.round(DAILY_CHEST[day] * (1 + CHEST_GROWTH * cycle));
};

// ---------------------------------------------------------------------------
// The invariants the simulator asserts (--test), rather than numbers it prints
// ---------------------------------------------------------------------------
//
//   - no sequence of legitimate actions produces unbounded coins
//   - every purchasable item is reachable within a bounded number of levels
//     from zero
//   - replaying a completed level never nets positive
//   - the difficulty walk these prices are computed from still matches the
//     metrics shipped on every board
//
// The last one is the join to the rest of the system. Prices derive from the
// same walk that rates the levels and drives the power-ups. If that walk drifts,
// the game keeps running and every price silently becomes wrong.

Eight coins on an Easy board, 35 on an Expert one. The floor exists so a tiny 3×3 still feels like it paid something; the slope is what makes a hard board worth choosing.

The module knows nothing about the game

Everything the game charges, pays or caps is in one file, and nothing in that file touches React, the store, or storage. It is data and pure functions.

That is not tidiness. It is what lets the simulator import the same module. A balance simulator that re-implements the economy is a simulator that tells you about a game you are not shipping — and it will diverge on the first tuning pass, silently, because nothing connects them.

Sharing the module means a number tuned in one place cannot drift from the other, and the balance the simulator reports is the balance that ships.

What the simulation actually found

I would not have found either of these by playing.

Star bonuses were 45% of all income. The single largest faucet in the game — larger than level completion, larger than ads, larger than the daily chest. And three stars is earned on about two thirds of levels, which makes a generous star bonus a tax-free salary rather than a reward for excellence.

They came down hard, and STAR_BONUS above pays them as a delta against the highest tier ever banked for that level. Replaying a 3-star level pays nothing. Dragging a 1-star up to 3 pays the difference, once, ever. Without the delta, replaying your easiest completed level is the optimal way to earn, which is both a broken economy and a boring game.

The daily chest escalated without limit. It grows a little each time the seven-day cycle completes, which is a nice feeling for the first fortnight. Left unbounded, by the eighth cycle the day-7 chest paid 225 coins — more than three level clears — for opening the app. CHEST_MAX_CYCLES is the whole fix. Retention rewards should be worth showing up for and never worth more than playing.

The price floor is set by the largest single payout

A subtle constraint I got wrong first. The most any single purchase can unlock is bounded by the biggest reward it can lead to — here, the top star delta. If a hint costs less than that, the optimal strategy is to buy hints until three stars is guaranteed, on every level, forever. The economy becomes a vending machine.

So the top star delta sets a floor under every price, and the floor is written down next to the constant that produces it. The relationship between two numbers in different sections of a file is exactly the thing that gets broken by someone reasonably tuning one of them.

Ads pay a multiplier, not a currency

Rewarded video doubles a first clear and never appears on a replay.

Tying the ad reward to the level rather than to a fixed coin amount means it inherits the difficulty scaling for free — doubling a 35-coin Expert clear is worth watching, doubling an 8-coin Easy one is not, and the player self-selects. A flat "watch for 25 coins" would have inverted that: farm the easiest level, watch the ad, repeat.

There is no interstitial between levels. That was a product decision rather than an economic one and I do not have data to defend it; what I can say is that it removed an entire class of tuning problem, because there was no longer a knob whose optimum is "as often as players tolerate".

Invariants, not just numbers

The simulator has a --test mode that asserts properties rather than printing a report:

  • No sequence of legitimate actions produces unbounded coins.
  • Every purchasable item is reachable within a bounded number of levels from zero.
  • Replaying a completed level never nets positive.
  • The difficulty walk the prices are computed from still matches the metrics shipped on every board (part 7).

The last one is the join between this post and the rest of the series. Prices are derived from the same walk that rates the levels and drives the power-ups. If that walk drifts, the game keeps running and every price silently becomes wrong. A test that says so is worth more than a spreadsheet.

What I still do not know

Whether any of it is right. Every number here is defended against a simulation, and a simulation is a model of a player I invented.

The honest position is that this is a starting balance whose main property is being internally consistent and cheap to change — one file, no game code, a simulator that shares it. When real telemetry arrives, most of these constants will move. The structure is built so that moving them is a one-line change with a test run behind it, rather than an archaeology expedition through the UI code.


Next: part 9, cutting a Capacitor Android build in half — where the download actually goes, and the consumer ProGuard rule that cost 9,304 classes.

More posts

New writing when there is something worth saying. By email or by RSS, whichever you prefer.