views:

99

answers:

5

This is almost certainly a very novice question, but being as I am a complete novice, I'm fine with that.

To put it simply, I'd like to know how to make a loot drop system in a simple game, where when you achieve a certain objective, you have a chance of getting certain objects more than others. If there are any open-source python games that have this, please refer me to them.

Here is what I know how to do: given a sample [A,B,C,D,E,F], select 3 items.

This is really simple and easy, however, what do I do when I would like to have somethings from the sample be selected more often than others, ie: given sample [A,B,C,D,E,F] have 3 be selected, without repeats, but have A be selected 30% of the time, B 25%, C 20%, D 15%, E 5%, F 5%.

Or, perhaps even better, have no limit (or a ranged limit, eg. 3-5 items) on the amount selected, but have each item in the sample be selected at a different rate and without repeats, so that I could do A 20%, B 20%, C 15%, D 10%, E 2%, F 1%.

Hope this makes sense.

A: 

pseudocode:

if(random()<0.2)addloot(a);
if(random()<0.15)addloot(b);
if(random()<0.1)addloot(c);
if(random()<0.02)addloot(d);
if(random()<0.01)addloot(e);

where random is a random number from 0 to 1. This is how it works in all MMORPG.

BarsMonster
It would be fairer (and faster) to evaluate `random()` only once.
wallyk
NO. this is wrong.If user would get 0.001 he will always have all the loot in this case. It would be impossible to get only rare thing.
BarsMonster
But in your case every time the user gets a drop he gets 5 `random()` s. Why not just do an if/else? Or are you trying to make sure they get more than one item each time? How then will you make sure that they only get 3 items?
Stephen
In the end of the question question-starter sayd that no-limit would be better, and described classical MMORPG-drop system. It's implemented here. if/else - because all these checks MUST be completely non-corelated.
BarsMonster
@BarsMonster - The checks do not have to be non-correlated. They only have to be non-correlated in the if-else system you've given here. I think it would be better to completely decouple the number of items you get from their probabilities. That way you can implement a number of heuristics to decide on the number of items dropped. For example make the probability of getting more decrease dramatically when more than a certain total value has been dropped.
Omnifarious
I'll try this implementation next. It seems to allow for more variability in what loot drops. Thanks for your input!
BrotherGA2
I see what you're saying with it being how it works in MMO's. If you kill boar #1, you might get nothing, boar #2, some boar hide, boar #3 Great Sword of Boar Tusks. I was thinking more along the lines of a text-based game, where as an exercise, a treasure is opened, and different loot will drop everytime. What you're presenting is potentially more interesting though. I just have to figure out how I'm going to implement drops (repeatable and/or not in the same sample) and inventory. Thanks again.
BrotherGA2
+1  A: 

Here's an easy, lazy way to do it.

Given a list of (item,weight) pairs.

loot = [ (A,20), (B,20), (C,15), (D,10), (E,2), (F,1) ]

Note, the weights don't have to add to anything in particular, they just have to be integers.

One-time preparation step.

choices = []
for item, weight in loot:
    choices.extend( [item]*weight )

Now it's just random.choice( choices ).

S.Lott
That's actually really simple. In a good way. At first I had a hard time figuring out how to implement this, but once I figured out everything necessary for this to work, I got it. Thanks, man! Not exactly neat, making the long lists, but it works for what I need it to do.
BrotherGA2
@BrotherGA2: "Not exactly neat, making the long lists"? It looks like 3 lines of code to me. What's "not exactly neat"?
S.Lott
A: 

Here's a nice recipe if you want a smooth gradation of likelihoods without making an enormous list to sample from:

class WeightedRandom(random.Random):
    """All numbers are random, but some are more random than others.

    Initialise with a weighting curve gamma. gamma=1 is unweighted, >1 returns
    the lower numbers more often, <1 prefers the higher numbers. 
    """
    def __init__(self, gamma):
        self.gamma= gamma # 1 is unweighted, >1 pushes values downwards
        random.Random.__init__(self)
    def random(self):
        return random.Random.random(self)**self.gamma

    # Override the standard sample method, whose pool-based 'optimisation' cocks
    # up weighted sampling. We know result set is small, so no need for dict
    # lookup either.
    #
    def sample(self, population, k):
        if k>=len(population):
            return population
        indexes= []
        for _ in range(k):
            while True:
                index= int(self.random()*len(population))
                if index not in indexes:
                    break
            indexes.append(index)
        return [population[index] for index in indexes]

>>> r= WeightedRandom(0.5)
>>> r.sample(range(100), 3)
[86, 98, 81]
bobince
Using a gamma function is an interesting idea. Is gamma a power-law distribution? If it isn't, I would suggest a power-law distribution instead.
Omnifarious
To be honest, this method is a bit all over my head right now (mostly the terminology--I'm a toddler at programming/python). As I get a little further along in my programming chops I'll try to read this again and see if I understand what's going on. Your description does make it sound desirable though. Thanks for the code!
BrotherGA2
+1  A: 

You threw me off a little when you characterized this question as a "very novice" one. It's not as simple as it looks, depending on what kind of behavior you want. BarsMonster's answer is a good one if you don't mind that a lucky player can win all the items and an unlucky player can come away with nothing.

If you want to always select a certain number of items, then I would go with S.Lott's method of picking one item, but use it repeatedly. If you don't want to allow the same item to be selected more than once, you have to remove the chosen item from loot and then rebuild choices between selections. For example (very rough pseudocode):

items_won = random.randint(3, 5)
for i in range(items_won):
    item_won = s_lott_weighted_selection()
    inventory.add(item_won)
    loot.remove(item_won)
John Y
Yeah... I didn't realize this wasn't just a simple "import random" and y'er pretty much done. S.Lott's method seems pretty simple, but like you said, has potential to require extra work or cause headaches. BarsMonster's, with some modification, might be what I'll end up using, if I can figure out how to get it to work.Thanks for the inventory/loot.remove addition. Helped out quite a bit in figuring it out.
BrotherGA2
A: 

An alternative to S.Lott's weighted selection.

Warning - untested code.

import random

def weighted_selection(weights):    
    """returns an index corresponding to the weight of the item chosen"""
    total_sum = sum(weights)
    rnd = random.uniform(0, total_sum)
    cumulative_sum = 0
    for (idx, weight) in enumerate(weights):
        if rnd <= cumulative_sum + weight:
            return idx
        cumulative_sum += weight
    assert(0) # should never get here

weights = [30, 25, 20, 15, 5, 5]
# example of choosing 1 - will return value from 0 to 5
choice = weighted_selection(weights)

# example of choosing 3 such values without repeats
choices = []
for n in range(3):
    new_choice = weighted_selection(weights)
    del weights[new_choice]
    choices.append(new_choice)

You may want to wrap the selection-without-replacement code at the end in some sort of wrapper that ensures the number of unique choices you make never exceeds the number of options available.

Kylotan