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},
{["Name"] = "Bill", ["Chance"] = .3},
{["Name"] = "Ryan", ["Chance"] = .2}
}
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)
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]
for i=1, #Chances do
if num <= Chances[i] then
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},
{["Name"] = "Bill", ["Chance"] = .3},
{["Name"] = "Ryan", ["Chance"] = .2}
}
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
3 comments
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..?
Efficiency isn't an issue with it - you're just talking about writing smaller code correct?
yes