0
\$\begingroup\$

For modelling a simple game of cricket, Considering a batsman has a single attribute rating. I can go about deciding run chances by weighted probabilities. Something like

    if batsman.rating <= 30:
        # map of run: probability
        probabilites = {0: 0.7, 1: 0.1, 2: 0.1, 3:0.5, 4:0.5, 6:0 }
    elif 30 <batsman.rating < 60
        probabilites = {0: 0.7, 1: 0.1, 2: 0.1, 3:0.5, 4:0.5, 6:0 }
    else:
        probabilites = {0: 0.7, 1: 0.1, 2: 0.1, 3:0.5, 4:0.5, 6:0 }

Now there are two challenges:

  1. What if I want finer control over batsman.rating and probabilities? How do I manage to keep the sum of all probabilities equal to 1?
  2. How to manage a second attribute to a batsman, let's say aggression. Which can lower chances of 0 and increase chances of 6?

Initially tried dealing to have finer control like below:

# map of batsman.rating: probability, 
rating_to_zero_chances = {0: 0.9, 10: 0.7, 30: 0.5, 60: 0.3, 90: 0.1}
rating_to_one_chances = {0: 0.9, 10: 0.7, 30: 0.5, 60: 0.3, 90: 0.1}
chances_of_zero =  <some library>.interpolate(rating_to_zero_chances).yvalue(batsman.rating)
chances_of_one =  <some library>.interpolate(rating_to_one_chances).yvalue(batsman.rating)
.
.
.
#do this for each run

The problem with this is that I cannot interpolate the probabilities such that they are equal to 1, Since the slope of runs for the interpolated data are different

For challenge 2, Initially tried handling it by making a probability density function and shifting the mean of the curve if the aggression is more. This creates a problem when there is a 3rd or 4th attribute added (such as timing etc are added).

Currently am planning to deal with a single run with its chance, rather than having to sum their probabilities to 1 like below:

zero_chance = interpolate(rating_to_zero_chances).yvalue(batsman.rating)
zero_chance += aggression / 100  # assuming aggression on 1-100
is_zero_scored = binom([0,1], p=zero_chance).random()
if is_zero_scored:
    return 0    
one_chance = interpolate(rating_to_one_chances).yvalue(batsman.rating)
one_chance += aggression / 90  # assuming aggression on 1-100
is_one_scored = binom([0,1], p=one_chance).random()
if is_one_scored:
   return 1
. # similarly for 2
.
.

Is there any neater way to do this? I am new to game development and had learned about probability distributions recently. Are there some theoretical/ practically established concepts I should be looking at? How do game developers handle and balance different chances of an event?

\$\endgroup\$
4
  • 1
    \$\begingroup\$ You can take values in an arbitrary value range for each probability. For example 0 to 10. Then just calculate the sum and divide each value by it. This is called normalization. Is that an option for you? \$\endgroup\$
    – wychmaster
    Aug 23, 2020 at 8:45
  • \$\begingroup\$ Yeah, actually did normalization at other places in the code. Does normalization work properly with probabilities? For eg: Considering runs of 0 and 1 each of them has a probability of 0.5, Now I want to improve the chance for 0 by 0.1, by normalization, I cant just add 0.1 since it turns out to be 0.54 (0.6/1.1). Any ideas on how to proceed with this? \$\endgroup\$ Aug 23, 2020 at 10:40
  • 1
    \$\begingroup\$ The only idea I have for your problem is to generate and solve a linear system of equations where one condition is that all your probabilities add up to 1. This can get a little bit complicated. \$\endgroup\$
    – wychmaster
    Aug 23, 2020 at 12:27
  • \$\begingroup\$ I think so. it is going to get a bit complicated. Feels like maybe I am going about this in the wrong way. \$\endgroup\$ Aug 23, 2020 at 12:39

1 Answer 1

1
+50
\$\begingroup\$

As mentioned in the comments, you can easily normalize a collection of relative weights in an arbitrary range to a set of probabilities summing to 1 as follows:

(Assuming no negative or #NAN weights)

float totalWeight

foreach(var item in items)
    totalWeight += item.weight;

foreach(var item in items)
    item.probability = item.weight / totalWeight;

Or you can sample from the weights directly, by replacing the division loop with:

float sample = random01() * totalWeight;

float accumulatedWeight = items[0].weight;
int selectedIndex = 0;

while(accumulatedWeight < sample) {
    accumulatedWeight += items[++selectedIndex].weight;
}

// selectedIndex now holds the index of your random selection.
return items[selectedIndex];

Personally, I like to author all my selection chances as relative weights, and let the computer sort out the mapping to probabilities. That way the tuning data is more robust against errors - I can't break the assumption that all probabilities will sum to 1 by changing one number and forgetting to change the others, because we're not making that assumption. Tuning changes become much easier, and so I get to do more iteration cycles, homing in on the ideal numbers more closely.

So, you could store relative weights for each of your rating tiers, interpolate them just the same as you would interpolate vectors, then use the normalization or sampling strategies above to choose randomly based on those interpolated weights.

If you want to increase the probability of an event with weight w, by a ratio of r, in the context of other background events with a total weight of b, then you can do so by replacing it with a new modified weight m, chosen like so:

$$\begin{align} P(m | b) &= r \cdot P(w | b)\\ \frac m {m+b} &= r \cdot \frac w {w+b}\\ mw + mb &= rmw + rwb\\ m (w + b - rw) &= rwb\\ m &= \frac {rwb} {(1 - r) w + b} \end{align}$$


Or, I should point out: you could also just stick with hard changes to the probability distributions at particular rating milestones, rather than smoothly blending from one tier to the next. The nature of randomness kind of "fuzzes-out" small incremental changes in probabilities, to the point that players would likely not be able to perceive a difference between being a rating 30 or rating 35. So in the name of simplicity, you could choose to use non-interpolated probabilities, and just space your milestones a little more tightly, so you get distinct, perceptible jumps in performance at key stat values.

\$\endgroup\$
2
  • \$\begingroup\$ It looks like you have the division the wrong way round in that second foreach loop - it should be weight /= totalWeight shouldn't it? \$\endgroup\$
    – Adam
    Sep 2, 2020 at 22:46
  • \$\begingroup\$ Yep! That'll learn me to type too fast. XD \$\endgroup\$
    – DMGregory
    Sep 2, 2020 at 22:48

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .