Game Design Technique: The Ranjit Range

loop

Most of the blog posts we put up here are high-concept entries about game design or other abstract ideas. But today we wanted to write about something more concrete and practical – a method we’re using to adjust and balance variables in a game under development.

On a team project, it’s always good idea to externalize tweakable variables to a file where everyone can access them and try out different settings and combinations without having to alter the game’s code. Externalizing variables like starting player stats, enemy strength and movement speed, or amount of rewards for various actions let you try out various possibilities as you search for the sweet spot of resistance, challenge and reward to give your game system moments of meaningful choice.

I’ve been doing this kind of thing since I started making videogames 20 years ago. At the Brooklyn Game Ensemble, we use a particular technique for describing a certain kind of random variable. In my own practice as a game designer, this technique evolved over the lifetime of my now-defunct company Gamelab, although it’s been used in one form or another by other game developers as well. We now call this type of random variable a “Ranjit Range” after Ranjit Bhatnagar, Gamelab’s former lead programmer and technologist. Ranjit Ranges are extremely useful for structuring how you can tweak and list variables in any kind of digital game.

Here’s an example of how they work: In the Gamelab game LOOP from 2000, the player uses the mouse to draw lines around groups of butterflies, attempting to encircle butterflies of the same color. We wanted the levels to grow in complexity as the player proceeded, in a way that felt consistent, but also organic – a little different each time. For example, in each level we wanted to gradually increase the number and variety of butterflies. When the variables were externalized, they looked something like this:

level=10
numButterflies=(42, 44, 44, 46, 46, 46, 48, 48, 50)
butterflyColor=[blue, blue, blue, red, red, yellow, yellow, pink]

As you might expect, this means that on the tenth level of LOOP, the player sees between 42 and 50 butterflies appear over the course of the level. The program picks one of the values of the numbers in parentheses – one of the numbers from 42 to 50. But since it picks one of the numbers in there, and some of the numbers appear multiple times, there will be different chances for each number. Just 1 out of 9 chances for a 42 or 50, 2 out of 9 for a 44 or 48, and a full 1/3 chance (3 out of 9) for a 46. A kind of miniature bell curve.

The program does the same thing for color. The game assigns a color to each butterfly by choosing a random entry from the butterflyColor list: first it looks to see how many entries are in the list (eight, in this case), then generates a random number between 0 and 7 to decide which color to use. If the number is 0, 1, or 2, then that particular butterfly will be blue; if it’s 3 or 4, the butterfly will be red, and so forth. It’s as if we were coloring the sides on an 8-sided die and then rolling it several times. The result is a level which has a slightly different composition of butterflies every time, but where on average, blue butterflies will be about three times as common as pink ones, with red and yellow ones in between.

What’s so special about this? It’s just a simple, intuitive, and visually clean way of defining these variables. To see what we mean, another way to describe this breakdown would be to specify the percentage chance for any given butterfly to be each of these colors, like this:

blue - 37.5%
red - 25%
yellow - 25%
pink - 12.5%

At first blush, percentages might seem like the most intuitive way to set up this kind of randomness in the game’s code — after all, it’s pretty close to how we talk about random events in the rest of the world. Lists of percentages are familiar for many players of tabletop role-playing games, which have a long tradition of using “random tables” to determine the effects of a die roll. Rules from such a game might look something like this:

Roll the dice to see what happens when you drink a Potion of Mystery.
1-75: Nothing happens.
76-80: Your skin turns blue for eight hours.
81-90: You're cured of all illnesses and wounds.
91-98: Your internal organs rupture, causing you to rapidly bleed to death.
99-100: The next thing you say out loud instantly becomes true.

This kind of setup makes sense when the players of your game are rolling physical dice with fixed numbers on them, and can then look up the number they rolled on a table that’s printed in a book. In the process of digital game development, especially while trying to iterate and balance the game, we need these numbers to be flexible and easily adjusted. If we’re prototyping and changing the structure of the game, we’d also like to have a system that programmers can set up quickly and easily, regardless of what language they’re coding in. A percentile table turns out to be a problematic tool, mostly because of what happens when you want to adjust the variables.

Imagine that as the designer of level 10, you’ve watched someone play your level and have decided that pink butterflies are just too rare — sometimes it takes forever for a group to appear! You decide to quickly see what the level would be like if pink butterflies were as common as red or yellow ones.

