# How to use Moving Average to avoid lucky streaks in RNG

I spent the last week reading about Luck Mitigation and what a dev can do to avoid extreme cases where the player's psychology will be messed up.

I came up to this question How do I avoid “too” lucky / unlucky streaks in random number generation?, plus some others outside GameDev site. To illustrate my question, let's have this simple game example.

We have a turned base fighting game, like BiteFight. Two characters have their attributes and the match outcome is based entirely to those attributes.

In specific:

Player 1: Str: 10 Def: 15 Agl: 20

Player 2: Str: 12 Def: 20 Agl: 15

The process is simple:

If rnd() * P1_Agl >= rnd() * P2_Agl:
P1 will attack
else:
P1 will attack


Then with the same way in case of Player 1 is attacking

If rnd() * P1_Str >= rnd()* P2_Def:
reduce life points from P2


Repeat the same unlimited times until one of players' life reach zero.

Solutions I found:

1. Instead of 1 rnd() call, use x-times rnd() and get the average. Something like ( rnd() + rnd() + rnd() ) / 3. This will move the results closer to 0.5 and make the whole process less "random". However, it is still possible enough to have streaks of lucky or unlucky events.

2. Keep track of the results and if you have x-times the same result then hard-code the other. For example, if on the above, P1 won the Agility test 3 times in the row, then the 4th time will always go to P2. What I didn't like on this are the predictability. Users might understand that behavior through time. But the most important is what would happen if P1 uses an item that changes his attributes on the 4th step? When you hard-code the outcome, the result would be based on different attributes.

3. I read many times the suggestion to use a Moving Average or a Cumulative Moving Average on the rnd() to guess if their is a streak of lucky results. For example, if Player 1 keeps getting rnd() above 0.9, it is a lucky streak. I wrote a script to calculate those two and here is an example for CMA and MA

Cumulative Moving Average

0 : Random number:  0.348  CMA:  0.348
1 : Random number:  0.74  CMA:  0.544
2 : Random number:  0.967  CMA:  0.685
3 : Random number:  0.578  CMA:  0.659
4 : Random number:  0.932  CMA:  0.713
5 : Random number:  0.134  CMA:  0.617
6 : Random number:  0.397  CMA:  0.585
7 : Random number:  0.537  CMA:  0.579
8 : Random number:  0.861  CMA:  0.611
9 : Random number:  0.893  CMA:  0.639


Moving Average

0 : Random number:  0.295 Moving Average:  0.098
1 : Random number:  0.866 Moving Average:  0.354
2 : Random number:  0.768 Moving Average:  0.492
3 : Random number:  0.888 Moving Average:  0.624
4 : Random number:  0.94 Moving Average:  0.729
5 : Random number:  0.325 Moving Average:  0.595
6 : Random number:  0.823 Moving Average:  0.671
7 : Random number:  0.208 Moving Average:  0.517
8 : Random number:  0.743 Moving Average:  0.592
9 : Random number:  0.038 Moving Average:  0.407


So, I end up to my 2 questions

1) Even if I read many times the suggestion of Moving Average, no-one mentioned how exactly you use it. So, my guess is that each time, you compare the rnd() with the moving average and based on that you take some actions. But what exactly you can do?

2) In the above example, you can see that we have two comparisons. One for Agility and one for Strength. Should one use two different Moving Averages to "identify" lucky streaks for each case, or it should be one for every case that RNG comes to the game?

• Randomness always involves luck. If you don't want lucky streaks, you don't want randomness. But one question which might help you: "How can I make a “random” generator that is biased by prior events?" – Philipp Jun 28 '18 at 11:40
• Thanks for the linked question. I read it during my research. But there isn't anything about Moving Average and how one can use it. – Tasos Jun 28 '18 at 11:48
• This seems like an XY problem, it sounds like the underlying problem is about breaking lucky streaks - personally, I'm not aware of solutions that rely on moving averages. On the other hand, if what you really want to know is "how does XYZ reference use moving average to mitigate luck", you'll need to link or list the reference in question. – Pikalek Jun 28 '18 at 13:01
• Shuffle a set of evenly distributed values? – Draco18s Jun 28 '18 at 17:22
• Have you considered that by breaking normally occurring streaks you are denying the player that "WOW did you just see that??" moments and replacing it with "ho hum more of the same this fight" mediocrity? If you're really against RNG working like RNG works then maybe you need to think outside the box and move away from random altogether instead of trying to duct tape the broken box you're in... – Patrick Hughes Oct 1 '18 at 18:05

