Proper Random Boolean function

So i got this code i’m using (cuz i miss using the Swift language tools so i’m adding my own via extensions lol):

public static class Bool {
    /// <summary>
    /// Returns true or false with equal probability.
    /// </summary>
    /// <returns>True or False</returns>
    public static bool random() {
        return UnityEngine.Random.value <= 0.5f;
    }
}

/// Usage:

if (Bool.random()) {
  // do thing
}

But the docstring for the Random.value read as follows:

float UnityEngine.Random.value { get; }
Returns a random float within [0.0..1.0] (range is inclusive) (Read Only).

Is Random.value < 0.5 or > 0.5 or <= 0.5 or… which set of operations / number comparisons should I do in order to get as close to 50/50 as possible.

If i’m reading the docstring correctly… it’ll go from 0.0 to 1.0, with all possible values in between. Meaning it’s not “1 to 100, 1-50 vs 51-100” but instead “0-100”… so that’s a slight imbalance if you compare the wrong direction, right???

I understand that missing an equals sign or using the wrong is marginally wrong and probably “close enough” but i’d rather not close enough in a game with lots of calls to this function.

Here’s an example of a previous discussion where multiple versions were used but nobody commented on that?

https://forum.unity.com/threads/random-randomboolean-function.83220/

I mean is this precision needed for a robotic AI-powered brain surgeon that will operate on the 20 most powerful persons on earth? < 0.5 or <=0.5 will return nearly the same probability, a so small difference that it might be smaller than Planck’s length…

Anyway, I’d use Random.Range(0f, 1f) this will exclude 1 then use < 0.5f. I think this is the closest you can get to a 50/50 chance.

1 Like

Don’t use floats if you don’t need floats.

Unity Random.Range has overload which works with integer ranges. That way you don’t have to speculate about any biases caused by < 0.5 vs <= 0.5 or other floating point issues. This was also suggested in the forum post you linked.

2 Likes

Exactly. a public static bool random() => Random.Range(0, 2) == 0; is more than enough.

1 Like

https://forum.unity.com/threads/random-randomboolean-function.83220/#post-1139598

According to this, Random.value performs better than that tho…

Maybe said another way… While I understand the < vs <= is probably very small net difference… Is there a mathematical way to know which one is closest to 50/50? I feel like this is less of a C# / Unity question tho :confused:

One thing you can do is run your function 100,000,000 times and count the number of true vs false. Do it a few times and see what sort of distribution you get.

1 Like

Sure… how many finite values between 0 and 1 exist in a float?

Well that boils down to how floats store values:

There is a sign, an exponent, and the ‘mantissa’/fraction. Note that the mantissa is 23 bits, but implicitly stores 24 bits because there is an implied 1 (all non-zero binary values have a leading 1 so it doesn’t actually store that 1 and just assumes it is there).

Note that the exponent value is actually the number stored there - 127. So this means that 0.5 is just:
01111110 00000000 00000000 00000000

That is no sign, 126 in the exponent (equals -1), and 0 in the mantissa. Which equates out to 1 * 2^-1 in scientific form, or 1/2.

All fractional values will be every possible value representable in the mantissa multiplied by the possible number of exponents that would result with any of those values shifted with all of its digits to the right of the radix. Honestly… I don’t know a simple proof for all of the exponents off the top of my head… but really it doesn’t matter. Because what we do know is this… the digits in the mantissa, including 0, is even. And an even * any number is always even. So we know whatever the number of values that can be represented is… it’s an even number.

BUT… this does not include 1! 1 is outside of this set.

So the number of values from 0->0.99999… that can be stored in a float is even. And if we include 1 then it’s odd (because an even + 1 = odd).

If we split a set in half whose length is even, then we result in 2 sets that are of the same length.

{0,1,2,3,4,5,6,7,8,9}
{0,1,2,3,4} {5,6,7,8,9}

But a set of length odd split in half results in 2 uneven sets.

{0,1,2,3,4,5,6,7,8,9,10}
{0,1,2,3,4} {5,6,7,8,9,10}

