We relieve the player of irritation: the correct use of random numbers

Original author: Kyle Speaker
  • Transfer
image

If you happen to chat with an RPG fan, you will soon hear complaints about randomized results and loot, as well as how annoying they can be. Many gamers express their annoyance, and while some developers come up with innovative solutions, most still force us to go through enrageous perseverance tests.

But there is a better way. Using random numbers and generating them in another way, we can create an exciting gameplay that creates an “ideal” level of difficulty without poking players. But before we get into that, let's look at the basics of random number generators (or RNGs).

Random number generator and its application


Random numbers are found throughout us and are used to add variation to the software. In general, RNGs are usually used to model chaotic events, demonstrate inconstancy, or are an artificial limiter.

Most likely, you interact daily with random numbers or the results of their actions. They are used in scientific experiments, video games, animations, art, and in almost every computer application. For example, RNG is most likely implemented in simple animations on your phone.

Now that we’ve talked a bit about RNG, let's take a look at their implementation and learn how to use them to improve games.

Standard random number generator


In almost every programming language, among other functions, there is a standard RNG. His job is to return a random value in the range of two numbers. In different systems, standard RNGs can be implemented in dozens of different ways, but in the general case they have the same effect: they return a random number from the interval, each value in which can be selected with the same probability.

In games, generators are often used to simulate dice rolls. Ideally, they should be used only in situations where each of the results should occur an equal number of times.

If you want to experiment with rarity or different degrees of randomization, then the following method is more suitable for you.

Weighted random numbers and rarity slots


This type of RNG is the basis for any RPG with a rarity item system. In particular, it is used when you need a randomized result, but some values ​​should drop out at a lower frequency than others. When studying probabilities, an example is often a bag of balls. With a weighted RNG, a bag can have three blue balls and one red. Since we need only one ball, we get either red or blue, but with a higher probability it will be blue.

Why can weighted randomization be important? Let's take the in-game SimCity events as an example. If each event were selected in unweighted ways, then the probability of each event occurring would be statistically the same. That is, with the same probability you would be offered to open a new casino or an earthquake would occur. By adding weights, we can make these events happen in proportionate probability, providing good gameplay.

Types and Applications


Grouping identical items


In many books on computer science, this method is called the "bag." The name speaks for itself - classes or objects are used to create a visual representation of the bag in the literal sense.

In fact, it works like this: there is a container in which you can place objects, a function for placing an object in a "bag" and a function for randomly selecting an item from a "bag". Returning to our example with balls, we can say that our bag will contain blue, blue, blue and red balls.

Using this randomization method, we can approximately set the frequency of occurrence of the results in order to average the gameplay for each player. If we simplify the results to the scale from “Very Poor” to “Very Good”, then we will get a much more pleasant system compared to when a player can get an unnecessary sequence of undesirable results (for example, “Very Bad” results 20 times in a row).

However, it is still possible to statistically obtain a series of poor results, just the likelihood of this has decreased. We’ll look at a way that goes a little further to reduce the amount of unwanted results.

Here's a quick example of what bag class pseudo-code might look like:

Class Bag {
    //Создаём массив для всех элементов, находящихся в мешке
	Array itemsInBag;
	//Заполняем мешок предметами при его создании
	Constructor (Array startingItems) {
		itemsInBag = startingItems;
	}
	//Добавляем предмет в мешок, передавая объект (а затем просто записываем его в массив)
	Function addItem (Object item) {
		itemsInBag.push(item);
	}
	//Для возврата случайного предмета используем встроенную функцию random, возвращая предмет из массива
	Function getRandomItem () {
		return(itemsInBag[random(0,itemsInBag.length-1)]);
    }
}

Implementation of rarity slots


Rarity slots are a standardization method for specifying the frequency of the object falling out (usually used to simplify the process of creating a game design and player rewards).

Instead of setting the frequency of each individual item in the game, we create a rarity corresponding to it - for example, the rarity “Normal” can represent the probability of a certain result of 20 to X, and the rarity level “Rare” can represent a probability of 1 to X.

This method does not greatly change the function of the bag itself, but it can be used to increase efficiency on the side of the developer, allowing you to quickly assign a statistical probability to an exponentially large number of items.

In addition, splitting rarity into slots is useful for changing a player’s perceptions. It allows you to quickly and without having to bother with numbers to understand how often an event should occur so that the player does not lose interest.

Here is a simple example of how we can add rarity slots to our bag:

