Introduction
Recently, a question arose in the Unity forums about how to randomly select an item from a pool of one hundred, consisting of 50 red and 50 green items. The goal is to pick a random item each time, but with an important caveat: we need to prevent extreme cases, such as having more than five consecutive items of the same color. This type of problem can appear in various forms across different games.
For example, in a card game where players open packs, there may be a set probability of finding a rare card. However, to protect against bad luck, we might want to guarantee that if a player opens a certain number of packs without finding a rare card, the next pack will definitely contain one.
The Problem
At first glance, the solution seems trivial: as soon as a color streak reaches your limit, force-select the opposite color, then resume normal random draws. But this approach mirrors a well-known issue in games that bucket players or characters into discrete groups based on a numeric attribute.
Consider a video game where characters have an intelligence score between 50 and 150. If we divide these into three groups: Dumb, Average, and Smart, based on score ranges, we risk making the game less realistic and more predictable. For instance, if “Dumb” covers 50–90, “Average” 91–120, and “Smart” 121–150, then a character with a score of 91 and another with 120 would both be “Average” and behave identically, while a character with a score of 121 would suddenly behave very differently as “Smart.” This creates an unrealistic and predictable system, as a one-point difference can drastically change behavior.
A solution to this comes from the field of AI, specifically fuzzy logic. Without delving too deeply (as it’s not the main topic here), the idea is to introduce percentage-based chances at the boundaries of groups. For example, for every 2 points of intelligence above 110, there might be a 10% chance of exhibiting “Smart” behavior. At 112 intelligence, a character would act “Smart” 10% of the time and “Average” 90% of the time. At 130, the split is 50-50, and at 136, it’s 80% “Smart” and 20% “Average.” This approach yields more realistic and less predictable behaviors, while still reflecting the underlying intelligence score.
The same principle can be applied to our color selection problem. Instead of switching to a 100% chance of the other color after a streak, we can gradually increase the probability of breaking the streak. For example, after two consecutive REDs, rather than keeping the GREEN chance at 50% (or whatever the true probability is based on the remaining items), we could add a 20% bonus to GREEN, making it a 70% chance. If RED appears again, we increase GREEN’s chance by another 10%, and so on, until eventually reaching 100%. This method allows for more granular control, making long streaks more rare the longer they are, but not impossible, up to a defined cap.
Description Of The Solution
Before diving into code, it’s best to outline an algorithm for this problem. Once the algorithm is clear, it can be implemented in any language.
A naive approach, would be to have a 50% chance for each item, as we have the same number of items and add the modifier to that chance, but there is a problem with that: it doesn’t account for the changing number of remaining items of each color. Nothing guarantees that at every point in the game, there will be the same number of GREEN and RED items. In fact, there probably won’t be, as we may have picked 15 RED and 6 GREEN as an example at one point.
To find the unmodified chance of picking a color at any point, we will create two collections, one that has the RED and another that has the GREEN, then the chance to pick a RED item will be the amount of RED items remaining divided the total items remaining, and similarly for GREEN. In the above example we would have (50-15)=35 RED items and (50-6)=44 GREEN items remaining. So our unmodified chances should be 35/79 = 44.3% chance for RED and 44/79 = 55.7% for GREEN
We also need two variables: one to track which color currently has a streak, and another for the length of that streak.
Next, we define our modifiers based on the number of consecutive appearances. However, there’s a complication: Let’s suppose that we need our modifiers to be 0%, 0%, 20%, 30% and 50%. These modifiers, will be based on a 50-50 split, so the chances will become after 1 or 2 appearances 50%, then 70%, 80% and finally 100% after 5 consecutive appearances. But because our chances are not 50-50 as described previously, the modifiers won’t have the same effect. For example if at a point the unmodified chance is 25% RED and 75% GREEN, and we have a streak of GREEN, then depending on the length of that streak, the chances become: 25%, 25%, 45%, 55% and finally 75%. Obviously this won’t do, as this means that our streak can go beyond 5, because we now have a max 75% modified chance for RED.
A simple fix might be to set the final modifier to 100%, but this distorts the distribution. For instance, with a 70% base chance for RED, the modified chances could become 70%, 70%, 90%, 100%, and 170%. With a 20% base chance, they become 20%, 20%, 40%, 50%, and 120%. This inconsistency affects the maximum possible streak length and the impact of modifiers.
To maintain consistent distribution, we scale the modifiers by the current chance, dividing by 50 (the base chance the modifiers are designed for). For example, with a 25% chance for RED and a 75% chance for GREEN during a GREEN streak, we multiply the modifiers by 0.75/0.5 = 1.5. This adjusts the modifiers for RED to 0%, 0%, 30%, 45%, and 75%, resulting in final modified chances of 25%, 25%, 55%, 70%, and 100%.
This process should occur in a loop, recalculating modifiers each time based on the current unmodified chances. Each iteration, we calculate the chances for RED and GREEN, randomly select an item from the appropriate collection, and add it to the result list. The loop should also check the collections, and if one is empty, the remaining items of the other should be shuffled (using a Fisher-Yates shuffle) and added to the result collection.
This solution ensures that we can limit the maximum number of consecutive appearances for any color, adjust probabilities to reduce the likelihood of long streaks, maintain consistent distribution of the modified chances regardless of remaining items, and facilitate debugging by allowing pre-calculation and inspection of results. The only scenario where a streak might exceed the desired cap is at the end, when only one color remains.
The code
Below is a simple C# program that demonstrates the basic implementation of the above algorithm.
This example uses a single class, with collections as lists of strings. An enum
is used for RED and GREEN, and the GetRandomString
method both returns a value and modifies a collection (not best practice for production code, but suitable for illustration). You should adapt this code to fit your specific use case and item types.
public class ProbabilitiesExample
{
private enum Color
{
Red,
Green
}
private readonly List<string> _red;
private readonly List<string> _green;
private readonly List<string> _result = [];
private Color _streakColor;
private int _streakCount;
private readonly Random _rand = new();
private readonly float[] _chanceModifiersForOppositeColor = [0f, 0f, 0.2f, 0.3f, 0.5f]; // based on a 50% chance
public ProbabilitiesExample(List<string> red, List<string> green)
{
_red = red;
_green = green;
}
public List<string> GetResult()
{
var aRandomNumber = _rand.NextDouble();
_streakCount = 1;
var chanceForRed = 1 - CalculateChance(Color.Green);
_streakColor = aRandomNumber <= chanceForRed ? Color.Red : Color.Green;
_result.Add(GetRandomString(_streakColor));
var iterations = _red.Count + _green.Count;
for (var i = 0; i < iterations; i++)
{
aRandomNumber = _rand.NextDouble();
var oppositeColor = _streakColor == Color.Red ? Color.Green : Color.Red;
var currentColorChance = 1 - CalculateChance(oppositeColor);
if (aRandomNumber <= currentColorChance)
{
_streakCount++;
}
else
{
_streakColor = oppositeColor;
_streakCount = 1;
}
_result.Add(GetRandomString(_streakColor));
if (_red.Count == 0)
{
_green.Shuffle();
_result.AddRange(_green);
_green.Clear();
break;
}
if (_green.Count == 0)
{
_red.Shuffle();
_result.AddRange(_red);
_red.Clear();
break;
}
}
return _result;
}
private float CalculateChance(Color oppositeColor)
{
float sum = _red.Count + _green.Count;
var unmodifiedChance = oppositeColor == Color.Red ? _red.Count / sum : _green.Count / sum;
var modifier = _chanceModifiersForOppositeColor[_streakCount-1];
var modifierMultiplier = (1 - unmodifiedChance) / 0.5f;
var finalChance = unmodifiedChance + modifier * modifierMultiplier;
return finalChance;
}
private string GetRandomString(Color color)
{
string result;
if (color == Color.Red)
{
result = _red[_rand.Next(_red.Count)];
_red.Remove(result);
}
else
{
result = _green[_rand.Next(_green.Count)];
_green.Remove(result);
}
return result;
}
}
This class can be used in the following way:
List<string> red = new();
List<string> green = new();
for (var i = 1; i <= 50; i++)
{
red.Add($"Red{i}");
green.Add($"Green{i}");
}
ProbabilitiesExample example = new(red, green);
var result = example.GetResult();
for (var index = 0; index < result.Count; index++)
{
if(index % 10 == 0)
Console.WriteLine();
var item = result[index];
Console.Write($"{item} ");
}
The Shuffle()
method in lists, is an extension method that implements the Fisher-Yates shuffle. An example implementation can be:
public static class ListExtensions
{
public static void Shuffle<T>(this IList<T> list)
{
Random random = new();
for(var i= list.Count - 1; i > 1; i--)
{
var rnd = random.Next(i + 1);
(list[rnd], list[i]) = (list[i], list[rnd]);
}
}
}
Problem Variations
This approach isn’t limited to just two sets or to fixed modifiers.
If you need more than two categories, you can still collapse them into “current‐streak” vs. “all others.” First, treat the streaking set as one group and combine every other set into a second group. When your algorithm selects from that second group, pick which original set it came from by computing each set’s unmodified chance (items in set divided by the total items in all “other” sets) and then draw randomly within that set.
Additionally, the modifiers do not have to be constant. For example, imagine a game with two armies, RED and GREEN units, and a tower that attacks them. In this scenario, the modifiers could be recalculated dynamically each time, perhaps based on the average distance of the units from the tower in the current streak. This approach can be used to subtly give the player who is currently losing a slight advantage, keeping the game competitive and enjoyable. Since these probabilities are not hard-coded and are recalculated based on the game state, players are unlikely to notice any pattern. The AI’s decisions are based on probability and small chance variations rather than predefined behaviors, making them non-deterministic and difficult for players to predict or exploit.
Conclusion
This post is based on a solution I proposed for a problem discussed in the Unity forums, and I thought it was interesting enough to share as a blog post. I believe that games which use probabilities influenced by the current game state—rather than purely random probabilities—offer a more engaging and enjoyable experience. When combined with procedural generation and emergent gameplay, these mechanics can significantly enhance replayability and increase the number of hours players are likely to spend enjoying a game.
Thank you for reading and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.