With a Ranjit Range, you simply add another pink butterfly to the list in the variable:

butterflyColor=[blue, blue, blue, red, red, yellow, yellow, pink, pink]

If you were adjusting a table of percentages, you’d have to recalculate and readjust all of the percentages, instead of just one. First you’d have to do the math — there are nine entries now, so each one has a 11.11% chance, and your percentage table ends up being:

blue - 33%
red - 23%
yellow - 22%
pink - 22%

Since all the numbers are being adjusted, and there’s math involved, there’s more risk of making a mistake and possibly causing an error in the game — especially if your game designers aren’t the most careful, meticulous arithmeticians. There’s also the matter of the code, which for the above percentages would always be generating a number between 0 and 99 — and note that we had to change one of the colors to 23% so that the total adds up neatly to 100! Since we’re not dealing with physical dice and human players, it’s almost as easy to tell the game to use the number of entries in our Ranjit Range, rather than requiring the values to add up to 100 every time.

So how exactly does it work?Here’s a snippet of C# code from our current prototype (which we’re now calling BORGES) that shows how straightforward the process is. The first part of this code even lets us detect whether a particular variable is set up as a Ranjit Range or not, depending on whether it has commas in it.

public static int GetInt(string intString) {
if (intString.Contains(",")) {

//Ranjit Range

    return GetIntRanjitRange(intString);

} else {

//Single value

    return int.Parse(intString);

}
}

public static int GetIntRanjitRange(string intString)

{

    string [] splitRange = intString.Split(',');

    intindex=Random.Range(0,splitRange.Length);
    return in.Parse(splitRange[index]);
}

 

Ranjit Ranges can get a little cumbersome to use if you end up wanting possibilities that are very close in their likelihood but not exact, or something like an overwhelming chance of one color, and a very rare chance of another. In a Ranjit Range, trying to create an exact “45% chance of rain” in a game with weather ends up requiring a list that’s twenty entries long, like this:

weather=[rain,rain,rain,rain,rain,rain,rain,rain,rain,
sunny,sunny,sunny,sunny,sunny,sunny,sunny,sunny,
sunny,sunny,sunny]

A 5% chance of treasure appearing in a dumpster would look like this:

dumpsterContents=[garbage,garbage,garbage,garbage,
garbage,garbage,garbage,garbage,garbage,garbage,
garbage,garbage,garbage,garbage,garbage,garbage,
garbage,garbage,garbage,gold]

This makes for a nice visual approximation of the possibilities, but we also developed a shortcut for these kinds of situations. Instead of listing out each item, we just indicate how many there are:

dumpsterContents=[garbage:19,gold:1]

Instead of looking at the length of the list (which in this case is only two) the code to handle this kind of variable would add the numbers 19 and 1 together in order to determine what range of random numbers to generate. With this system, we could even specify percentages like “95” and “5” if that’s what felt intuitive — but the great thing is that in a longer list of random items, we still wouldn’t have to recalculate the rest of the numbers. What if we wanted the player to be able to find twenty different items in that dumpster?

dumpsterContents=[moldy_cheese:5,banana_peel:5,rags:5,
unidentified_scum:5,rotting_chair:5,
damp_magazine:5, false_teeth:5,jar_of_urine:5,
smelly_sock:5,newspaper:5, one_dollar:5,
five_dollars:5,your_car_keys:5,broken_vcr:5,
leaves:5,rancid_butter:5,old_lightbulb:5,
antique_lamp:5,apocalyptic_sign:5,gold:5]

If we were trying to make these numbers add up to a total like 100, it would be a real pain to adjust each one — but with a Ranjit Range, if we’d like the “rags” item to be twice as common as the others, all we have to do is change its associated number to 10; the code does the rest!

In the early prototyping stages of a game, this kind of nuanced balancing usually isn’t the main concern anyway. It’s often more useful to get a feel for the possibilities of the system by quickly making big changes that swing the system dramatically in one direction or another. You can do this by adjusting a relatively short Ranjit Range, then playing to see what kind of effect that change has on the game, and repeating the process.

That’s iteration! The basic Ranjit Range, which is quick to set up, ends up being a perfect tool for the job.

Leave a Reply

Your email address will not be published. Required fields are marked *