Pick random item based on percentages

local percentages = {
    Common = 50
    Uncommon = 25
    Rare = 15
    Epic = 10
}

local getRandomItem = math.random(1, percentages)

print('Item won: ' .. getRandomItem)

Basically, what I’m trying to do is the print should return the item that was selected at random (Common, Uncommon, Rare, Epic), but I don’t know how to actually get the percentages and have it pick the item based on its percentage of getting picked.

9 Likes

I don’t really know exactly the way to help.

However, after looking online, I’ve seen this post on Scripting Helpers which has a solution that makes sense.

3 Likes

You could try including an upper bound to your percentages so that there are two numbers to compare between:

local R = Random.new()

local MAX_RANGE = 100
local MIN_RANGE = 0
local percentages = {
    --Name = {lowerBound, upperBound}
    Common = {50, 100},
    Uncommon = {25, 50},
    Rare = {10, 25},
    Epic = {0, 10}
}
local getRandomItem = R:NextNumber(MIN_RANGE, MAX_RANGE)
local found
for item, bounds in pairs(percentages) do
    if getRandomItem >= bounds[1] and getRandomItem < bounds[2] then
        found = item
        break
    end
end
--whatever you wanna do with `found`

If you had a larger list of items and wanted to assign them appropriately according to a weight like you have, you could take the sum of all weights and make that the max range, and in the meantime create lower and upper bounds for all your items:

local weightedItems = {
    Duck1 = 30,
    Duck2 = 80,
    Duck3 = 11
}
local percentages = {}
local sum = 0
MIN_RANGE = sum
for k, weight in pairs(weightedItems) do
    percentages[k] = {sum, sum + weight}
    sum += weight
end
MAX_RANGE = sum

I just checked that scripting helpers page, and that guy came up with a better concept than I did, so just use that. In fact, I’ll just repurpose my code to model it:

local R = Random.new()

local weightedItems = {
    Duck1 = 30,
    Duck2 = 80,
    Duck3 = 11
}

local function getRandomItem(weightedItems)
    local totalWeight = 0
    for _, weight in pairs(weightedItems) do
        totalWeight += weight
    end
    
    local choice = R:NextNumber(0, totalWeight)
    local result
    for item, weight in pairs(weightedItems) do
        choice -= weight
        if choice <= 0 then
            return item
        end
    end

    error("item not found")
end
23 Likes

You should take a look at weighted randoms.

4 Likes

I will usually choose a random number from the total amounts in the list, and then loop through each item and check if the random number is <= to the current “percentage” it is looping through.

It’s a bit hard to explain with words, so I’ll make a mock piece of code and comment it:

local items = {
    {"Item1", 50},
    {"Item2", 20},
    {"Item3", 10}
} -- set a list of items or rarities with their weighted number

local maxVal = 0 -- add up all the values
for pos,val in next, items do
    maxVal = maxVal + val[2]
end -- get a total of the numbers in the list of items

local function getRandomItem(num)
    local count = 0 -- keep a temporary count of the current number
    for pos,val in next, items do
        count = count + val[2] -- add the probability to count
        if num <= count then -- if num <= count then that value has been picked
            return val[1] -- return the value
        end
    end
    return nil -- just in-case?
end

local randNum = math.random(1,maxVal)
local chosenItem = getRandomItem(randNum)

if chosenItem then
    -- do stuff
end

I wrote this on mobile, but hopefully you get the jist - you could also use this method to choose a rarity and then go on to choose a random weapon from a table or dictionary of that specific rarity.

7 Likes
local percentages = {
    Common = 50
    Uncommon = 25
    Rare = 15
    Epic = 10
}

local total = 0
for i, v in pairs(percentages) do
    total += v
end

local function getRandomItem()
     local r = math.random(1, total)
     local add = 0
     for item, v in pairs(percentages) do
        if r > add and r <= add + v then
            return item
        end
        add += v
    end
end

print(getRandomItem())

We get a random number between 1 and the total. Then, we iterate over all the item types, and add to the add variable so add evens out to the total at the end.

(Edit 3/4/21): Updated to work with totals that don’t add up to 100 aswell as added missing “do”. I wrote this 2 years ago, so I’ve updated it for future readers to use aswell.

9 Likes

I think this is being overcomplicated. First off, make the percentages numbers (just divide by 100). Then, put them in tables (so them don’t get called randomly) in descending order (order actually doesn’t matter but looks nicer that way). Now, you would do this:

local percentages = {
  {Name = "something1", Percentage = 0.5},
  {Name = "something2", Percentage = 0.3},
  {Name = "something3", Percentage = 0.2}
}

function getRandomObject(table)
   local rand = math.random(); -- random number between 0 and 1
   local pastPercentage = 0; -- bank so we can add percentage up as we go
   for i = 1, #table do --loop through all objects
      if rand < table[i].Percentage + pastPercentage then -- check if rand is under the percentage
         return table[i]; -- if so, that is our random choice
      end
      pastPercentage = pastPercentage + table[i].Percentage; -- if it isn't, add its percentage to the bank and scan the other
   end