Now lets just assume that Random.value is a perfectly uniform RNG (we can’t actually guarantee that unless we knew the exact algorithm used. But lets just assume for sake of discussion). Meaning that it returns every possible value from 0 to 1.

If it excludes 1, then we result in an even count of values, meaning that we need to put the 0.5 into the top set. We can do that by saying >= 0.5 or < 0.5.

If it includes 1, then we result in an odd count of values, meaning that we can’t split the set evenly. Putting 0.5 in the upper set skews us towards the upper, and putting it in the lower set skews us to the lower.

This is part of why most RNG algorithms, say like System.Random, actually return in ranges of 0->1, 1 eclusive (it’s not the only reason, and actually is one of the lesser reasons as I’ll get into later):
https://learn.microsoft.com/en-us/dotnet/api/system.random.nextdouble?view=net-8.0

BUT, Unity’s Random.value actually returns 1 inclusive:
https://docs.unity3d.com/ScriptReference/Random-value.html

This means we can’t guarantee perfect uniformity.

But what is exclusive of its top range? Random.Range(int, int)!

So if you want perfect uniformity, you should tend towards Random.Range… even though Eric5h5 says it’s slightly slower in that post you linked. Because while its technically slower… it’s negligibly slower.

BUT… speaking of negligible.

There’s billions and billions and trillions of values between 0 and 1. Also, we’re assuming that Unity’s algorithm is perfectly uniform… which it likely isn’t! So 2 things could happen here.

  1. It’s not perfectly uniform, it results in an odd number of possibilities, and the inclusion of 1 as an upper makes it even. In which case it’s an even split… but we don’t know where that exclusionary midpoint is (since the non-uniform values are noise across the set) meaning this is all for not. Resulting in a noisy/random skew that is likely imperceptible in real-life scenarios.

  2. It is perfectly uniform and that skew towards the top is 1/gajillion… which… really? Does your game need to care about that 1/gajillion skew?

So really at the end… it doesn’t matter! (this is why really most RNG algos excluding 1 has less to do with this unformity, it actually just has to do with usefulness in arithmetic)

But… if you wanted to know the math. There you go.

2 Likes

Just take note that Unity/System.Random are not even real RNGs (Random Number Generators), but Pseudo-RNGs. They’re more like a fancy lookup table.

Anyway, I did what @kdgalla suggested just for fun, and these are the results:

So all methods are pretty close to 50/50, and you can use any one of them without any noticeable difference. For speed, it seems that Random.Value is indeed faster than Random.Range. What surprised me though is methods with < are slightly faster than their <= equivalent… maybe it could be just a fluke.

Note: Run in build/release mode (on a 7 years old i7 CPU).

Edit: was curious about the performance of .NET’s System.Random compared to UnityEngine.Random, turns out it’s a lot faster:

1 Like

Good point. It seems kind of silly to worry about the precise midpoint when maybe there’s no guarantee of an even distribution to begin with.

It’s like trying to cut a lumpy pancake in to precise halves.

3 Likes

PERFECT summation.

2 Likes

If a player were to peek into the code and see if (random>0.5f) and not >=0.5f then they’ll have an advantage and they’ll be able to make 0.0000000000001% more virtual frog coins than their opponents. :hushed:

3 Likes

In my tests performance difference between Random.value and Random.Range(0, 2) was at most 20%. In most cases that doesn’t matter. And when it does you still shouldn’t use floating point version, because you can use the integer version to generate 30 random bits, thus improving performance by almost 30x . Without knowing exact details of Unity implementation full 31-32 bits is a bit iffy.

Parity of floating point element count is meaningless because floating point numbers aren’t distributed evenly. Due to the way floating point exponent works, there are more than 100x times more unique floating point values in the range [0, 0.125] than there floating point values in the range [0.125, 1]. This results in unusual situation that to choose a random floating point number with even distribution from the range [0, 1] or any other range, you can’t evenly choose from every possible floating point value in the corresponding range. So in theory an evenly distribute floating point random should choose exactly 0.75 a lot more often than exactly 0.0625 . Probability of hitting range (0.75, 0.75+x) and (0.0625, 0.0625+x) should still be roughly the same.

