Math.random() visualizer + source code


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

Screenshot_456
(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:

Screenshot_457
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)


quick lua tip, if you wanna declare a bunch of variables set to nil you could just do

local a,b,c

instead of

local a
local b
local c
1 Like
local RunService = game:GetService("RunService")
local player = game:GetService("Players").LocalPlayer

local numberUI, stDevUI, resetButton, stepBox, sampleBox, scaleButton, barButton, laserButton, expectedBar, laserBar, toggleBarsButton

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

this is how I would fix that up

1 Like

Thanks, initially they were defined but I had to move it to a setter function to deal with player respawning.

Okay, I’m pretty sure that game just broke my pc. Anyways, nice job!

1 Like