Help with Luck and rng

I have been thinking of ways of making a rng system with a luck multiplier. The problem I have had is that whenever the luck factor (luck multiplier) is for example 10, the chance of getting the rarest color (0.00001) becomes very easy to get. When printing the adjustedtable table, something in the middle of rarest and least rare became much harder to get (sometimes even harder than the rarest)

Does anyone have any improvements or ways to make a “system” with luck factor.

local ChanceTable = { --in precentage
	
	{Color = Color3.new(1, 1, 1), Chance = 40};
	{Color = Color3.new(0, 0, 0), Chance = 20};
	{Color = Color3.new(1, 0, 0), Chance = 20};
	{Color = Color3.new(1,1,1), Chance = 10};
	{Color = Color3.new(1, 0.431373, 0.952941), Chance = 5};
	{Color = Color3.new(0.0313725, 1, 0.513725), Chance = 1};
	{Color = Color3.new(1, 0.933333, 0), Chance = 0.00001};
} --I have alot more colors, but this is just an example


local function GetRandomRarity(luckFactor)

	-- Function to adjust chances with luck factor
	local function adjustChancesWithLuck(ChanceTable, luckFactor)
		local adjustedTable = {}
		local totalWeight = 0

		-- Adjust each entry's chance
		for _, entry in ipairs(ChanceTable) do
			local adjustedChance = entry.Chance ^ (1 / luckFactor) 

			table.insert(adjustedTable, {Money = entry.Money, Chance = adjustedChance})

			totalWeight = totalWeight + adjustedChance
		end

		-- Normalize the adjusted chances
		for _, entry in ipairs(adjustedTable) do
			entry.Chance = (entry.Chance / totalWeight) * 100
		end
	
		return adjustedTable
	end

	-- Function to select a reward based on normalized chances
	local function selectReward(ChanceTable)
		local totalChance = 0
		for _, entry in ipairs(ChanceTable) do
			totalChance = totalChance + entry.Chance

		end

		local randomChance = Random.new():NextInteger(1, totalChance)
		local cumulativeChance = 0

		for _, entry in ipairs(ChanceTable) do

			cumulativeChance = cumulativeChance + entry.Chance
			if randomChance <= cumulativeChance then

				return entry
			end
		end
	end

	-- Example usage

	local adjustedChanceTable = adjustChancesWithLuck(ChanceTable, luckFactor)
	local selectedReward = selectReward(adjustedChanceTable)
	return selectedReward
end
local i = 0
while i < 100 do
local RandomPrize = GetRandomRarity(1) 

	print(RandomPrize)
i += 1	
end

I have a WeightedRandom module you can use that has useful built-in functions. You would have to manually give items individual luckInfluences (default of 0). All it does is weight + (weight * (luckInfluence * luckFactor)).

WeightedRandom.lua
--!strict

local WeightedRandom = {}

export type WeightedRandom<I> = typeof(setmetatable({} :: WeightedRandomClass<I>, {} :: WeightedRandomMetatable))

export type WeightedRandomClass<I> = {
	Random: Random,
	GetItems: (self: WeightedRandom<I>) -> {[I]: Item},
	AddItem: (self: WeightedRandom<I>, item: I, weight: number, luckInfluence: number?) -> (),
	RemoveItem: (self: WeightedRandom<I>, item: I) -> (),
	GetWeight: (self: WeightedRandom<I>, item: I, luckFactor: number?) -> number?,
	GetWeights: (self: WeightedRandom<I>, luckFactor: number?) -> {[I]: number},
	GetTotalWeight: (self: WeightedRandom<I>, luckFactor: number?) -> number,
	GetProbability: (self: WeightedRandom<I>, item: I, luckFactor: number?) -> number?,
	GetProbabilities: (self: WeightedRandom<I>, luckFactor: number?) -> {[I]: number},
	Next: (self: WeightedRandom<I>, luckFactor: number?) -> I
}

export type WeightedRandomMetatable = {
	__index: (self: any, index: any) -> any,
	__newindex: (self: any, index: any, value: any) -> (),
	__iter: (self: any) -> (),
	__tostring: (self: any) -> string
}

export type Item = {
	Weight: number,
	LuckInfluence: number
}

local function clone<T>(t: T, deep: boolean?): T
	t = table.clone(t :: any) :: any
	
	if deep == true then
		for index: any, value: any in t :: any do
			if typeof(value) == "table" then
				(t :: any)[index] = clone(value, true)
			end
		end
	end
	
	return t
end

function WeightedRandom.new<I>(random: Random?): WeightedRandom<I>
	local items: {[I]: Item} = {}
	
	local weightedRandomClass: WeightedRandomClass<I> = {
		Random = random or Random.new(),
		GetItems = function(weightedRandom: WeightedRandom<I>): {[I]: Item}
			return clone(items, true)
		end,
		AddItem = function(weightedRandom: WeightedRandom<I>, item: I, weight: number, luckInfluence: number?): ()
			items[item] = {
				Weight = weight,
				LuckInfluence = if luckInfluence ~= nil then luckInfluence else 0
			}
		end,
		RemoveItem = function(weightedRandom: WeightedRandom<I>, item: I): ()
			items[item] = nil
		end,
		GetWeight = function(weightedRandom: WeightedRandom<I>, item: I, luckFactor: number?): number?
			if items[item] ~= nil then
				return items[item].Weight + (items[item].Weight * (items[item].LuckInfluence * (if luckFactor ~= nil then luckFactor else 0)))
			else
				return nil
			end
		end,
		GetWeights = function(weightedRandom: WeightedRandom<I>, luckFactor: number?): {[I]: number}
			local weights: {[I]: number} = {}

			for item: I in items do
				weights[item] = weightedRandom:GetWeight(item, luckFactor) :: number
			end

			return weights
		end,
		GetTotalWeight = function(weightedRandom: WeightedRandom<I>, luckFactor: number?): number
			local totalWeight: number = 0

			for _: I, weight: number in weightedRandom:GetWeights(luckFactor) do
				totalWeight += weight
			end

			return totalWeight
		end,
		GetProbability = function(weightedRandom: WeightedRandom<I>, item: I, luckFactor: number?): number?
			if items[item] ~= nil then
				return (weightedRandom:GetWeight(item, luckFactor) :: number) / weightedRandom:GetTotalWeight(luckFactor)
			else
				return nil
			end
		end,
		GetProbabilities = function(weightedRandom: WeightedRandom<I>, luckFactor: number?): {[I]: number}
			local probabilities: {[I]: number} = {}

			for item: I in weightedRandom:GetWeights(luckFactor) do
				probabilities[item] = weightedRandom:GetProbability(item, luckFactor) :: number
			end

			return probabilities
		end,
		Next = function(weightedRandom: WeightedRandom<I>, luckFactor: number?): I
			local weightedItems: {[I]: number} = weightedRandom:GetWeights(luckFactor)
			local totalWeight: number = weightedRandom:GetTotalWeight(luckFactor)
			local randomWeight: number = weightedRandom.Random:NextNumber() * totalWeight
			local lastItem: I = nil

			for item: I, weight: number in weightedItems do
				randomWeight -= weight
				lastItem = item

				if randomWeight <= 0 then
					return item
				end
			end

			return lastItem
		end,
	}
	
	local weightedRandomMetatable: WeightedRandomMetatable = table.freeze({
		__index = function(self: any, index: any): any
			return weightedRandomClass[index]
		end,
		__newindex = function(self: any, index: any, value: any): ()
			if index == "Random" then
				if typeof(value) ~= "Random" then
					error(`invalid value for index '{index}' (Random expected, got {typeof(value)})`, 2)
				end
			else
				error(`{index} cannot be assigned to`, 2)
			end

			weightedRandomClass[index] = value
		end,
		__iter = function(self: any): ()
			error(`attempted to iterate over a WeightedRandom`, 2)
		end,
		__tostring = function(self: any): string
			return `WeightedRandom`
		end
	})
	
	return table.freeze(setmetatable({}, weightedRandomMetatable)) :: any
end

return WeightedRandom

Usage:

local WeightedRandom = require(Library.Modules.WeightedRandom)
-- if you want to use type declarations
type WeightedRandom<I> = WeightedRandom.WeightedRandom<I>

local world1Pets: WeightedRandom<string> = WeightedRandom.new()

-- AddItem(item: I, weight: number, luckInfluence: number?)
world1Pets:AddItem("Common", 50, 0)
world1Pets:AddItem("Uncommon", 30, 0)
world1Pets:AddItem("Rare", 10, 0)
world1Pets:AddItem("Epic", 5, 1)
world1Pets:AddItem("Legendary", 3, 1)
world1Pets:AddItem("Mythical", 1.5, 2)
world1Pets:AddItem("Secret", 0.5, 4)

-- Next(luckFactor: number?): I
print(`Chosen pet with 0 luckFactor: {world1Pets:Next()}`)

--  GetProbabilities(luckFactor: number?): {[I]: number}
print(world1Pets:GetProbabilities(10))
1 Like