2 issues with my race rerolling system in my game

So my game has a system where you spawn with a “race” that gets abilities. You get a random race when you spawn and some are more rare than others. You can then purchase race rerolls in the game to change your race however I have 2 issues.

Problem 1:

First of all, this is the race chances table

local races = { -- ["Race"] = Chance (%)
	["Corvus"] = 2,
	["Jinn"] = 2,
	["Aquaran"] = 4,
	["Hollow"] = 4,
	["Aurorian"] = 5,
	["Phantom"] = 6,
	["Cactacae"] = 7,
	["Dulask"] = 10,
	["Colubran"] = 11,
	["Golem"] = 14,
	["Magus"] = 17,
	["Mimiga"] = 18,
}

return races

It’s obviously a module script that gets the race chances.
So when you reroll one problem occurs. What if you already own that race? Well to solve this issue I simply get your current race and get its % then split it and add it to the other race chances and remove your current race from the pool.

This is the code to roll your race and it also splits the race chances like I mentioned as you can see:

local RS = game:GetService("ReplicatedStorage")
local RaceChances = require(RS.Modules:WaitForChild("RaceChances"))

local function deepCopy(original)
	local copy = {}
	for k, v in pairs(original) do
		if type(v) == "table" then
			v = deepCopy(v)
		end
		copy[k] = v
	end
	return copy
end

local function rollRace(plr)
	local chances = deepCopy(RaceChances)
	
	if plr and chances[plr.data.Race.Value] then
		local currentRaceChance = chances[plr.data.Race.Value]
		chances[plr.data.Race.Value] = nil
		
		local totalRaces = 0
		
		for _,_ in pairs(chances) do
			totalRaces += 1
		end
		
		for index,_ in pairs(chances) do
			chances[index] += currentRaceChance/totalRaces
		end
		
		print(chances)
		print("UPDATED RACE CHANCES")
	end
	
	local total = 0
	local random = math.random(1,100)
	
	local result = nil
	
	for race,chance in pairs(chances) do
		total += chance
		if random <= total and result == nil then
			result = race
		end
	end
	
	if total ~= 100 then
		warn("RACE CHANCES DONT ADD UP TO 100, ADDS UP TO: "..tostring(total))
		print(chances)
	end
	
	return result
end

return rollRace

As per ROBLOXs guidelines I am required to show race percentages. There is no issue with this except when the race chances get split with your current race it creates big decimals. Example:

image

I obviously cant just round them otherwise I’d technically be breaking roblox guidelines since they aren’t accurate. I also don’t want to do that personally even if it were legal. How can I make it so the % split doesnt create large decimals? Aiming to have single decimals or double digit decimals at the most.

Thats problem 1

Problem 2:
This also has to do with the current race being split
When it splits, and only SOMETIMES with certain races I get this warning which I made:
image
image
All of the race chances wont add up to exactly 100 and sometimes will be subtracted VERY slightly or added onto VERY slightly. Why is this and how can I fix it?

All help is greatly appreciated thank you in advance for any help given!

3 Likes

Problem 1: To avoid large decimals when splitting the race chances, you can limit the decimal places by multiplying the chances by a factor (e.g., 10 or 100), rounding them, and then dividing them back by the factor. This way, the result will have fewer decimal places while maintaining the overall chances’ distribution.

Here’s an example of how you can modify the code within the rollRace function:

local factor = 100
for index, _ in pairs(chances) do
    local modifiedChance = chances[index] * factor
    local roundedChance = math.round(modifiedChance + (currentRaceChance * factor / totalRaces))
    chances[index] = roundedChance / factor
end

This will limit the decimals to two places, while still maintaining the overall distribution of race chances.

Problem 2: The issue with the race chances not adding up to exactly 100 could be due to floating-point arithmetic inaccuracies when dealing with decimals. To solve this issue, you can adjust the remaining difference after the loop.

Here’s how you can modify the code in the rollRace function to ensure the total equals 100:

local totalChances = 0
for _, chance in pairs(chances) do
    totalChances += chance
end

local difference = 100 - totalChances
local lastRace = next(chances, next(chances))
chances[lastRace] = chances[lastRace] + difference

This will add or subtract the difference to the last race in the chances table, ensuring that the total sums up to 100. This adjustment is minimal and should not significantly impact the overall distribution of race chances.

1 Like

Wow this was an amazing solution thank you so much!

There is one issue. When the difference gets added onto the last race it becomes a large decimal again.

Also the issue that stops them from adding up to 99.99999999 or 100.0000001 doesn’t work unfortunately. I still get the warning.

Edit: ^ this issue is caused because it actually makes one of the chances negative
image

Edit 2: This is because when u split the race chances it actually increases the total chance making the difference a lot bigger.

We can address the large decimal issue by applying the rounding technique to the difference as well. Also, to prevent negative chances and ensure the total sums up to 100, we can distribute the difference among all races, proportionally.

Here’s a modified version of the rollRace function to address these issues:

local function rollRace(plr)
    local chances = deepCopy(RaceChances)

    if plr and chances[plr.data.Race.Value] then
        local currentRaceChance = chances[plr.data.Race.Value]
        chances[plr.data.Race.Value] = nil

        local totalRaces = 0

        for _, _ in pairs(chances) do
            totalRaces += 1
        end

        local factor = 100
        local totalWeight = 0
        for index, _ in pairs(chances) do
            local modifiedChance = chances[index] * factor
            local roundedChance = math.round(modifiedChance + (currentRaceChance * factor / totalRaces))
            chances[index] = roundedChance / factor
            totalWeight = totalWeight + chances[index]
        end

        local difference = 100 - totalWeight
        for index, _ in pairs(chances) do
            local proportion = chances[index] / totalWeight
            chances[index] = chances[index] + (difference * proportion)
            chances[index] = math.floor(chances[index] * factor + 0.5) / factor  -- Round it to avoid large decimals
        end
    end

    local total = 0
    local random = math.random(1, 100)

    local result = nil

    for race, chance in pairs(chances) do
        total += chance
        if random <= total and result == nil then
            result = race
        end
    end

    if total ~= 100 then
        warn("RACE CHANCES DONT ADD UP TO 100, ADDS UP TO: " .. tostring(total))
        print(chances)
    end

    return result
end
1 Like

copying the exact function, I get this error still (but less precise)
image

the same problem still occurs as well with splitting the percents where one race will have a large decimal (not always though)
image

if the chances being 100.00000000001 is a floating error doesnt that mean my chances aren’t actually being messed up? Its just the result of all of them being added together with the floating error therefore making the %s still accurate.

is there a way I can just divide the race percents evenly to make sure they all have at most 2 digit decimals? The code prior actually increases the total race pool chances so I cannot just use that.

You are correct that the floating-point error doesn’t affect the actual distribution of chances; it’s just an artifact of the floating-point arithmetic. However, if you want to ensure that each chance has at most two decimal places, you can apply the rounding technique to each chance after distributing the difference.

Here’s a modified version of the rollRace function that ensures each chance has at most two decimal places:

local function rollRace(plr)
    local chances = deepCopy(RaceChances)

    if plr and chances[plr.data.Race.Value] then
        local currentRaceChance = chances[plr.data.Race.Value]
        chances[plr.data.Race.Value] = nil

        local totalRaces = 0

        for _, _ in pairs(chances) do
            totalRaces += 1
        end

        local factor = 100
        local totalWeight = 0
        for index, _ in pairs(chances) do
            local modifiedChance = chances[index] * factor
            local roundedChance = math.round(modifiedChance + (currentRaceChance * factor / totalRaces))
            chances[index] = roundedChance / factor
            totalWeight = totalWeight + chances[index]
        end

        local difference = 100 - totalWeight
        for index, _ in pairs(chances) do
            local proportion = chances[index] / totalWeight
            chances[index] = chances[index] + (difference * proportion)
            chances[index] = math.floor(chances[index] * factor + 0.5) / factor  -- Round it to avoid large decimals
        end
    end

    local total = 0
    local random = math.random(1, 100)

    local result = nil

    for race, chance in pairs(chances) do
        total += chance
        if random <= total and result == nil then
            result = race
        end
    end

    if total ~= 100 then
        warn("RACE CHANCES DONT ADD UP TO 100, ADDS UP TO: " .. tostring(total))
        print(chances)
    end

    return result
end

The total may not be exactly 100 due to rounding, but the distribution should still be accurate.

1 Like

If this is true mabye just try editing the value of the chance to get each race. Im still not sure why this problem is happening or if there is something built in to fix it.

1 Like

This is actually an issue because sometimes the total chance adds up to 99.99 and when math.random returns 100 it will return nil.

Oh that makes more sense. Did the last script I gave you work? If it didn’t does it give the same error or a different one?

1 Like

The chances wouldn’t add up to 100 and it wasnt because of the floating error since it would return a number like 99.99000000001 as opposed to 99.9999999999999 that the floating error would’ve returned.

Can actually fix the returning nil error by doing this
image
But obviously theres still an issue with dividing the race %s to create double digit decimals.

Ok here is some pretty terrible code but it always adds up to 100

local function rollRace(plr)
    local chances = deepCopy(RaceChances)

    if plr and chances[plr.data.Race.Value] then
        local currentRaceChance = chances[plr.data.Race.Value]
        chances[plr.data.Race.Value] = nil

        local totalRaces = 0

        for _, _ in pairs(chances) do
            totalRaces += 1
        end

        local factor = 100
        local totalWeight = 0
        for index, _ in pairs(chances) do
            local modifiedChance = chances[index] * factor
            local roundedChance = math.round(modifiedChance + (currentRaceChance * factor / totalRaces))
            chances[index] = roundedChance / factor
            totalWeight = totalWeight + chances[index]
        end

        local difference = factor - totalWeight
        local remainingDifference = difference
        for index, _ in pairs(chances) do
            local proportion = chances[index] / totalWeight
            local adjustment = math.floor(difference * proportion * factor + 0.5) / factor
            chances[index] = chances[index] + adjustment
            remainingDifference = remainingDifference - adjustment
        end

        -- Distribute any remaining difference to the first race
        for _, value in pairs(chances) do
            chances[_] = chances[_] + remainingDifference
            break
        end
    end

    local total = 0
    local random = math.random(1, 100)

    local result = nil

    for race, chance in pairs(chances) do
        total += chance
        if random <= total and result == nil then
            result = race
        end
    end

    if total ~= 100 then
        warn("RACE CHANCES DONT ADD UP TO 100, ADDS UP TO: " .. tostring(total))
        print(chances)
    end

    return result
