Scripting HelpersARCHIVE
A little piece of Roblox history. Preserved for everyone, one question at a time.Read about the archive →
✓ ANSWER ACCEPTED

How to pick something random from a table using chance?

local numbers = {1, 2, 3}

I want to make a system which chooses something from a table based on chance, an example usage of this system would be in something like a case opening system.

How would i be able to make it select one of the three numbers based on chance instead of JUST using math.random(1, 3) ?

For example, 1 has a 70% chance, 2 has a 20% chance and 3 has a 10% chance.

Thanks in advance.

2 answers

✓ Accepted answer

Ok, so my solution to the problem is this:

We have an array of objects that have a different chance of being picked:

Monsters = {
{["Name"] = "Jeff", ["Chance"] = .5},--1 (indices)
{["Name"] = "Bill", ["Chance"] = .3},--2
{["Name"] = "Ryan", ["Chance"] = .2} --3
}

Now here's the magic part, we create "bins" for each object. Each bin's size will reflect its chance of being selected. We do this by making a parallel array which values correspond to the sum of the object's chance of selection plus all of the object's chances before it. It's important to note that this method will still work even if your chances don't add up to 1, but the percentages will be different than specified if it isn't.

For this array it's {.5, .8, 1})

{0.5, 0.5+0.3, 0.5+0.3+0.2}

function GenerateBins(WeightedTable) --This'll generate the array that defines the bin values
    local Arr = {}
    for i=1, #WeightedTable do
        if not (i <= 1) then
            Arr[i] = Arr[i-1] + WeightedTable[i].Chance
        else
            Arr[i] = WeightedTable[i].Chance
        end
    end
    return Arr
end

Next, we just have to create the bin array and generate a random number range 0 to the sum of the chances. Then we just have to look for the first bin that the random number is smaller than the value in the bin array to see which bin it belongs in.

Note: I could optimize this by not generating the table everytime I call the function but it's fine for this example. Just store it in a variable or cache it some other way.

Rand = Random.new(tick())
function PickMonster(Monsters)
    local Chances = GenerateBins(Monsters)
    local num = Rand:NextNumber()*Chances[#Chances] --Generates a number range 0 to 1, so if we multiply by the sum of the chances (last value in bin array) it'll be 0 to the desired value.
    for i=1, #Chances do
        if num <= Chances[i] then --If it's smaller than the highest possible number for the bin, select it
            return Monsters[i]
        end
    end
end

Then I tested it using this, which just counts how many times each item was selected and the calculates the percentage.

Tests = 10000
Appearances = {}
for i=1, Tests do
    local pick = PickMonster(Monsters)
    if not Appearances[pick.Name] then
        Appearances[pick.Name] = 1
    else
        Appearances[pick.Name] = Appearances[pick.Name] + 1
    end
end

for i, v in pairs(Appearances) do
    print(i .. "\tshowed up " .. v .. " times out of " .. Tests .. " (" .. v/Tests * 100 .. ")")
end

Example output: Ryan showed up 1942 times out of 10000 (19.42) Bill showed up 3037 times out of 10000 (30.37) Jeff showed up 5021 times out of 10000 (50.21)

Which reflects the intended result!

Whole code

Monsters = {
{["Name"] = "Jeff", ["Chance"] = .5},--1 (indices)
{["Name"] = "Bill", ["Chance"] = .3},--2
{["Name"] = "Ryan", ["Chance"] = .2} --3
}

--Gets the topmost value that will select the monster, and places it in a parallel array
function GenerateBins(WeightedTable)
    local Arr = {}
    for i=1, #WeightedTable do
        if not (i <= 1) then
            Arr[i] = Arr[i-1] + WeightedTable[i].Chance
        else
            Arr[i] = WeightedTable[i].Chance
        end
    end
    return Arr
end

Rand = Random.new(tick())
function PickMonster(Monsters)
    local Chances = GenerateBins(Monsters)
    local num = Rand:NextNumber()*Chances[#Chances]
    for i=1, #Chances do
        if num <= Chances[i] then
            return Monsters[i]
        end
    end
end

Tests = 10000
Appearances = {}
for i=1, Tests do
    local pick = PickMonster(Monsters)
    if not Appearances[pick.Name] then
        Appearances[pick.Name] = 1
    else
        Appearances[pick.Name] = Appearances[pick.Name] + 1
    end
end

for i, v in pairs(Appearances) do
    print(i .. "\tshowed up " .. v .. " times out of " .. Tests .. " (" .. v/Tests * 100 .. ")")
end

Well depending on if you want something to be picked every time you try to pick it, then you would need to set up three values...

local chance1 = 245235 -- Doesn't matter if you wan't a default, will always be picked
local chance2 = 30		-- 30% chance
local chance3 = 10		-- 10% chance

local random_chance = math.random(1,100)

if random_chance <= chance3 then
	-- lowest chance picked
elseif random_chance <= chance2 + chance3 then
	-- second lowest changed picked
else
	-- most common chance picked
end

~

3 comments

0

Seems like a nice solution, however, i'm sure that some case opening systems don't have a dozen ifs and elseifs for each item.. is there an efficient way of doing this, if you have for example, 10 values..?

0

Efficiency isn't an issue with it - you're just talking about writing smaller code correct?

0

yes

This discussion is preserved as it was. New answers, comments, and votes are closed.