I created this quick visualizer for looking into the math.random() function to see how random it is, after I saw someone doubting its statistical reliability:
Each of the bars represent one integer: 1 as the leftmost bar, to the max range as the rightmost (default 100). The height of each bar represents that integer’s frequency relative to the other integers. There is a horizontal bar that shows the expected statistical frequency, or the maximum frequency, depending on the mode. It also displays the standard deviation of the frequencies between the random integers (0 meaning that every integer has the same frequency in the data):
(Yes I really did 12 billion numbers)
It comes with a few options, too!
You can change what range of random integers are generated and how many of them are generated for the data per frame, from 1 to infinity (or however many your ram can handle): This lets you create millions and billions or more random integers! Depending, of course, on your computer’s processing power. There are no checks here, so you can even go to the point where roblox will crash if you’d want to. Since the data is stored in a fixed-length dictionary, there is negligible performance drop for every random sample stored.
You can also change the scale type of the bars, whether they are scaled relative to the statistical value or the maximum frequency/mode of the number set.
The horizontal bar is toggleable and can be changed to “laser mode” which makes its top surface equal to the exact expected/maximum value, and would cause z-fighting to show if an integer’s frequency is at the exact desired value:
The visual bars are toggleable to reduce cpu impact (since the scale calculations would be removed)
Here is a video of me using it:
The bar positions and sizes are recalculated whenever they are scaled to eliminate drift caused by floating point error. It works on any device (probably except console) and is very performant: scaling with your desired numbers per frame, my pc can handle 6 million per frame at ~1.4 fps (8.4M nums per second) to 110k at ~60 fps (6.6M nums per second).
Source code:
local RunService = game:GetService("RunService")
local player = game:GetService("Players").LocalPlayer
local numberUI, stDevUI, resetButton, stepBox, sampleBox, scaleButton, barButton, laserButton, expectedBar, toggleBarsButton, laserBar
local scale = "Expected"
local amt = 500
local upper = 100
local tab = {}
local physParts = {}
local currParts = {}
local connections = {}
local expected = 1/100
local scaleStat = 10 --What number the back/stat bars should scale to
local scaleFrame = 5 --What number the front / frame bars should scale to
local visualBars = true
local function createLaserBar() --Creates a laser-thin red bar that will z-fight bars at the expected value (for skeptics)
expectedBar.Color = Color3.new(1, 0.101961, 0.219608)
expectedBar.Position = Vector3.new(-35, upper*expected*scaleStat - 0.025, 1)
expectedBar.Size = Vector3.new(0.2*2 + 1,0.05, upper + .4)
end
function createExpectedBar() --Was originally only meant for the "expected" scale, but works perfectly for "maximum" too
if not expectedBar then
expectedBar = Instance.new("Part") --Create if doesnt exist
expectedBar.Transparency = 0.6
expectedBar.Parent = workspace.DebugParts
end
expectedBar.Anchored = true
expectedBar.CanCollide = false
expectedBar.Position = Vector3.new(-35, upper*expected*10, 1)
expectedBar.Size = Vector3.new(0.2*2 + 1,0.3, upper + .4)
expectedBar.Material = Enum.Material.SmoothPlastic
expectedBar.TopSurface = "Smooth"
expectedBar.BottomSurface = "Smooth"
expectedBar.Color = Color3.new(0.772549, 0.772549, 0.772549)
if laserBar then createLaserBar() end
end
local function toggleLaserBar()
if expectedBar then
laserBar = not laserBar
if laserBar then
createLaserBar()
else
createExpectedBar()
end
end
end
local function toggleBar()
if expectedBar then
if expectedBar.Transparency ~= 1 then
expectedBar.Transparency = 1
else
expectedBar.Transparency = 0.6
end
end
end
local function toggleScale()
if scale == "Expected" then
scale = "Maximum"
else
scale = "Expected"
end
scaleButton.Text = "Scale: " .. scale
end
local function toggleBars()
visualBars = not visualBars
if not visualBars then
for i, v in ipairs(physParts) do
v.Transparency = 1
end
for i, v in ipairs(currParts) do
v.Transparency = 1
end
--(The scale function takes care of making them 0 transparency again)
end
end
local function setUI()
local numGui = player.PlayerGui:WaitForChild("NumGui")
numberUI = numGui.NumberFrame.Number
stDevUI = numGui.NumberFrame.StDev
resetButton = numGui.CenterFrame.ResetButton
stepBox = numGui.Interactor.StepBox
sampleBox = numGui.Interactor.SampleBox
scaleButton = numGui.CenterFrame.ScaleButton
barButton = workspace.Billboard2.SurfacePart.BackGui.BackFrame.BarButton
laserButton = workspace.Billboard2.SurfacePart.BackGui.BackFrame.LaserButton
toggleBarsButton = workspace.Billboard1.SurfacePart.BackGui.BackFrame.BarsButton
table.insert(connections, resetButton.MouseButton1Click:Connect(reset))
table.insert(connections, scaleButton.MouseButton1Click:Connect(toggleScale))
table.insert(connections, barButton.MouseButton1Click:Connect(toggleBar))
table.insert(connections, laserButton.MouseButton1Click:Connect(toggleLaserBar))
table.insert(connections, toggleBarsButton.MouseButton1Click:Connect(toggleBars))
end
local function scalePartVert(part, newVertSize)
if newVertSize < 0.05 then part.Transparency = 1 part.CanCollide = false return end
if part.Transparency ~= 0 then part.Transparency = 0 part.CanCollide = true end --Send thru if statement instead of updating each frame
part.Position = Vector3.new(part.Position.X, (newVertSize/2) ,part.Position.Z) --Updated to calc Y pos to mitigate floating point error
part.Size = Vector3.new(part.Size.X, newVertSize, part.Size.Z)
end
local function getAverages(inTab)
local retTab = {}
local total = inTab.Total
for key, value in pairs(inTab) do
if tonumber(key) then
retTab[key] = (value / total) * 100
end
end
return retTab
end
local function tableAverage(inTab)
local avg = 0
for key, value in pairs(inTab) do
avg += value
end
avg /= #inTab
return avg
end
local function getStDev(inTab)
local stDev
local avg = tableAverage(inTab)
local sum = 0
for key, value in pairs(inTab) do
sum += ((value - avg)^2)
end
stDev = math.sqrt(sum/#inTab)
return stDev
end
local function destroy() --Destroys parts and pauses simulation
for key, value in pairs(physParts) do value:Destroy() end
for key, value in pairs(currParts) do value:Destroy() end
amt = 0
end
local function rng()
local amtInput = tonumber(stepBox.Text) --Could just do these on events with the input box but whatever
if amtInput and math.floor(amtInput) ~= amt then amt = amtInput end
local sampleInput = tonumber(sampleBox.Text)
if sampleInput and math.floor(sampleInput) ~= 0 and math.floor(sampleInput) ~= upper then reset() end --Must reset if sample range is changed since it messes w/ statistics
local max = 0
local currFreq = {}
for i = 1, upper do currFreq[i] = 0 end
for i = 1, amt do
local res = math.random(1,upper)
tab[res] += 1 --Add to the frequency of that element
currFreq[res] += 1
tab.Total += 1
if tab[res] > tab.Max then tab.Max = tab[res] end
if currFreq[res] > max then max = currFreq[res] end
end
numberUI.Text = tab.Total
local chanceTab = getAverages(tab)
stDevUI.Text = "StDev: " .. getStDev(chanceTab)
if visualBars then --Only scale parts if you want to visualize them (duh)
if max ~= 0 then --Don't divide by zero lmao
for i = 1, upper do
scalePartVert(currParts[i], scaleFrame*(currFreq[i]/ max)) --Scale the current random list relative to its max value
end --(scaling by % of amt is very ugly when amt < upper)
end
for key, value in pairs(chanceTab) do
if scale == "Expected" then
scalePartVert(physParts[key], value / (expected*scaleStat)) --"Expected" scaler, based on what the freq should be
else
scalePartVert(physParts[key], value * (tab.Total/tab.Max)/scaleStat) -- "Relative" scaler, based on the max value
end
end
end
end
function reset()
visualBars = true --Make the bars visual on reset
for i, v in ipairs(connections) do --Disconnect the character-specific events
v:Disconnect()
connections[i] = nil
end
setUI() --Reset UI (Needs to be done if player respawns)
numberUI.Text = 0 --Reset texts
stDevUI.Text = 0
scaleButton.Text = "Scale: " .. scale
destroy()
local amtInput = tonumber(stepBox.Text)
if amtInput and math.floor(amtInput) then amt = amtInput
else amt = 500 end
local sampleInput = tonumber(sampleBox.Text)
if sampleInput and math.floor(sampleInput) ~= 0 and math.floor(sampleInput) ~= upper then upper = sampleInput
else upper = 100 end
expected = 1 / upper --Expected statistical chance for each number
--Reset tables
tab = {}
physParts = {}
currParts = {}
tab.Total = 0
tab.Max = 0
--Set table up
for i = 1, upper do
tab[i] = 0
end
createExpectedBar()
--Create parts
for i = 1, upper do
local part = Instance.new("Part")
part.Size = Vector3.new(1,10,1)
part.Position = Vector3.new(-35, scaleStat/2, i - upper/2 + 0.5)
part.Anchored = true
part.Transparency = 0
part.Color = Color3.fromHSV(math.random(120,300)/360,.6,math.random(200,255)/255)
part.TopSurface = "Smooth"
part.BottomSurface = "Smooth"
part.Parent = workspace.DebugParts
physParts[i] = part
end
for i = 1, upper do
local part = Instance.new("Part")
part.Size = Vector3.new(1,5,1)
part.Position = Vector3.new(-30, scaleFrame/2, i - upper/2 + 0.5)
part.Anchored = true
part.Transparency = 0
part.Color = physParts[i].Color
part.TopSurface = "Smooth"
part.BottomSurface = "Smooth"
part.Parent = workspace.DebugParts
currParts[i] = part
end
table.insert(connections, RunService.RenderStepped:Connect(rng))
end
reset()
player.CharacterRemoving:Connect(destroy)
player.CharacterAdded:Connect(reset)