end
1 Like

image
Not all of the races are double decimals.

Also got these results.
image
The last one is obviously a float but the first one ends with 00003 which makes it a non-float if I understand correctly. (Maybe im wrong, just assuming the float difference is like 0.00000000000001 always)

Also appreciate you continuing to help :pray:
What if we somehow had a way to always divide the currentRaceChance in a way that wouldn’t produce decimals longer than 2 digits? Could we possibly predict when longer decimals will be the result of the division and cut it up into uneven pieces. I don’t mind them being uneven as long the difference is negligible or very small. (like 0.5% or less)

local function rollRace(plr)
    local chances = deepCopy(RaceChances)

    if plr and chances[plr.data.Race.Value] then
        local currentRaceChance = chances[plr.data.Race.Value]
        chances[plr.data.Race.Value] = nil

        local totalRaces = 0

        for _, _ in pairs(chances) do
            totalRaces += 1
        end

        local factor = 100
        local maxDifference = 0.5 / factor
        local totalWeight = 0

        -- Divide the currentRaceChance into smaller pieces
        local dividedChance = currentRaceChance / totalRaces
        while dividedChance > maxDifference do
            dividedChance = dividedChance / 2
        end

        for index, _ in pairs(chances) do
            local modifiedChance = chances[index] * factor
            local roundedChance = math.round(modifiedChance + (dividedChance * factor * totalRaces))
            chances[index] = roundedChance / factor
            totalWeight = totalWeight + chances[index]
        end

        local difference = factor - totalWeight
        local remainingDifference = difference
        for index, _ in pairs(chances) do
            local proportion = chances[index] / totalWeight
            local adjustment = math.floor(difference * proportion * factor + 0.5) / factor
            chances[index] = chances[index] + adjustment
            remainingDifference = remainingDifference - adjustment
        end

        -- Distribute any remaining difference to the first race
        for _, value in pairs(chances) do
            chances[_] = chances[_] + remainingDifference
            break
        end
    end

    local total = 0
    local random = math.random(1, 100)

    local result = nil

    for race, chance in pairs(chances) do
        total += chance
        if random <= total and result == nil then
            result = race
        end
    end

    if total ~= 100 then
        warn("RACE CHANCES DONT ADD UP TO 100, ADDS UP TO: " .. tostring(total))
        print(chances)
    end

    return result
end

This function now divides the currentRaceChance into smaller pieces with a maximum difference of 0.5% or less before distributing it among the other races. This should help prevent decimals longer than two digits as well as ensure that the chances always add up to 100.

1 Like

This actually makes it so most chances add up to 100 (outside of floats which obviously dont matter but even floats are rare now)

Only downside is that it does not fix the issue:
image

And in some cases 2 races can have large decimals.
image

Close to a solution though (hopefully?)

Very close. Ill try more tommorow

1 Like

Ok in this function I round then make sure chances add up to 100.

local function rollRace(plr)
    local chances = deepCopy(RaceChances)

    if plr and chances[plr.data.Race.Value] then
        local currentRaceChance = chances[plr.data.Race.Value]
        chances[plr.data.Race.Value] = nil

        local totalRaces = 0

        for _, _ in pairs(chances) do
            totalRaces += 1
        end

        local totalWeight = 0

        for index, _ in pairs(chances) do
            local adjustedChance = chances[index] + (currentRaceChance / totalRaces)
            chances[index] = math.floor(adjustedChance * 100 + 0.5) / 100
            totalWeight = totalWeight + chances[index]
        end

        local difference = 100 - totalWeight
        local remainingDifference = difference
        for index, _ in pairs(chances) do
            local proportion = chances[index] / totalWeight
            local adjustment = math.floor(difference * proportion * 100 + 0.5) / 100
            chances[index] = chances[index] + adjustment
            remainingDifference = remainingDifference - adjustment
        end

        -- Distribute any remaining difference to the first race
        for _, value in pairs(chances) do
            chances[_] = chances[_] + remainingDifference
            break
        end
    end

    local total = 0
    local random = math.random(1, 100)

    local result = nil

    for race, chance in pairs(chances) do
        total += chance
        if random <= total and result == nil then
            result = race
        end
    end

    if total ~= 100 then
        warn("RACE CHANCES DONT ADD UP TO 100, ADDS UP TO: " .. tostring(total))
        print(chances)
    end

    return result
end
1 Like