The comment in Random.value about hitting both 0 and 1 roughly once every 10^7 samples is somewhat suspicious. I wouldn’t be surprised if Unity is slightly cheating, by skipping some of numbers in the 0-1 range to make it easier producing even distribution. If anyone has free time, this seems like fun thing to investigate. Does Unity Random.value not generate every possible floating point value in the 0-1 range, does it generate every value and each specific small value is less common (in which case I would expect exactly 0 to be very rare, since there are a lot of unique values in the 0-0.00001 range) meaning unity documentation about probability of exactly 0 is wrong, or does it roughly follow the principle of each specific small value being less common and only 0 is an exception (that seems very wrong and unlikely).

Okay so this forum post confirmed what I had a suspicion of but never quite knew how to look it up so thank you all for that.

TLDR: Use whatever you want cuz it basically doesn’t matter (which is ironically not as satisfying to hear without seeing the examples above)

But HAH I fricking love this forum. Going so deep on the math examples (great links and formatting, made it very easy to read), thank you lordofduct
And the lumpy pancake example is a good visual too

Because yeah fair, even with “law of large numbers” at play with 10m+ iterations, you still aren’t getting a true 50/50… so “close enough” really should be acceptable. Thank you for the variety of tests Nad_B

As for the RNG time test i actually find it mildly more interesting to consider using the System.Random as opposed to the UnityEngine version based on that example you gave.
IF at the end of this calculation it’ll be converted to a boolean anyways, then operationally it makes sense to use the System.Random as the rough distribution of above / below the midpoint of the set of numbers to pick from feels the same (see Nad_B’s edit) – but also i’m not sure how speed would be shown on a Switch vs a modern Mobile vs a high end gaming PC vs a PS5 etc

they’ll be able to make 0.0000000000001% more virtual frog coins than their opponents
Dead

As far as I’m concerned this thread and question is considered answered. Feel free to continue discussing tho because it’s definitely an interesting thought experiment. Thank you again for entertaining my (somewhat ridiculous) question and giving great examples and answers.

1 Like

Some times it’s amusing to go back to the NES. A lot of games had random item drops, but the NES had no psuedo-random number gen and the formulas we use now would have been too heavy for the NES processor. Games all had their own weird, improvised solutions. I could never tell, though. To me it was all just random.

No one links the obvious, apparently…

https://docs.unity3d.com/Packages/com.unity.mathematics@0.0/api/Unity.Mathematics.Random.html#Unity_Mathematics_Random_NextBool

Oh yeah we did! We just didn’t want the same game twice, that’s all.

Back in the TRS-80 days (Z-80 and 8085 CPUs) this was my typical RND function:

; random number generator - returns pseudo-random in A
; uses 3 bytes of state, incorporates arbitrary data
; contained in A and B and the memory (HL) points to.
rnd:
; starts out with whatever happened to be in A register
    push    hl
    adc    (hl)        ; add with carry whatever HL happened to be pointing to
    ld    hl,rr
    adc    (hl)        ; add and store
    ld    (hl),a
    inc    hl
    adc    (hl)        ; add and store
    ld    (hl),a
    inc    hl
    sbc    (hl)        ; sub and store
    ld    (hl),a
    adc    b            ; fold in the incidental contents of B register
    pop    hl
    ret

; random state store:
rr ds 3

Still in use today in some of my 8085 emulated games in KurtMaster2D. :slight_smile:

I had a similar one in 6502 on the Apple II, just don’t have any of that code anymore, alas.

Apple iTunes: https://itunes.apple.com/us/app/kurtmaster2d/id1015692678
Google Play (including TV): https://play.google.com/store/apps/details?id=com.plbm.plbm1

Itch.io: https://kurtdekker.itch.io/kurtmaster2d