end

Only problem is that the math.random excludes the number 1 so percentages won’t be perfect. If you want, you can even make this OOP so you can just do “percentage:RandomObject”:

OOP version
local percentages = {
  {Name = "something1", Percentage = 0.5},
  {Name = "something2", Percentage = 0.3},
  {Name = "something3", Percentage = 0.2}
}

function percentages:RandomObject()
   local rand = math.random(); -- random number between 0 and 1
   local pastPercentage = 0; -- bank so we can add percentage up as we go
   for i = 1, #self do --loop through all objects
      if rand < self[i].Percentage + pastPercentage then -- check if rand is under the percentage
         return self[i]; -- if so, that is our random choice
      end
      pastPercentage = pastPercentage + self[i].Percentage; -- if it isn't, add its percentage to the bank and scan the other
   end
end

--Now you can do
local randomObject = percentages:RandomObject();

This is probably not the most efficient thing, but just showing you how it could simply work.

Hope this helps!

1 Like

By the way, nitpicking, but you should be using the Random.new() API instead of math.random - its far better, and far more effective then the old and deprecated math.random API.

4 Likes

Adding on to my post, I found a video that explains this super well. In fact, his method of subtracting from rand is more efficient (I believe) than my percentage bank method.

Also, his other method of not even having to use percentages and just chances is very convenient as well (although less efficient).

Hope it helps!

Also, I’ve been talking a lot about efficiency, but to be honest efficiency on this level doesn’t matter unless you call this function hundreds of times per second. Having an extra addition calculation in a function that is called every few seconds won’t hurt.


@3dsboy08
Thanks, change has been made :smile:

Edit: anddd reverted due to @Ugh_Lily’s test. Again, efficiency at this level is not really needed but I’m a perfectionist so I use for i = 1, #table and things like that when I can DX

1 Like

I just so something like this:

local applechance = 30
local orangechance = 70
local choosetbl = {}
for i=1, applechance do
   choosetbl[#choosetbl + 1] = [[Apple]]
end
for i=1, orangechance do
   choosetbl[#choosetbl + 1] = [[Orange]]
end
local selected = choosetbl[math.random(1, #choosetbl)]

Apologies for the weird strings, I’m in my phone and quotation marks don’t work.

1 Like

The laziest way to do this is simply, create X “Common” values for the common percentage, and etc… for all of them in one table. Then just do math.random(1,#table)

2 Likes

Random.new() API instead of math.random - its far better, and far more effective then the old and deprecated math.random API.

Except it’s not. It’s the exact same API, just with a nicer syntax and slower.
image

Random.new() only has one thing even worth your time, which is NextNumber, but you can just do Min + random() * (Max - Min), and it’d still be faster.

I do greatly prefer how Random.new() looks, but honestly, it’s just more work to make it close to as fast as math.random.

4 Likes

That resembles a mapping function… oh wait… IT IS :man_facepalming: (my brain hates me sometimes)


But thank you for your testing! Hopefully OP has enough information to solve his problem.

Also, I really suggest Coding Math’s series of videos as they cover a wide range of topics from trig to fractals. Seriously, check him out. (He is the creator of the video I linked.)

I didn’t know it was a mapping function, but now I see it. And I don’t mind testing, the benchmark tool I have is open source in my models somewhere.

2 Likes

Obligatory disclaimer: This doesn’t actually matter. Seems this post has already caused confusion, so I guess it’s necessary to explain how and why this doesn’t have any real impact.

To get the fractions of a second you’ve measured in each of these cases, you need to generate a random number multiple millions of times. Even heavy random generation (e.g. terrain generation) doesn’t generate this many random numbers (e.g. the terrain generation plugin generates 1000 random numbers up front and that’s it), so you will never encounter any noteworthy performance difference between math.random and Random in real development.

Even if you do need to generate millions of random numbers, I’m not sure a 1/100th to a 1/10th of a second difference in random generation will be noticeable? If you’re doing so many operations, other parts of those operations will matter significantly more than any difference between the random APIs. If you are discussing performance of low level data types or Roblox APIs, you are doing something wrong. The only time you should be considering performance is at an algorithmic level or when dealing with something that involves latency/web requests.

https://devforum.roblox.com/t/is-division-slower-than-multiplication/216622/19

3 Likes

I just want to come out and say I completely understood what I was arguing in the other thread and it’s my fault if I caused confusion. Lily just did the thing she is best at: Testing speeds. I now know it’s foolish to say that math.random should be used over the Random object as speed differences are negligible compared to the readability that the Random object has; the locality (I think that’s the word?) that the Random object supports is the final blow to math.random.

But thank you for these further clarifications! I’m sure those stumbling upon this thread in the future will appreciate this :smile:

(Funny thing: I was actually going to make a post saying why multiplication is faster than division and why you should use it *0.1 instead of /10… Good thing I tested because sometimes division beat multiplication (not lying) and the difference ended up being so small I discerned it wasn’t worth it. I’m happy I came to the right conclusion.)

1 Like