0

I am making a text-based RPG. I have an algorithm that determines the damage dealt by the player to the enemy which is based off the values of two variables. I am not sure how the first part of the algorithm will work quite yet, but that isn't important.

(AttackStrength is an attribute of the player that represents generally how strong his attacks are. WeaponStrength is an attribute of swords the player wields and represents generally how strong attacks are with the weapon.)

Here is how the algorithm will go:

import random
Damage = AttackStrength (Do some math operation to WeaponStrength) WeaponStrength
DamageDealt = randrange(DamageDealt - 4, DamageDealt + 1) #Bad pseudocode, sorry

What I am trying to do with the last line is get a random integer inside a range of integers with the minimum bound as 4 less than Damage, and the maximum bound as 1 more than Damage. But, that's not all. I want to assign probabilities that:

  • X% of the time DamageDealt will equal Damage
  • Y% of the time DamageDealt will equal one less than Damage
  • Z% of the time DamageDealt will equal two less than Damage
  • A% of the time DamageDealt will equal three less than Damage
  • B% of the time DamageDealt will equal three less than Damage
  • C% of the time DamageDealt will equal one more than Damage

I hope I haven't over-complicated all of this thank you!

4 Answers4

1

I think the easiest way to do random weighted probability when you have nice integer probabilities like that is to simply populate a list with multiple copies of your choices - in the right ratios - then choose one element from it, randomly.

Let's do it from -3 to 1 with your (original) weights of 10,10,25,25,30 percent. These share a gcd of 5, so you only need a list of length 20 to hold your choices:

choices = [-3]*2 + [-2]*2 + [-1]*5 + [0]*5 + [1]*6

And implementation done, just choose randomly from that. Demo showing 100 trials:

trials = [random.choice(choices) for _ in range(100)]

[trials.count(i) for i in range(-3,2)]
Out[18]: [11, 7, 27, 22, 33]
roippi
  • 25,533
  • 4
  • 48
  • 73
  • I'd argue that this is a suboptimal solution; this works well in this specific case, but fails to generalize elegantly (since the likelihood that the gcd will remain tidy across all dev iterations of a damage formula is slim, and a low gcd makes this pretty gross from a space perspective, since you're either generating a O(n) list each time you run this or holding onto that list which feels a little clumsy). – jmduke Jul 30 '14 at 01:59
  • Even with a gcd of 1, a list of 100 elements is /tiny/. This approach is clumsy to update if the probabilities are dynamic, yes, otherwise I see no reason to micro-optimize. – roippi Jul 30 '14 at 02:11
0

Essentially, what you're trying to accomplish is simulation of a loaded die: you have six possibilities and want to assign different probabilities to each one. This is a fairly interesting problem, mathematically speaking, and here is a wonderful piece on the subject.

Still, you're probably looking for something a little less verbose, and the easiest pattern to implement here would be via roulette wheel selection. Given a dictionary where keys are the various 'sides' (in this case, your possible damage formulae) and the values are the probabilities that each side can occur (.3, .25, etc.), the method looks like this:

def weighted_random_choice(choices):
    max = sum(choices.values())
    pick = random.uniform(0, max)
    current = 0
    for key, value in choices.items():
        current += value
        if current > pick:
            return key
jmduke
  • 1,657
  • 1
  • 13
  • 11
0

Suppose that we wanted to have these relative weights for the outcomes:

a = (10, 15, 15, 25, 25, 30)

Then we create a list of partial sums b and a function c:

import random
b = [sum(a[:i+1]) for i,x in enumerate(a)]
def c():
    n = random.randrange(sum(a))
    for i, v in enumerate(b):_
        if n < v: return i

The function c will return an integer from 0 to len(a)-1 with probability proportional to the weights specified in a.

John1024
  • 109,961
  • 14
  • 137
  • 171
0

This can be a tricky problem with a lot of different probabilities. Since you want to impose probabilities on the outcomes it's not really fair to call them "random". It always helps to imagine how you might represent your data. One way would be to keep a tuple of tuples like

probs = ((10, +1), (30, 0), (25, -1), (25, -2), (15, -3))

You will notice I have adjusted the series to put the highest adjustment first and so on. I have also removed the duplicate "15, -3) that your question implies because (I imagine) of a line duplicated by accident. One very useful test is to ensure that your probabilities add up to 100 (since I've represented them as integer percentages). This reveals a data fault:

>>> sum(prob[0] for prob in probs)
105

This needn't be an issue unless you really want your probabilities to sum to a sensible value. If this isn't necessary you can just treat them as weightings and select random numbers from (0, 104) instead of (0, 99). This is the course I will follow, but the adjustment should be relatively simple.

Given probs and a random number between 0 and (in your case) 104, you can iterate over the probs structure, accumulating probabilities until you find the bin this particular random number belongs to. This would look (something) like:

def damage_offset(N):
    prob = random.randint(0, N-1)
    cum_prob = 0
    for prob, offset in probs:
        cum_prob += prob
        if cum_prob >= prob:
            return offset

This should always terminate if you get your data right (hence my paranoid check on your weightings - I've been doing this quite a while).

Of course it's often possible to trade memory for speed. If the above needs to work faster then it's relatively easy to create a structure that maps random integer choices direct to their results. One way to construct such a mapping would be

damage_offsets = []
for i in range(N):
    damage_offsets.append(damage_offset(i))

Then all you have to do after you've picked your random number r between 1 and N is to look up damage_offsets[r-1] for the particular value of r1 and you have created an O(1) operation. As I mentioned at the start, this isn't likely going to be terribly useful unless your probability list becomes huge (but if it does then you really will need to to avoid O(N) operations when you have large N for the number of probability buckets).

Apologies for untested code.

holdenweb
  • 33,305
  • 7
  • 57
  • 77