Click or drag to resize
Numeric Ranges

The most common use of random engines is of course the generation of random numbers distributed uniformly within some desired range. Make It Random provides functions for generating numbers of all primitive types, offering a few different ways in which the desired ranges can be specified.

Open and Closed Range Boundaries

When specifying a numeric range, it is important to note whether the lower and upper boundaries are included or excluded. For example, a roll of a standard six-sided die results in a number between 1 and 6, with both 1 and 6 included as possibilities, whereas selecting a random element from a zero-indexed array requires generating a number from 0 (inclusive) up to but not including the length of the array.

The design of Make It Random adopts the policy that it should always be very clear in code whether the lower and upper boundaries are included in the range. As such, all range generating functions include the abbreviations 'C' and 'O' to indicate that the range boundaries are closed or open, respectively.

A closed boundary is one that includes the boundary value, while an open boundary does not. Closed boundaries are often notated using square brackets, and open boundaries using round brackets (parentheses). Hence, a range designated 'CO' is one that includes the lower boundary but not the upper boundary, and could be notated as [lower, upper).

Closed/open ('CO') ranges are very common, and due to various implementation details will also typically be the most efficient to generate. This is especially true when the lower boundary is 0. Closed/closed ('CC') ranges frequently occur in informal speech (Guess a number from 1 to 10.), and so it can be convenient to specify that directly in code when desired. Open/open ('OO') ranges sometimes occur when a value is needed that is explicitly between two values (for example, a volume that is bigger than a breadbox but smaller than a house). Open/closed ('OC') ranges are appropriate when one wants a range that includes a specific number of options and starts at an implicit value of 1 instead of 0 (see below), such as when rolling a die with a particular number of sides.

Implicit Lower Boundaries

All range functions include an overload with only one boundary parameter. In this case, that parameter indicates the upper boundary, and the lower boundary is assumed to be 0. Note that for integer ranges with an open lower boundary, this means that the range essentially starts at an implicit value of 1. Floating point ranges with an implicit open lower boundary start at a value that is almost but slightly larger than 0, within the limitations of the binary floating point representation, and the particular methods of generation (see below).

Specifying the Range Type

If specifying the range boundaries explicitly, the type can be automatically deduced by the type of the boundaries supplied. In these cases, you may call random.RangeOO(...), random.RangeCO(...), random.RangeOC(...), or random.RangeCC(...) and simply supply the lower and upper boundaries as the type you desire. For example, random.RangeCC(1, 6) will return a 32-bit signed integer, whereas random.RangeCC(1UL, 6UL) will return a 64-bit unsigned integer and random.RangeCC(1f, 6f) will return a 32-bit floating point number.

For common ranges that need no parameters, the name of the type needs to be used instead of 'Range'. For floating point numbers, the standard range is from 0 to 1, so random.FloatCC() will return a float that is greater than or equal to 0 and less than or equal to 1, while random.DoubleCC() returns that same range as a double instead. For the range -1 to +1, prefix the word 'Signed' to the function, such as random.SignedFloatCC().

Floating Point Precision Near Zero

Due to the way floating point numbers are stored, they can have much higher absolute precision the closer they get to 0. In ordinary situations this is almost always preferable, but it may be undesirable when working with uniform random distributions.

By default, the design of Make It Random is such that random floating point numbers selected from a range do not exhibit this increased precision. Thus, the difference between the smallest possible number in a range and the next larger number is exactly the same as the difference between the largest possible number in that same range and the next smaller number. It doesn't matter where a number is within this range, the next numbers larger or smaller than it will always differ by exactly this amount.

If you explicitly want to have the increased precision near 0 that floating point numbers offer, you can prefix the function name with 'Precise'. Hence, random.PreciseSignedDoubleOO() will produce numbers between -1 and +1 with ultra-high absolute precision for numbers that are really close to 0.

Note Note

Note that this does not change the property of uniform distribution within the range. Values closer to 0 remain no more likely to occur than numbers further away from 0. It's just that the clustering that is inevitable for numbers not near 0 is less severe for numbers near 0.

For example, if calling random.FloatCO(), there are around 4 million possible values that are between 0.5 and 1.0, and only half as many, around 2 million, that are between the range 0.25 and 0.5. This makes sense, because the second range is half the size of the first. If random.PreciseFloatCO() is called instead, there are still 4 million possible values in the first range, but the second range also includes 4 million possible values, despite filling a range half the size. This discrepancy is resolved by the fact that each of these 4 million values in the second range have only half the chance of occuring as each of the 4 million in the first. Moving down to the next range from 0.125 to 0.25, once again there are 4 million values possible, but each one only has a quarter of the likelihood of occuring as those in the first range.

With random.FloatCO() in contrast, each value has exactly the same chance of occuring as any other regardless of which subrange it is in, and subranges contain a number of possible values that is proportional to the size of the subrange.

Perfect Distribution

Although perhaps a feature of mere academic curiosity for most games, it should be noted that Make It Random selects numbers from ranges with perfect uniform distribution, not just approximately uniform distribution, to the extent that the underlying random engine also produces complete unbiased bits.

Simple RNG implementations have frequently used module/remainder operations to generate numbers within ranges, and this method can suffer from quite a severe bias for certain range sizes. More cautious implementations rely on division to avoid the above type of bias, but this method still exhibits minor bias whereby some values are slightly more or less likely to occur than others. In addition, both of these methods rely on division operations, which aren't the fastest possible operations available.

Make It Random instead uses a trial-and-error technique to avoid both the bias and the division operation. This does however mean that the underlying random engine might need to produce a variable number of bits in order to get a single number within a specific range. For ranges whose size are just slightly less than or equal to the nearest power of 2, in combination with good branch prediction of modern CPUs, this still tends to be very fast. Ranges whose size is just barely over the nearest power of 2 on the other hand will on average require twice as many random bits from the random engine before they generate an acceptable number with the proper distribution.

Floating point ranges can suffer the same bias as integers when using division to turn the bits from a random engine into a random floating point number, so a similar trial-and-error technique is used when necessary for floating pointer numbers also. Fortunately, because the common floating point ranges have fixed properties, this knowledge can be exploited to reduce the frequency of failed attemps. Half-open ranges ('CO' and 'OC') in particular never fail, since they use integer ranges whose sizes are exact powers of 2. Open/open ranges use integer ranges whose sizes are just barely less than the nearest power of 2, and so they succeed on the first trial the vast number of times. Closed/closed ranges use integer ranges that are just barely greater than the nearest power of 2, typically the worst case possible, but special techniques are used even in this case to reduce the error rate to around one in a few thousand trials.

Regardless of the details or the exact effect on performance, the main takeaway is that uniform distribution means exactly that, with no bias at all that is not inherent in the bits generated by the underlying random engine or introduced during further arithmetic operations by the limitations of binary numeric representations.

Caution note Caution
While special care and attention has been put into the uniformity of distribution, this shall not be construed as a legal guarantee of perfect uniformity. Experilous shall not be held liable for any faults in the production of random values by Make It Random. If you're making a gambling game, for example, it's on you to ensure that there are no tiny biases that a user could find and exploit to your detriment or the detriment of other users.
See Also