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:

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's1 - 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 issign(dP(win)/dvalue), supplied by each game. It is-1when a bigger value makes the game harder (fish swim faster ⇒ fewer wins) and+1when 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. Withalpha = 0.025and a 50% target, every game movesCurrentValueby exactly0.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
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;
}
}