Lesson 07

Difficulty and pacing: make the game challenging, not hostile.

A difficult game can still be fair. A hostile game asks for reactions before the player understands the controls. Tuning is the difference between "one more try" and closing the tab.

IntermediateBalancingTime management30-45 minutes

1. Put difficulty into data

Moonlight Diner uses a difficulty table instead of scattering timing numbers through the code. That makes balancing visible. Easy mode gives more patience, slower customer arrival and better tips. Hard mode does the opposite.

var DIFF = {
  easy:   { patience: 120, interval: 18, cookMod: 0.85, floodDur: 10, powerChance: 0.05, tipMod: 1.5, name: "Easy" },
  normal: { patience: 90,  interval: 14, cookMod: 1.0,  floodDur: 15, powerChance: 0.10, tipMod: 1.0, name: "Normal" },
  hard:   { patience: 65,  interval: 11, cookMod: 1.15, floodDur: 20, powerChance: 0.15, tipMod: 0.8, name: "Hard" }
};

function getDiff() {
  return DIFF[state.difficulty] || DIFF.normal;
}

Source pattern: Moonlight Diner.

The key lesson is that difficulty is not one number. It is a bundle of pressures: time, arrival rate, task length, mistakes, rewards and random events. If easy mode is still too hard, increase the gaps between pressures rather than only increasing player health or score rewards.

2. Tune spawn intervals around human actions

In time management games, the player has to see an order, decide what it needs, click the station and wait for progress. If orders spawn faster than that loop, the game becomes impossible even if the code technically works.

state.customerTimer -= speedDt;
if (state.customerTimer <= 0 || activeCustomerCount() === 0) {
    spawnCustomer();
    var interval = getDiff().interval;
    if (state.mode === "endless") interval = 12;
    if (state.rushActive) interval = interval * 0.5;
    state.customerTimer = interval + (Math.random() * 4 - 2);
}

That final random offset creates variation, but the base interval still comes from difficulty data. This is exactly where many time management games break: random arrivals stack on top of short timers until the player cannot recover.

3. Use recovery windows

Good pacing has waves. Pressure rises, then the player gets a breath. If every second is more intense than the last, the game becomes exhausting. Recovery can be a quiet room after a boss, a slow customer after a rush, a healing pickup after a dangerous arena or a safe platform before a long jump.

function nextWaveDelay(wave) {
    if (wave % 5 === 0) return 14; // breathe after a boss wave
    if (wave < 3) return 10;       // teach the player first
    return Math.max(5, 10 - wave * 0.35);
}

Recovery windows are not "making it easy." They are where the player processes what just happened, upgrades, reads the UI and chooses to continue.

4. Progression should solve a felt problem

An upgrade is more satisfying when the player already felt the pain it solves. If cooking is too slow, buy a faster grill. If enemies swarm, buy a wider attack. If the platformer has long routes, unlock a double jump or dash. Progression should answer the player's experience.

function applyUpgrade(id) {
    if (id === "fast_grill") stationMods.grill *= 0.8;
    if (id === "patient_customers") patienceMod *= 1.18;
    if (id === "better_tips") tipMod *= 1.25;
    saveGame();
}

This is better than a generic score multiplier because the player can feel it immediately in the next round.

5. Balance checklist

  • Can a first-time player survive the first 60 seconds without perfect input?
  • Does easy mode change enough numbers that it actually feels different?
  • Does every new mechanic appear alone before it appears in combination?
  • Does the player get a recovery window after a high-pressure moment?
  • Do upgrades change decisions, or only inflate score?
  • Can you explain why hard mode is hard without saying "everything is faster"?

When tuning Supagames time management levels, the biggest lesson was simple: a game can look complete and still be unplayable if the timing was tuned by a machine instead of by a human hand.

6. A simple tuning worksheet

When a game feels too hard, write down the actual player loop. In a diner game it might be: notice customer, read order, click station, wait for cooking, click next station, serve. In an action game it might be: see enemy, dodge, attack, recover stamina, collect reward. Then put rough seconds next to each step.

const playerLoop = {
    noticeOrder: 1.0,
    chooseStation: 1.5,
    stationTravel: 0.5,
    cookWait: 6.0,
    serveClick: 1.0
};

const minimumOrderTime =
    playerLoop.noticeOrder +
    playerLoop.chooseStation +
    playerLoop.stationTravel +
    playerLoop.cookWait +
    playerLoop.serveClick;

If the minimum loop takes 10 seconds, a 7-second patience timer is not "hard." It is impossible unless the player pre-acts before seeing the task. Easy mode should give enough time for reading and minor mistakes. Normal mode can assume familiarity. Hard mode can assume mastery, but still needs a valid route to success.

This is also how to tune tutorials. The first task should usually be slower than the rest of the game. Give the player a safe first customer, a slow first enemy or a clearly visible first jump, then increase pressure after they have performed the core verb once.

7. Progression pacing over a 30-minute game

For a longer browser game, progression should appear in waves. The first 5 minutes teach the core loop. Minutes 5-15 introduce combinations and upgrades. Minutes 15-25 test the player's build. The final minutes should pay off the theme with a boss, finale, deadline or epilogue.

A common mistake is unlocking everything too early. If the player gets every mechanic in the first two minutes, the rest of the session becomes repetition. A better rhythm is one meaningful new idea every few minutes, followed by a challenge that uses it.

Next: Preserve progress and difficulty choices. Back: Use randomness without unfair pressure.