Chance (easy random items + luck modifiers)!

Chance


By @avodey (my main account)!

:package: 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:

image

And here is the result:

image

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.

:warning: 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. :pleading_face:

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!

:+1:

55 Likes

Pretty cool module, idk what to say xd

3 Likes

It is a good module but next time spell asset A-S-S-E-T

Thanks : )

1 Like

Huh? That is not a typo. I made an Assert module a while ago, and this uses that.

4 Likes

I tried making a system like this but it always made the most uncommon this the most common, thanks!

1 Like

Very nice module, I’m a huge fan of procedural stuff using luck/chance and this will be extremely helpful for my project (which also happens to be a roguelike, lol)
10/10 module, will be using

1 Like

Chance (v1.0.1)!


  • Added Super.fromResult which takes in a dictionary of numbers, where the value is the amount of index there is. It returns a generator whose chances are equal to v/total. They will always add up to 100%. You can use this to test the precision of a generator.
  • Added luck: (number?) parameter to the GetItems function. The default value is the generator’s set luck. You can use this parameter to get the chances of a different luck value without changing anything.
  • Default luck is now 0, used to be 1
  • The generator seed is now the generator id, plus a constant. The constant is os.clock() * 256 to ensure that every generation in different. Seeds are floored so running multiple times in a second used to output the same results in that second. This constant offset stops that!
1 Like

The “Murder Mystery” example is funny because it just proves my theory that you and avodey are the same person, and the fact that I am in the people list [cough cough @VSCPlays]

also this is a good module, I was about to give a suggestion about adding a constructor which creates a new Chance object with every item having an equal chance [100 / #items] but I noticed that Super.fromResult does the job, also what does [any]: number mean in the Super.fromResult constructor? and how did you do that type of OOP? using Super as the base constructor and Chance as the class.

I found that separating the table that creates the objects, and the table that holds their methods was cleaner for me. I haven’t seen anybody else do this, but then again, I don’t look inside many scripts that aren’t mine.

The randomly generated item can be anything apart from nil!

1 Like

I get about the [any] part, but i want the number part

Clean and Unique.

Oh. The number is how many of the item were generated. It will do the math and will convert them into chances that add up to 100%. For example:

Chance.fromResult({
    Apple = 50,
    Banana = 150,
})

Apple would be a 25% chance, while banana would be a 75% chance. That’s because 50/(50+150) is 0.25, and 150/(50+150) is 0.75.

Basically meaning if I do all to 1, it will be 100 / #items?

Yes! I made this function because I thought people might need it for testing the luck system. I know it’s not perfect, and I’m looking to improve it with more complicated math. :smiling_imp:

My alt account was suspended for a day due to something I said a week ago. :roll_eyes:

1 Like

I’m not that advanced of a scripter. So I was wondering what is the minimal scripting I would have to do just for a loot box system? I would assume I only need


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

That’s about it, yes.

This method will print the generated item, so you can use this any way you want. The item doesn’t have to be a string, either. It could be anything apart from nil, like a table!

2 Likes

Hey! This is a really neat module. Just have a question though, could you input a dictionary with values that equal LESS than 100? I know more than 100 isn’t an option, but if I wanted to have lots of extremely low results, I wouldn’t want to have to account for that, haha.

Yes. If the chances add up to less than 100%, there is a chance the result could be nil. You could account for this by using the or operator:

print(generator:Run()) --> nil
print(generator:Run() or "Common") --> Common

Good idea! Thanks for that. I’ll be using this soon then.

What are the chances? I was literally about to make a custom chance module, but this just saved me the time and effort of creating my own. Thanks!

1 Like

How would I use a better way to sort of garuntee a rare item after lets say, 20 runs?