Search Unity

Making Random.Range generate a value with probability?

Discussion in 'Scripting' started by Batman_831, Jun 26, 2015.

  1. Batman_831

    Batman_831

    Joined:
    Oct 17, 2014
    Posts:
    106
    Using something like this
    Code (CSharp):
    1. Random.Range(0, 11);
    generates a random value between 0-10. But, can I somehow generate random values but with specific probability, like there is 50 % chance for 0-5 to be generated, 30% of 6-8 to be generated and 20% chance of 9-10 to be generated?
     
  2. larku

    larku

    Joined:
    Mar 14, 2013
    Posts:
    1,422
    There are lots of ways to do this. One common way is to fill a container with the set of values and then select one from it. Something like this pseudo code (add the correct number of values to get the distribution you're wanting):

    Code (csharp):
    1. private string[] values = {0,0,0,0,0, 1,1,1,1,1, 2,2,2,2,2, 3,3,3,3,3, 4,4,4,4,4, 5,5,5,5,5,
    2.                            6,6,6, 7,7,7, 8,8,8, 9,9, 10,10};
    3.  
    4. //
    5. // Then randomly select a value from that list:
    6. //
    7. private int selected = values[Random.Range( 0, values.Length )];
     
    Last edited: Jun 26, 2015
  3. Batman_831

    Batman_831

    Joined:
    Oct 17, 2014
    Posts:
    106
    @larku , good idea but the above method is certainly not efficient to write, and also it is not possible if I want in case of large range of values. What are the other ways?
     
    matkoniecz likes this.
  4. larku

    larku

    Joined:
    Mar 14, 2013
    Posts:
    1,422
    You could just populate the array (or list) programatically. Would be trivial to do in a few loops. I would do it programatically, I was just illustrating the concept.
     
    honor0102 and Carterryan1990 like this.
  5. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    If you simply need your use case, the easiest thing is just to hard code the probability:

    Code (CSharp):
    1. int GetRandomValue() {
    2.     float rand = Random.value;
    3.     if (rand <= .5f)
    4.         return Random.Range(0, 6);
    5.     if (rand <= .8f)
    6.         return Random.Range(6, 9);
    7.  
    8.     return Random.Range(9, 11);
    9. }
    If you need a more general case, some simple data structure isn't hard to write up:

    Code (CSharp):
    1. //helper structure
    2. struct RandomSelection {
    3.     private int minValue;
    4.     private int maxValue;
    5.     public float probability;
    6.  
    7.     public RandomSelection(int minValue, int maxValue, float probability) {
    8.         this.minValue = minValue;
    9.         this.maxValue = maxValue;
    10.         this.probability = probability;
    11.     }
    12.  
    13.     public int GetValue() { return Random.Range(minValue, maxValue + 1); }
    14. }
    15.  
    16. //usage example
    17. void Start() {
    18.     int random = GetRandomValue(
    19.         new RandomSelection(0, 5, .5f),
    20.         new RandomSelection(6, 8, .3f),
    21.         new RandomSelection(9, 10, .2f)
    22.     );
    23. }
    24.  
    25. int GetRandomValue(params RandomSelection[] selections) {
    26.     float rand = Random.value;
    27.     float currentProb = 0;
    28.     foreach (var selection in selections) {
    29.         currentProb += selection.probability;
    30.         if (rand <= currentProb)
    31.             return selection.GetValue();
    32.     }
    33.  
    34.     //will happen if the input's probabilities sums to less than 1
    35.     //throw error here if that's appropriate
    36.     return -1;
    37. }
     
  6. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    Was going to just write up something like Baste posted.

    But since it's already there, no need for me to do so.

    I would like to point out @larku, your example would not distribute into the odds you describe.

    You end up with 43 total values in the array, 30 that are 0-5, 9 that are 6-8, and 4 that are 9-10. This would result in the respective odds of: 69.76%, 20.9%, 9.3% (all approximations).
     
    matkoniecz likes this.
  7. Batman_831

    Batman_831

    Joined:
    Oct 17, 2014
    Posts:
    106
    hard coding probaility seems fit. I need to ask just one thing, in
    Code (CSharp):
    1.     int GetRandomValue() {
    2.         float rand = Random.value;
    3.         if (rand <= .5f)
    4.             return Random.Range(0, 6);
    5.         if (rand <= .8f)
    6.             return Random.Range(6, 9);
    7.  
    8.         return Random.Range(9, 11);
    9.     }
    10.  
    are we comparing thresholds(just a word I guessed it might be, don't know if it was the right one to use)? like if 0.5f is for 0-5 and 0.8f is for 6-8 then the probability chance for 6-8 is actually(0.8-0.5) = 0.3, right?
     
  8. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    Yup, pretty much.

    That's why I like the more advanced example better, because you're writing out the actual probability for the value, instead of building the "0.8-0.5"-maths into a bunch of if's. But if you only need this one place, hard-coding the stuff is probably the way to go.
     
  9. larku

    larku

    Joined:
    Mar 14, 2013
    Posts:
    1,422
    Yeah, that's why I qualified it with "add the correct number of values to get the distribution you're wanting", I was being rushed (was being called away)..
     
    Maco21 likes this.
  10. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    So I usually do my randoms with weights, instead of direct probabilities. With probabilities your need to ensure that all of them add up to 1. With weight we calculate the total and distribute the weights across it. IF you make sure they add up to 1 (or your can even do 100), the number just happens to read the probability you want... but it fills in the gaps for us if they don't.

    So I had never actually written this in C# for Unity before today, but it's based on the same logic behind what I wrote for 'IEnumerable.PickRandom' that can be found here:
    https://github.com/lordofduct/space...uppyBase/Utils/IEnumerableExtensionMethods.cs

    Code (csharp):
    1.  
    2.         public static T PickRandom<T>(this IEnumerable<T> lst, System.Func<T, float> weightPredicate, IRandom rng = null)
    3.         {
    4.             var arr = (lst is IList<T>) ? lst as IList<T> : lst.ToList();
    5.             if (arr.Count == 0) return default(T);
    6.             var weights = (from o in lst select weightPredicate(o)).ToArray();
    7.             var total = weights.Sum();
    8.             if (total <= 0) return arr[0];
    9.  
    10.             if (rng == null) rng = RandomUtil.Standard;
    11.             float r = rng.Next();
    12.             float s = 0f;
    13.  
    14.             int i;
    15.             for (i = 0; i < weights.Length; i++)
    16.             {
    17.                 s += weights[i] / total;
    18.                 if (s >= r)
    19.                 {
    20.                     return arr[i];
    21.                 }
    22.             }
    23.  
    24.             //should only get here if last element had a zero weight, and the r was large
    25.             i = arr.Count - 1;
    26.             while (i > 0 || weights[i] <= 0f) i--;
    27.             return arr[i];
    28.         }
    29.  
    The range one can now be found here:
    https://github.com/lordofduct/space...ster/SpacepuppyBase/Utils/Rand/RandomRange.cs

    You may note that both of these abstract that random number generator to an interface of IRandom. This is so that my random utils can take in which random number generator to use (unity, or ms). You can find that here:
    https://github.com/lordofduct/spacepuppy-unity-framework/blob/master/SpacepuppyBase/IRandom.cs
    and the imps here:
    https://github.com/lordofduct/space...lob/master/SpacepuppyBase/Utils/RandomUtil.cs

    Anyways, here it is swapped out to just hard code use unity's Random class:
    Code (csharp):
    1.  
    2.     public struct IntRange
    3.     {
    4.  
    5.         public int Min;
    6.         public int Max;
    7.         public float Weight;
    8.  
    9.     }
    10.  
    11.     public struct FloatRange
    12.     {
    13.         public float Min;
    14.         public float Max;
    15.         public float Weight;
    16.     }
    17.  
    18.     public static class RandomRange
    19.     {
    20.  
    21.         public static int Range(params IntRange[] ranges)
    22.         {
    23.             if (ranges.Length == 0) throw new System.ArgumentException("At least one range must be included.");
    24.             if (ranges.Length == 1) return Random.Range(ranges[0].Max, ranges[0].Min);
    25.  
    26.             float total = 0f;
    27.             for (int i = 0; i < ranges.Length; i++) total += ranges[i].Weight;
    28.  
    29.             float r = Random.value;
    30.             float s = 0f;
    31.  
    32.             int cnt = ranges.Length - 1;
    33.             for (int i = 0; i < cnt; i++)
    34.             {
    35.                 s += ranges[i].Weight / total;
    36.                 if (s >= r)
    37.                 {
    38.                     return Random.Range(ranges[i].Max, ranges[i].Min);
    39.                 }
    40.             }
    41.  
    42.             return Random.Range(ranges[cnt].Max, ranges[cnt].Min);
    43.         }
    44.  
    45.         public static float Range(params FloatRange[] ranges)
    46.         {
    47.             if (ranges.Length == 0) throw new System.ArgumentException("At least one range must be included.");
    48.             if (ranges.Length == 1) return Random.Range(ranges[0].Max, ranges[0].Min);
    49.  
    50.             float total = 0f;
    51.             for (int i = 0; i < ranges.Length; i++) total += ranges[i].Weight;
    52.  
    53.             float r = Random.value;
    54.             float s = 0f;
    55.  
    56.             int cnt = ranges.Length - 1;
    57.             for (int i = 0; i < cnt; i++)
    58.             {
    59.                 s += ranges[i].Weight / total;
    60.                 if (s >= r)
    61.                 {
    62.                     return Random.Range(ranges[i].Max, ranges[i].Min);
    63.                 }
    64.             }
    65.  
    66.             return Random.Range(ranges[cnt].Max, ranges[cnt].Min);
    67.         }
    68.  
    69.     }
    70.  
    And your use case:

    Code (csharp):
    1.  
    2. var value = RandomRange.Range(new IntRange(0, 6, 50f),
    3.                              new IntRange(6, 9, 30f),
    4.                              new IntRange(9, 11, 20f));
    5.  
    Noting I used the ranges so that they floor out correctly as the max value is never included during a random range generation (the random number is distributed from 0->0.99999..).
     
    Last edited: Jun 26, 2015
    SUPERHEii, sdb7, Hadronomy and 4 others like this.
  11. Batman_831

    Batman_831

    Joined:
    Oct 17, 2014
    Posts:
    106
    @lordofduct well, that's pretty good script but honestly I can only understand parts of it(as a beginner), I am currently trying to understand whole of it. I don't understand what do you mean by 'weight'? How can it fill the gaps left?
     
  12. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,531
    So if you do percentage probability, you have to make sure all your probabilities sum to 1 (100%). In Baste's example you'l run into a problem if you do:

    Code (csharp):
    1.  
    2.     int random = GetRandomValue(
    3.         new RandomSelection(0, 6, .4f),
    4.         new RandomSelection(6, 9, .2f),
    5.         new RandomSelection(9, 11, .15f)
    6.     );
    7.  
    Because this sums up to .75, you won't get any of those results. As in Baste's code he comments it:

    Code (csharp):
    1.  
    2. int GetRandomValue(params RandomSelection[] selections) {
    3.     float rand = Random.value;
    4.     float currentProb = 0;
    5.     foreach (var selection in selections) {
    6.         currentProb += selection.probability;
    7.         if (rand <= currentProb)
    8.             return selection.GetValue();
    9.     }
    10.  
    11.     //will happen if the input's probabilities sums to less than 1
    12.     //throw error here if that's appropriate
    13.     return -1;
    14. }
    15.  
    It returns -1 in this situation.

    Furthermore if you do an odds that sums greater than 1:

    Code (csharp):
    1.  
    2.     int random = GetRandomValue(
    3.         new RandomSelection(0, 6, .5f),
    4.         new RandomSelection(6, 9, .3f),
    5.         new RandomSelection(9, 11, .3f),
    6.         new RandomSelection(11, 15, .25f)
    7.     );
    8.  
    This sums to 1.35, as a result the 11-15 selection will have a 0% odd (never get reached), and the 9-11 selection has a 20% odd despite being flagged as 30%.



    In a weights system. The probability is based on the relative weights of the entries included. This means adding new entries keeps the same relative odds to one another no matter what. Say you weight out entries a-c as:

    A : 2
    B : 1
    C : 1

    If you take a weighted odd of this, A is twice as likely to be called as B or C. The probability becomes its weight / total.

    total = 2 + 1 + 1 = 4
    A : 2 / 4 : 50%
    B : 1 / 4 : 25%
    C : 1 / 4 : 25%

    Now, if we add a new entry, and we just give it a new weight you get:

    A : 2
    B : 1
    C : 1
    D : 4

    Here's the thing, A is STILL 2 times as likely as B and C. That didn't change.

    total = 8
    A : 2 / 8 : 25%
    B : 1 / 8 : 12.5%
    C : 1 / 8 : 12.5%
    D : 4 / 8 : 50%

    Note that 25 is twice that of 12.5.

    This is how odds in sports races are usually determined. You can tally up the relevant attributes of a competitor, speed, strength, mass, etc (whatever is relevant to the sport) and determine a 'weight' for that competitor. Now as long as that weight is determined the same way, it directly corresponds relativisticly to any other competitor and their weight. If a new competitor is included, a 3rd party enters the race, the calculated odds still remain the same, where as the overall chance of the race only changes.

    Where as if you go the percentage way. Adding a new entry requires recalculating each individuals odds each time to make sure they sum to the 100%.

    Programmatically, what I'm doing is just doing that in the odd calculator. I'm determining the odds for the specific race off the relative weighted odds of each member in the race.

    Of course, if you want something that reads like a percentage, you can still do that. If we take the first example:

    A : 2
    B : 1
    C : 1

    I could just put in:

    A : 50
    B : 25
    C : 25

    Which equals out to the desired percentage result:

    total = 100
    A : 50 / 100 : 50%
    B : 25 / 100 : 25%
    C : 25 / 100 : 25%

    And because this is all calculated at time of the random selection, you don't have the chance of something getting ignored from the selection, or the -1 coming back.
     
    SUPERHEii, jdrandir, pansoul and 3 others like this.
  13. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,334
    Weights are in all ways supperior to straight up hardcoded percentages.
     
    MikeMnD likes this.
  14. superpig

    superpig

    Drink more water! Unity Technologies

    Joined:
    Jan 16, 2011
    Posts:
    4,657
    Another approach you could take is to use an AnimationCurve. You'd add a public AnimationCurve field to your class so you can see and edit it in the inspector, and then call AnimationCurve.Evaluate(), passing in the 'unweighted' value from Random.range.

    What this will do is treat the value you pass in like an 'x' coordinate, and look up the corresponding 'y' coordinate on the curve.

    By changing the shape of the curve, you can alter the range and weighting of the values you get - a straight line from 0 to 1 would have no effect, but if you make the top of the curve flatter, you weight the function towards higher values, etc. You can also use this to scale and offset the results - e.g. by moving the bottom point on the curve up so that it is at y=0.2, you will not get values less than that. It can take a little getting used to but it's a very efficient and flexible way to encode (float => float) functions like in this situation.
     
    Magic_Nano, ysftulek, Reedex and 10 others like this.
  15. Kiwasi

    Kiwasi

    Joined:
    Dec 5, 2013
    Posts:
    16,860
    Yet another way is to come up with addition functions that produce the desired distribution of results. In many D20 games you might have a choice between a weapon that rolls 1d20 and one that rolls 2D10. The absolute values of the damage are pretty much the same, but the distribution of results is quite different.

    This property of adding random numbers together to modify the distributions is quite cool. Adding together as few as five small Random.Range will give you a distribution that is essentially normal. Very useful if you want normal distributions.

    If you don't want normal distributions this method will still work, but its probably inferior to many of those methods already discussed. I would suggest the AnimationCurve method as being the most flexible method.
     
    Batman_831 likes this.
  16. Batman_831

    Batman_831

    Joined:
    Oct 17, 2014
    Posts:
    106
    @superpig just experimented with the Animation curve... awesome ! Very flexible and quite easy. Thank you very much !
     
    superpig likes this.
  17. idurvesh

    idurvesh

    Joined:
    Jun 9, 2014
    Posts:
    495
  18. RockoDyne

    RockoDyne

    Joined:
    Apr 10, 2014
    Posts:
    2,234
    Batman_831 and idurvesh like this.
  19. idurvesh

    idurvesh

    Joined:
    Jun 9, 2014
    Posts:
    495
    dooleydragon likes this.
  20. dooleydragon

    dooleydragon

    Joined:
    Aug 4, 2017
    Posts:
    1
    Wow, this is an amazing piece of code to use for finding a random number by using a weighted system. I'm always amazed to see code like this that I can use and implement in my code today.
     
  21. mikaxms

    mikaxms

    Joined:
    Jun 23, 2018
    Posts:
    8
    Excuse me for posting in an old thread, but these all seem like needlessly complicated solutions. I would propose this (input chance from 0.01 to 1.00):

    Code (CSharp):
    1.  
    2. /// <summary> For chance calculations.
    3. /// Note: input chance from 0.01 to 1.00 </summary>
    4. public static bool FindProbability (float chance)
    5. {
    6.     // convert chance
    7.     int target = (int)(chance * 100);
    8.  
    9.     // random value
    10.     int random = Random.Range(1, 101);
    11.  
    12.     // compare to probability range
    13.     if (random >= 1 && random <= target)
    14.     {
    15.         return true;
    16.     }
    17.     else
    18.     {
    19.         return false;
    20.     }
    21. }
    22.  
     
  22. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Your version even complicates things quite a lot, because you can simply inline that by taking the call to Random.Range and comparing the return value to the probability value.
    Code (csharp):
    1. if (Random.Range(0f, 1f) < X) { ... }
    However, your implementation serves a different purpose than the ones discussed in this thread.
    That's because your code only returns information whether a certain probability is hit (true or false).

    What yours can do:
    Example: You've got a X% chance that a chest opens. It either opens (true), or not (false). That's why your function is sufficient for that.

    What yours cannot do:
    Example: You've got a X% chance to get a normal item, a Y% chance to get a rare item, a Z% chance to get an elite item.

    And that's exactly what the OP was looking for. A weighted system that takes multiple probabilities into account.
     
    MikeMnD likes this.
  23. unity_niWqoFh70ON5rg

    unity_niWqoFh70ON5rg

    Joined:
    Jun 24, 2019
    Posts:
    1
    Does this work ?
    The closer you get to zero, the higher the probability, even if it less customisable than other responses
    Code (CSharp):
    1. random = Random.Range(0, Random.Range(0, 11));