How should I implement weighted luck into my game?

I want to create a weighted luck for my game, what is the best way of doing so? I want my luck tables looking somewhat like this:

local rarities = {
	['Common'] = 50,
	['Uncommon'] = 25,
	['Rare'] = 24,
	['Legendary'] = 1
}

I will have luck modifiers (obviously wont be p2w), im not sure what is the best way of implementing this.

1 Like

See this post:

1 Like

Is there a way to implement luck modifiers into that? I want to add x2 luck and stuff to some of the features.

Well it’s certainly possible, but implementation relies heavily on what you mean by 2x luck.

Weighted chance is a pretty simple idea.
Start with the simplest case. Equal weight

local rarities = {
	'Common',
	'Rare',
}

In this case, I merely have to pick one at random

return rarities[math.random(#rarities)]

Now a weighted random… Let’s say (again keeping it simple, we want Common to appear 75% of the time.

We could do this

local rarities = {
    'common',
    'common',
    'common',
    'rare',
}

local function pick()
    return rarities[math.random(#rarities)]
end

Now it’s weighted. Since common is in the list 3 times and rare in there once, we would expect common to be the result 3/4 times. Now it’s a weighted random. We have 2 more changes to make, but these changes just make it more convenient to work with and use less memory, it’s still fundamentally doing exactly what’s above.

The first change is that writing out a new one for every item is tedious and balancing them is annoying.
So like how you are doing so in you’re example, we will assign a weight. This weight is the amount of times we want it to appear in our list. So to match our last example we can do

local rarities = {
    common = 3
    rare = 1
}

At this point though, we are going to work with your original data

local rarities = {
	['Common'] = 50,
	['Uncommon'] = 25,
	['Rare'] = 24,
	['Legendary'] = 1
}

In which case our pool will have 50 commons listed out and 25 uncommons, 24 rares and 1 legendary.

Now we could write some code that expands that into a list

local pool = {}
for name, count in pairs(rarities) do
    for i=1, count do
        table.insert(pool, name)
    end
end

Which would write it all out so we can just pick a random one from the list again.

However, there is an optimization we can make. It technically won’t be faster, but it will use less memory. Instead of making a list of all the names, we can just keep the count of how many are in each and get a total

local total = 0
for name, count do
    total = total + count
end

Now with your example total is equal to 100 since that’s how many entries we would have if we were to expand it out into a list. Now we pick a random number 1-100. This is going to effectively be the index we want in our list.

local r = math.random(100) --If only one value is given it defaults to math.random(1,100) btw

And now since we don’t really have a list of 100 values, we will have to do some math to figure out which category this would have landed in

local function pick()
    local r = math.random(total)
    for name, count in pairs(rarities) do
        r = r - count
        if r <= 0 then
            return name
        end
    end
end

Basically we just loop through each value and subtract the amount of entries it would have in the pool. If it ends up being less than or equal to 0, then it would have landed somewhere in the section with that name.

Hopefully this makes sense and you understand weighted random systems better.

To change the effects of luck, you will need to double the entries of an item you want the chance to double for. However, note that because it’s dynamic, you won’t quite double the chances.

Say you double the amount of tickets rare has.
Now there are 48 rares
The older percentages are common 50%, uncommon 25%, rare 24%, legendary 1%. Now that we have doubled the tickets for rare, the new percentages are (tickets/124(total)) common 50/124 = 40.32% uncommon 25/124 = 20.16% rare 48/124 = 38.71% legendary 1/124 = 0.81%.

So the exact definition of 2X is super important here as changing one changes the chances for all. Just keep in mind if you change the amount of entries an item has, you will need to recalculate the total to account for it, otherwise you’ll make some of the entries either more rare than they should be, or impossible to get.

1 Like

In fact, when it comes to 2x. It might just be easier to make a separate chance table for it

local rarities = {
	['Common'] = 50,
	['Uncommon'] = 25,
	['Rare'] = 24,
	['Legendary'] = 1
}

local raritiesHigherOdds = {
	['Common'] = 25,
	['Uncommon'] = 25,
	['Rare'] = 45,
	['Legendary'] = 5
}

Though you of course still can dynamically change the weights if it makes more sense

Common items - Less chance
Rare items - More chance.

My current prototype looks like this:

Sorry for this scripting
local module = {}

local function convertToRetardedTable(tbl)
    local new = {}
    for i, v in pairs(tbl) do
        table.insert(new, {i, v})
    end
    return new
end

function module:Roll(luckTable, luckMod)
    luckMod = tonumber(luckMod) or 1 -- 100 IQ fr
    
    local luck = 0
    for i, v in pairs(luckTable) do
        luck+=v
    end
    local chosen = math.random(1, luck/luckMod) --Lower number -> Better drop, so high luck will result in most common items having 0% chance.
    luckTable = convertToRetardedTable(luckTable)
    table.sort(luckTable, function(a,b) return a[2]<b[2] end)
    
    local luck = 0
    for i, v in pairs(luckTable) do
        luck+=v[2]
        if luck >= chosen then
            return v[1]
        end
    end
end

return module

The problem I already see is that it uses luck to sort it out, so if legendary have a higher chance of dropping and you apply a luck modifier, it will have less chance. I still dont find any better solution, adding a tier table for proper sorting would help in my module.

Hmm… I’m still not entirely certain of desired behavior, but you could make it so that luck takes tickets from common/uncommon and gives them to rare/legendary. That will actually keep the chances the same. Though if you want a smoother transition you’ll want to multiply every weight by 100 or something so you can add parts of a percentage instead when transferring tickets.

Or you could even split it up


local low = {
    common = 50
    uncommon = 25
}

local high = {
    rare = 24
    legendary = 1
}

local tiers = {
    low = 75
    high = 25
}

Then you can add more tickets to high tier without worrying to much about how you are affecting the actual numbers. And the higher your luck, the less impact those 75 tickets would have on the actual pull, though you could remove those tickets as well if you don’t want them to ever show up.

I think I will just stick to my current way.

1 Like