Epistemology

How to Make a Player Win 50% of the Time?

Adaptive difficulty with a constant-step Robbins–Monro balancer

Premise. You have one number in your game that controls how hard it is - enemy speed, enemy accuracy, spawn rate, whatever. This tutorial shows how use a stochastic algorithm that can tune that number automatically, game by game, so the player wins a target fraction of the time. The difficulty stops being a value you guess and becomes a value the game learns.


An example

In this example a simple fishing game similar to the game Stardew Valley was recreated. The fish speed was adapted by Robbins Monro algorithm to make the player win only at 50% chance:

Where is how fish speed was changing through the games and adapting:

undefined


The difficulty problem

Most games ship difficulty as a menu: Easy, Normal, Hard. It is a guess made once, by a designer, for an imagined "average" player. Two problems follow. First, no player is average - the number that frustrates one person bores another. Second, and worse, the same player changes. The setting that felt fair in hour one is trivial by hour ten.

Suppose we want something specific and measurable instead: the player should win about half the games. Win every time and the game is a chore; lose every time and it's a wall. Somewhere near a coin-flip is where most of the tension lives. The question is how to hit that number without hand-tuning, and keep hitting it as the player gets better.

The one-sentence idea

Treat difficulty like a room treats temperature: install a thermostat.

After each game you already know one fact — the player won or lost. If they're winning too often, nudge the difficulty knob toward harder. If they're losing too often, nudge it toward easier. Do a little of this after every single game and the knob drifts to wherever the win rate sits at your target. That's the whole idea. The rest of this post is about doing the nudge correctly and stably.

What the Robbins–Monro algorithm actually is

Robbins–Monro (1951) is a method for solving g(x) = target when you cannot see g(x) - you can only feed in an x and get back a noisy measurement of g(x).

That is exactly our situation. Let

g(value) = P(player wins | difficulty = value)

We can never observe this probability directly. All we get, after playing one game at a given value, is a single coin flip: 1 if they won, 0 if they lost. That flip is a noisy sample of g(value) - its long-run average is g(value), but any single flip is just 0 or 1.

Robbins–Monro says: you don't need the function, and you don't need its slope. You only need to know which direction the function moves, and then take a small step against the observed error. Repeat, and x settles near the root.

Reframing difficulty as root-finding

Our target is a win rate, say target = 0.5. Finding the difficulty that produces it is finding the root of

g(value) - target = 0

We approximate the unknown error g(value) - target with the only thing we can measure:

observed - target      where observed = won ? 1 : 0

A single game's observed - target is a wild, high-variance estimate of the true error (it's always either +0.5 or -0.5 when the target is 0.5). But it is unbiased - on average it points the right way — and averaging happens automatically as we take many small steps. This is the key trade the algorithm makes: accept noisy per-game signals, recover accuracy over many games.

The update rule, dissected

Here is the single line that does the work, straight from the base class:

CurrentValue -= alpha * DirectionSign * (observed - targetWinRate);
CurrentValue = Mathf.Clamp(CurrentValue, minValue, maxValue);

Read it piece by piece.

  • (observed - targetWinRate) — the error signal. Won when we wanted 50%? That's 1 - 0.5 = +0.5, "too easy." Lost? 0 - 0.5 = -0.5, "too hard." The magnitude says how far off this sample was; the sign says which way.

  • DirectionSign — which way the knob works. This is sign(dP(win)/dvalue), supplied by each game. It is -1 when a bigger value makes the game harder (fish swim faster ⇒ fewer wins) and +1 when a bigger value makes it easier (the AI blunders more ⇒ more wins). Multiplying by it converts a raw "too easy / too hard" reading into a correctly-signed instruction for this particular knob, no matter which way the knob happens to be wired.

  • alpha — the step size. How hard we nudge. With alpha = 0.025 and a 50% target, every game moves CurrentValue by exactly 0.025 × 0.5 = 0.0125, up on a loss-correction, down on a win-correction. Small on purpose.

  • -= and the clamp. We step against the error (negative feedback), then pin the result inside [minValue, maxValue] so a bad streak can't drive the knob off a cliff


The source code for game balancer for Unity

Download link

Then simply use it in game like this:

using UnityEngine;

// Robbins-Monro difficulty balancer for the fishing game example.
// Before game starts, call: balancer.ConfigureGame(game);
// After game ends, call: balancer.RecordResult(gameOutcome/*true or false*/);
public class FishingGameBalancer : RobbinsMonroBalancer
{
    protected override string StateKey => "fishing";

    // Higher fishSpeed -> harder to track -> player wins less.
    protected override float DirectionSign => -1f;

    // Call before the game starts to inject the current difficulty.
    public void ConfigureGame(FishingGameState state)
    {
        EnsureLoaded();
        state.fishSpeed = CurrentValue;
    }
}