In this article I'll go over some design considerations when using random numbers in games. I'll go through some examples of common random number systems. I'll end with an example of a design challenge I faced, and the algorithm I used to solve it.
Fair warning – I will not be using the correct mathematical terminology for anything in this article, except perhaps by accident, so don't use it as a resource for your master's thesis.
There are endless uses for randomness in game design, but each use requires random numbers that possess specific characteristics. In most cases you'll need the numbers to fall within a certain range, to be of a certain level of granularity, and to occur in a desired distribution. For the purposes of this article, here's what I mean by these terms:
Range means the minimum and maximum values.
Granularity means the size of the gap between possible values.
Distribution means how often each value occurs relative to other possible values.
There are 2 very common solutions to generate well-designed sets of random numbers, which I'll just call Dice and Cards here for simplicity. I'll go over these approaches and highlight the important distinctions.
Dice rolling is a simple, versatile algorithm for generating random numbers. In its simplest form you just generate a random number within the desired range and then round or mod it down to the desired granularity. Often you'll apply an offset or multiplier to adjust the range and granularity as well.
A single dice roll usually results in an equally weighted distribution. For example, a single roll of a six-sided dice (1d6) results in the following distribution:
If multiple dice are rolled and added together, it creates a more interesting distribution of values. For example, rolling 3 six-sided dice and adding the values (3d6) results in the following distribution:
This gives us a nice bell curve distribution of values between 3 and 18.
With dice there are some standard tricks to apply. So if we wanted to change the distribution of values between 3 and 18 to favor higher numbers, we could instead roll 4 six sided dice and drop the lowest value rolled (4d6-L). Then we would get the following distribution:
So with the right adjustments, dice rolling gives you a lot of power and control over the results of random numbers. However, one fundamental aspect of dice rolling is that each roll is completely independent of each other roll. This means that no matter what your ideal probability curve looks like, there's nothing stopping you from rolling 18 3's in a row. Depending on what your random number is actually controlling, this might lead to some very undesirable behavior, even if it does only happen very rarely.
An alternative approach to getting random values is to simulate something like a deck of cards. This can be a little bit more code intensive, but offers us some features that dice alone cannot. In this approach we create a list of desired values in the distribution we want, and then shuffle the values about randomly. Each time we need a value we pull the top one off of the stack, leaving a fixed set of the remaining values. After some amount of time we can reshuffle the deck.
The most important distinction between the Cards approach from Dice is that with Cards your subsequent values have a different probability of occurring based on what values you've already pulled.
Say we want a very simple set of cards analogous to a six-sided die. So we make 6 cards, 1 each with the values 1, 2, 3, 4, 5, and 6. The cards are shuffled and we pull one from the top. Say it's a 2. Now there are only 5 cards in the deck, and we'll never get another 2 until we decide to reshuffle the deck. This prevents the potential issue of getting a very unlikely value several times in a short duration. If you don't reshuffle until the entire deck is consumed, then you'll always be getting the exact distribution you want.
There are some other design considerations with cards, however. In particular, choosing the number of cards to use can be tricky. If there are only a few possibly result values, then the card algorithm can be obvious and lead to card-counting behavior. You make a larger deck of your small set of values by duplicating each one some number of times to help counter this. But a large deck of only a few different values will then start to reintroduce the problem of drawing more unlikely cards in a row. It can be tricky to balance.
The Wave algorithm is something I came up with to meet a design challenge in my farming game Plotz. In Plotz there are 5 different weather conditions: stormy, rainy, cloudy, sunny, and hot. Each day I need to determine what the weather should be. I wanted the weather to be unpredictable, but to still move in somewhat realistic patterns.
A Dice algorithm was out of the question because it would cause the weather to wildly jump from one condition to another with no rhyme or reason.
I experimented for awhile with a Cards approach, but ran into some of the balance issues I described about it. With just a few distinct values to pull from, everything I tried turned into being very predictable or very chaotic.
Essentially the problem wasn't the generation of a single random number, it was more about the pattern that successive numbers formed that was important. The end result was a relatively simple algorithm that randomly oscillates up and down by random amounts. It creates a flowing but non-predictable set of results.
First I created my distribution. Cloudy weather was a pretty neutral condition so it was the most common. Sunny and rainy days are less common but still normal events. Stormy and hot days are more extreme and therefore more rare events. I assigned each event an appropriately sized chunk of a number line. For simplicity let's say it looks like:
[0-1) = Stormy
[1-3) = Rainy
[3-6) = Cloudy
[6-8) = Sunny
[8-9] = Hot
To start the wave I choose the middle value of 4.5, and then I randomly pick a direction – positive or negative. In this case a negative direction will lead to wetter weather and a positive direction will lead to more dry weather. Each day I do a Dice roll to get a value in a range where the weather might stay the same or shift by one type at most. For this example that might be something between 0.25 and 1.25. This will slowly shift the weather over time, but you'll never know exactly how many days of each type of weather you'll be getting this time around.
The final component of the algorithm is to know when to swing the direction of the wave back the other way. To do this I chose random thresholds, one in the top half and one in the bottom half. Each time the wave value crosses the respective threshold, I change the direction of the wave. Then a new random threshold for that half is generated. In this way, the top and bottom of each wave is also unpredictable. So, for example, just because you see one sunny day, it doesn't mean you're going to get a lot of sun or any hot weather. The very next day could dip back down into clouds if the threshold was particularly low that time around.
I've been really happy with the wave algorithm and I think it could have lots of other uses. One common use of random numbers is determining whether an attack hits, misses, or goes critical in a combat system. This is another case where there are only a small number of actual results. While Dice are often used to solve this, I think the wave algorithm might be interesting to try. It might seem more realistic for you to land a couple of critical blows in a row, like your opponent was really stunned by the first one, leaving them open for further critical hits before they recover some of their defenses.
That's All For Now
There are many more ways of combining and controlling random numbers, but I hope this is a helpful primer for thinking about some of the design challenges you might face.
If you want to see the wave algorithm in action, check out the Plotz demo at http://www.nuclearcompost.com/plotz
If you have thoughts or questions, feel free to contact us:
Email: [email protected]