Class Bag {
    //Создаём массив для всех элементов, находящихся в мешке
	Array itemsInBag;
	//Добавляем предмет в мешок, передавая объект
	Function addItem (Object item) {
		//Отслеживаем циклы относительно разделения редкости
		Int timesToAdd;
		//Проверяем переменную редкости предмета
        //(но сначала создаём эту переменную в классе предмета,
        //предпочтительно перечислимого типа)
		Switch(item.rarity) {
			Case 'common':
				timesToAdd = 5;
			Case 'uncommon':
				timesToAdd = 3;
			Case 'rare':
				timesToAdd = 1;
		}
        //Добавляем экземпляры предмета в мешок с учётом его редкости
		While (timesToAdd >0)
		{
            itemsInBag.push(item);
            timesToAdd--;
        }
	}
}

Variable Frequency Random Numbers


We talked about some of the most common ways to deal with accidents in games, so let's move on to more complex ones. The concept of using variable frequencies begins similarly to the bag from the above examples: we have a given number of results, and we know how often we want them to occur. The difference in implementation is that we want to change the probability of the results when they occur.

Why do we need this? Take, for example, pick-up games. If we have ten possible results for the obtained item, when nine are “ordinary” and one is “rare,” then the probabilities are very simple: a player will receive a normal item 90% of the time, and a rare one 10% of the time. The problem arises when we consider several pulls from the bag.

Let's look at our chances of getting a series of common results:

  • At the first pull, the probability of getting a regular item is 90%.
  • With two pulls, the probability of getting both ordinary items is 81%.
  • With 10 pulls, there is still a 35% chance of all common items.
  • With 20 stretches, there is still a 12% chance.

That is, although the initial 9: 1 ratio seemed ideal to us, in fact, it corresponds only to the average results, that is, 1 out of 10 players will spend twice as much as desired to get a rare item. Moreover, 4% of players will spend three times as much time getting a rare item, and 1.5% of losers four times as much.

How variable frequencies solve this problem


The solution is to implement a randomness interval in our objects. To do this, we set the maximum and minimum rarity of each object (or rarity slots, if you want to connect this method with the previous example). For example, let's give our ordinary item a minimum rarity of 1 and a maximum of 9. A rare item will have a minimum and maximum of 1.

Now, in the scenario above, we will have ten items, nine of which are rare and one is rare. At the first pull, there is a 90% chance of getting a normal item. At variable frequencies, after stretching an ordinary item, we reduce its rarity by 1.

In this case, in the next stretch, we will have a total of nine objects, eight of which are ordinary, which gives a probability of 89% stretching of the usual. After each result with a normal item, the randomness of this item drops, which increases the likelihood of drawing a rare one until we are left with two items in the bag, one ordinary and one rare.

Thus, instead of a 35% chance of pulling 10 ordinary items in a row, we’ll only have a 5% chance. The probability of boundary results, such as pulling 20 ordinary objects in a row, decreases to 0.5%, and then it becomes even less. This creates permanent results for the players, and protects us from boundary cases in which the player constantly draws a bad result.

Creating a variable frequency class


The simplest variable frequency implementation is to retrieve an item from a bag, rather than just returning it:

Class Bag {
    //Создаём массив для всех элементов, находящихся в мешке
	Array itemsInBag;
	//Заполняем мешок предметами при его создании
	Constructor (Array startingItems) {
		itemsInBag = startingItems;
	}
	//Добавляем предмет в мешок, передавая объект (а затем просто записываем его в массив)
	Function addItem (Object item) {
		itemsInBag.push(item);
	}
	Function getRandomItem () {
        //pick a random item from the bag
		Var currentItem = itemsInBag[random(0,itemsInBag.length-1)];
        //Снижаем количество экземпляров этого предмета, если он выше минимума
		If (instancesOf (currentItem, itemsInBag) > currentItem.minimumRarity) {
			itemsInBag.remove(currentItem);
		}
		return(currentItem);
}
}

Although some problems appear in such a simple version (for example, the bag gradually changes to a normal distribution state), it displays minor changes that can stabilize the results of randomization.

Idea development


We described the basic idea of ​​variable frequencies, but in our own implementations we need to consider quite a few more aspects:

  • Removing items from the bag allows you to achieve consistent results, but over time brings us back to the problems of standard randomization. How can we change functions in such a way that both the increase and decrease of objects allow this to be avoided?
  • What happens when we deal with thousands or millions of items? In this case, the solution may be to use a bag filled with bags. For example, you can create a bag for each rarity (all ordinary items in one bag, rare in another) and place each of them in slots inside a large bag. This will provide ample opportunities for manipulation.

Less boring random numbers


Many games still use standard random number generation to create complexity. In this case, a system is created in which half of the players deviate in one or the other direction from the expected results. If you ignore this, there will be the possibility of boundary cases with too many repeated unsuccessful results.

By limiting the scatter intervals, we can provide a more holistic gameplay and allow more players to enjoy it.

To summarize


Random number generation is one of the pillars of good game design. To improve the gameplay, carefully check your statistics and implement the most suitable types of generation.

Also popular now: