Chance
By @avodey (my main account)!
Module
This uses an Assert module, which I made not specifically for this. It would help if you found bugs in that!
Summary (v1.0.1)
We’ve all been there. Making a chance system is quite the challenge, especially if you wanted to also add a “2x Luck gamepass” or some sort of luck modifier. What does double the luck even mean? What if they’re too lucky?? This module will allow you to make an easy chance system with no worries!
You will be able to create a Chance generator that takes in a dictionary like this:
Apple: 50
Banana: 20
Orange: 15
Pear: 10
Dragonfruit: 4
Starfruit: 1
All these chances must not go over 100%! Calling the Run
method of the generator will allow you to get one of these items based on their specified odds.
Precision
Ah, so you wanted proof on how reliable it is? Here’s the code that I ran:
And here is the result:
The testing code generated 1 trillion items and calculated the percent of times it showed up! You would have to run it a infinite amount of times to get it as close to the real chances as possible. This took 4 minutes to calculate, so maybe do less than a trillion for your own test.
You can test the precision using this code segment:
local results = {}
local total = 1e3 -- Increase for more precision, but more computing time
for i = 1, total do
local item = chance:Run()
if item then
results[item] = (results[item] or 0) + 1
end
end
for i, v in pairs(results) do
local chance = v / total * 100
warn(`{i}: {math.floor(chance * 100) / 100}%`)
end
Use Cases
1: Lootboxes, Crates, and Eggs
This use case will allow you to make a random item system.
Heads up!
If your allowing the user to buy random items with robux (directly, or indirectly), the system should comply with PolicyService. Not all countries allow people to gamble in the metaverse, or at all!
Additionally, consider making luck inactive with direct robux random items. This isn’t required, but it would make your games more fair.
View Examples
local Chance = require(game.ReplicatedStorage.Chance)
local crate = Chance.new({
Apple = 50,
Banana = 20,
Orange = 15,
Pear = 10,
Dragonfruit = 4,
Starfruit = 1,
})
for i = 1, 20 do
print(crate:Run())
end
This script creates a new crate with chances to get fruits. The chance to get an apple is 50%, banana is 20%, etc. Let me explain each line!
1: Get the Chance module
3: Create a new generator called Crate
12: Iterate 20 times:
13: Print a generated item each time
The generated item is one of the keys, based on their odds. It can be a string, an Enum, an instance, or anything! You should see Apple printed 50% of the time, Banana 20% of the time, and so on.
The module also provides a simple chance method which only uses one number, which is the chance.
local Chance = require(game.ReplicatedStorage.Chance)
for i = 1, 10 do
print(Chance:Run(50)) --> true 50% of the time
end
You can use this for whatever you want! For the lootboxes use case, it can be used to also determine if this item is gold, rainbow, shiny, evolved, or whatever.
Now, let’s add luck to our lootboxes. You can use the second argument of the constructor to set the luck, or the SetLuck
method of the generator. The luck should be between 0 and 1, but feel free to go over 1.
local chance = Chance.new({
Apple = 50,
Banana = 20,
Orange = 15,
Pear = 10,
Dragonfruit = 4,
Starfruit = 1,
})
local function test(luck: number)
warn(`Luck: {luck}`)
chance:SetLuck(luck)
local results = {}
local total = 1e5
for i = 1, total do
local item = chance:Run()
if item then
results[item] = (results[item] or 0) + 1
end
end
for i, v in pairs(results) do
local chance = v / total * 100
print(`{i}: {math.floor(chance * 100) / 100}%`)
end
end
for i = 0, 3, 0.5 do
test(i)
end
The output of the script looks something like this:
As you can see, the rarer items get less rare, and the common items get less common. Overall, the chances still add up to the same 100%. Luck logic will be explained later!
2: Murder Mystery
This use case allows you to pick who’s innocent, the sheriff, and the murderer!
View Examples
local items = {}
local chance = Chance.new(items)
local players = {"avodey", "bluebxrrybot", "BitDancer", "VSCPlays", "lolmansReturn", "KrimsonWoIf"}
for _, player in pairs(players) do
items[player] = 100 / #players
end
print(chance:Run()) --> KrimsonWoIf
This script choses a random murderer or sheriff among all the players in the server. In reality, it wouldn’t be a list of strings, it would be players. For the sake of testing, I hardcoded the test with a list of strings! When picking the sheriff, we should remove the murderer first.
local items = {}
local players = {"avodey", "bluebxrrybot", "BitDancer", "VSCPlays", "lolmansReturn", "KrimsonWoIf"}
local chance = Chance.new(items)
local function recalculate()
table.clear(items)
for _, player in pairs(players) do
items[player] = 100 / #players
end
end
local function remove(player: string)
table.remove(players, table.find(players, player))
recalculate()
end
recalculate()
local murderer = chance:Run()
remove(murderer)
local sheriff = chance:Run()
remove(sheriff)
warn(`The murderer is {murderer}!`)
warn(`But don't worry, the sheriff is {sheriff}!`)
print("Innocents:", players)
--[[ Output:
The murderer is KrimsonWoIf!
But don't worry, the sheriff is lolmansReturn!
Innocents: {avodey, bluebxrrybot, VSCPlays}
]]
We need to recalculate the chances after a player is removed, or else there is a chance the resulting item could be nil. It may also go over 100% if you don’t remove the murderer or sheriff.
This script modifies the items even after used in the generator. It will error if the items are invalid only when you run their chances, because that is when they are validated.
3: Random Events
This use case lets you spawn random events based on their odds per minute.
View Examples
local chance = Chance.new({
Comet = 1,
BloodMoon = 5,
SolarEclipse = 2.5,
SlimeRain = 2.5,
RandomBoss = 1,
})
local results = {}
local total = 1e3 -- Increase for more precision, but more computing time
for i = 1, total do
local item = chance:Run()
if item then
results[item] = (results[item] or 0) + 1
end
end
for i, v in pairs(results) do
local chance = v / total * 100
warn(`{i}: {math.floor(chance * 100) / 100}%`)
end
--[[ Output (total = 1e5):
SolarEclipse: 2.51%
SlimeRain: 2.49%
RandomBoss: 0.99%
BloodMoon: 5.01%
Comet: 0.99%
]]
This is a long one, but don’t worry. Half the code here just tests the precision of the event odds. It will get the result item 10^4 times and prints out the percent of times they each happened. You can use this same logic to test any use case!
As you may have noticed, these odds do not add up to 100%, but it works. It works because nil
fills the gaps automatically.
Luck Logic
Precondition: #result > 2
The most common item is the item with the highest chance. Rare items are ones whose chance is not the most common or second to most common. Rare items will steal some chance from the most common, not being able to steal more than common.Chance / 3
among all the rare items. Individually, the maximum one can take is common.Chance / 3 / amountOfRares
. This is so the luck is distributed nicely. If there is too much luck, all rare items could cap out and higher luck will not have an effect! This system ensures that the sum of all odds is the same after the luck modifies the chances.
Precondition: #result == 2
The rarest item takes some chance out of the most common item. The luck modifier doesn’t directly multiply the rarest item’s odds, but it helps to increase it.
Precondition: #result == 1
The only item is returned.
Thoughout all these conditions, if odds don’t add up to 100%, there is still a chance for nil
to be a result.
API
Super.new(items: {[T]: number}, luck: number?): Chance
Returns a random key based on each key’s odds.
items: A dictionary of items, where the values ideally add up to 100%
luck:(default = 0)
A modifier that skews the chances based on the luck logic.
Super.fromResult(result: {[any]: number}): Chance
Returns a new generator whose chances for each item is their value divided by the sum of all values. They will always add to 100%.
result: The dictionary containing the count for items
Super:Run(chance: number): boolean
Returns true
chance
% of the time.
chance: A number between 0 and 100.
type ChanceSlot
Min:
number
Max:number
Value:number
Chance:number
Item:any
Chance:Run(): any
Returns a random item based on their odds.
Chance:SetLuck(luck: number)
Set the generator’s luck modifier.
luck: The luck modifier
Chance:GetItems(luck: number?): {ChanceSlot}
Returns the ordered list of items. These items are modified by luck.
luck:(number)
Overrides the luck setting without changing it
luck:(nil)
Uses the current luck setting
Conclusion
If you find any bugs, I will try and fix them. Just provide a code segment that causes the bug!
Any typos on this topic or in the code? That’s bad! DM about those so I can fix them.
I hope you can find this module useful!