Moving average just means you take an average of the last N rolls. To use it for breaking lucky streaks, you compare the moving average against the ideal random chance, and adjust your next roll to move the average towards the ideal.

For example, let's look at adjusting coin flips over a period of 4. Suppose we've had the following coin flips:

0 1 2 3 4 5 6
H T H T T T T


In the last 4 flips we had 4 Tails. This can happen 1-in-16 times but let's suppose we want to avoid this kind of streak. We force the next flip to be a Head.

0 1 2 3 4 5 6 7
H T H T T T T H


Now the last 4 flips has 1 Head and 3 Tails.

Avoiding streaks is especially useful for rare events like critical hits and rare loot drops. Consider if you have a rare item that drops only 1% of the time. What's the chance that someone misses that item after 300 tries? 0.99^300 or 5%, which is quite likely, similar to the chance of getting 4-tails in a row. But for the poor player grinding 300 times, that would seem very unfair.

Moving average also works for non-binary outcomes. Suppose you have a weapon that deals "12-17 damage per hit", but you want the moving average to be 14-15 over a period of 4 hits. Maybe in this game, enemies usually take 5 or more hits to kill and you want the number of hits to be predictable. Start with the following hits:

 0  1  2
12 14 13


What would the next roll need to be so that the moving average is 14-15? 4*14-(12+14+13) to 4*15-(12+14+13), or 17 to 21. Since we can't roll any higher than 17 without breaking our own game rules, we force the next roll to be 17.

 0  1  2  3
12 14 13 17


Now the average is 14. But what about the next roll? It would need to fall between 4*14-(14+13+17) to 4*15-(14+13+17), or 12 to 16.

 0  1  2  3  4
12 14 13 17 16


Similarly, the next roll needs to be between 12 (raised from 10) and 14, since we rolled a few high numbers last time.

 0  1  2  3  4  5
12 14 13 17 16 14


And so on and on.

Can't you just do something like: lets say Crit chance is 25% [round 1]. If there's a crit then he/she can't crit again for at least 4 rounds [till round 5 no crit] (because of 25%) & will crit 100% after 4 rounds [so in round 9 there will be a crit if there was no crit between round 5 to 8].

Or you can do it even simpler. I've thought for some projects if I have crit chance of 25% I will crit every 4 rounds/hits. I like that idea because I'm able to add nice combos. So the player will think: I will use my best damage skill in round 4 so that it will crit, also the enemy could know after some rounds what is the % crit and react to that with a evade skill or just a block skill, something like that.

I like your idea. I like Bite Fight but there is something missing (lost much potential). I would like to be one of your testers when your ready/finished.

Got curious so I ran some numbers by hand. If you restrain your randomness to [0, 1] as you had done then you could do soemthign along the lines of calculating your moving average and multiply your next roll by 1.5 - MovingAverage and it will move it back towards the center.

• What if my moving average drops a little below 0.5, say 0.45, and my next roll is > 0.9524, then I get a result greater than 1. (If you clamp to 1, then you're increasing the probability of 1.0 rolls vs 0.99) Is that a concern? – DMGregory Oct 1 '18 at 18:27
• That is a fair concern, but in this case I would not worry about it as the value is only being used in comparison to another value and not technically needing to be strictly in the [0, 1] on the output for such an operation. if that is a concern then another solution(that is much more mathematically rigourous) would be best – TurtleKwitty Oct 1 '18 at 18:30
• You can improve the symmetry by doing.... movingAverage <= 0.5 ? Lerp(1, random, 2 * movingAverage) : Lerp(random, 0, 2 * movingAverage - 1) then you're effectively bracketing to low rolls when the average is high, and high rolls when the average is low, without overflow or asymmetry between low & high averages. – DMGregory Oct 1 '18 at 18:42
• Thats a good way! I must admit that i just fiddled with a couple numbers over lunchbreak so it wasnt very in depth haha. Do oyu mind if i add your comment into my answer ? – TurtleKwitty Oct 1 '18 at 18:44
• You're welcome, even encouraged, to incorporate feedback that users offer you via comments. :) – DMGregory Oct 1 '18 at 18:55