Call math.random() 157 times to make sure the RNG heats up properly. I print math.random() and its different every time as expected.
There is a delay when I do some datastore update
Call math.random() and print it. It prints the same number every time.
I only have 3 short scripts and there is only one instance of math.randomseed() (confirmed with ctrl+f just to be 100% certain)
The only reason I can think of this happening, is if randomseed is global (It is, right?), and roblox is in some hidden script setting randomseed to the same constant every time, after a slight delay (overriding my own call to randomseed)
The effect is that I get the exact same results for all my randomness every timeâŚ
edit:
TO REPRODUCE:
wait(1)
math.randomseed(os.time())
for i=1,157 do
local a=math.random()
if i<10 then
print(a) --should be randomish because just set seed (OK)
end
end
wait(6)
print("delayed:")
print(math.random()) --same every time you run the server (NOT OK)
Thatâs a strange finding. I havenât had this issue myself, but Iâve run into other issues with math.random which prompted me to make a âmore randomâ random function.
I do believe math.randomseed is global, so thereâs a ton of things that could have an effect on the number sequence youâre getting.
The function I wrote is this:
function random(min,max)
if (min == nil) then min = 0 end
if (max == nil) then max = 1 end
if (min == max) then return min end
local interval = max-min
local rnd = tick() * math.random()
return min + (rnd % interval)
end
The only difference to math.random() is that it wonât return integers, but simply some random number between min and max. You may have to wrap it with math.round to get integers if thatâs what you need.
The random seed is not shared between game scripts and core scripts, so that cannot be the issue unless you are doing it yourself in one of your scripts.
I also tried to replicate this but I could not get the same value each time. Are you sure you are waiting long enough before setting math.randomseed each time? I think the seed is rounded down somewhere in math.randomseed and os.time() changes only once per second, so you need to wait at least a second before calling math.randomseed(os.time()) again.
Script I used for testing:
-- Seed is current time:
local seed = os.time()
math.randomseed(seed)
-- Get some random numbers:
for i=1,157 do
local _ = math.random()
end
-- Small delay:
wait()
-- Print seed and first random number after delay:
print("Seed:", seed)
print("First random number after delay:", math.random())
I checked each script for calls to math.randomseed() so it cant be one of mine either.
I do get random numbers initially (after the randomseed call), but that only seems to be a temporary state
I tried moving the seed setting to AFTER the initial delay, thinking the seed might be getting reset during that somehow, but then it gets reset after ANOTHER delay, so Im thinking its either a loop calling randomseed somewhere, or just multiple staggered calls.
Yeah Im giving it enough time, as I only call randomseed when server starts, once, and theres multiple seconds of difference there (I even printed the time to make sure)
Try this yourself:
math.randomseed(124524) wait() print(math.random(), math.random(), math.random())
The first is always the same as âexpectedâ. But the ones after are different every call (FOR THE SAME SEED).
This could just be something about the commandline though, Ill try in script.
Itâs true that the delay apparently affects the number sequence that you get, but if you compare the rows of this output, then the first, second and third number always occur in the same way in sequence. These are the unique lines in my output:
But anyways, I couldnt reproduce in studio play, but I could reproduce in START SERVER:
wait(1)
math.randomseed(os.time())
for i=1,157 do
local a=math.random()
if i<10 then
print(a) --should be randomish because just set seed
end
end
wait(6)
print("delayed:")
print(math.random()) --same like every time
(throw in script, start server, repeat a few times)
Look at the number it gives after the delay (the last one), I got something like 0.0125 most of the time. Which is the exact same number I kept getting in my actual place.
(the first set of numbers should be somewhat random, but after the delay it always gives the same number)
Youâre witnessing the behavior of one of the most pathetic C functions, rand().
There is no such thing as math environment - Lua implements math.randomseed/math.random by directly routing them to C stdlib.h rand()+srand() (which is a really poor choice but what can you do).
There are three great implementation decisions that a platform vendor can make when implementing rand()/srand():
Store RNG state as a global, update it in srand() and rand().
The problem is, if you have several threads all calling rand(), the random state gets intermixed and you may see many threads concurrently generating the same random value, which would be a problem if youâre trying to generate independent streams of data.
Store RNG state as a global, update it in srand() and rand() but hold a mutex during the update.
This fixes the state intermixing issue - now all state updates are sequenced so that threads calling rand() get different items from essentially a single random stream.
The problem is, many threads calling rand() end up serializing their execution with a mutex, which reduces the performance of parallel programs that are dumb enough to use rand().
Store RNG state in the thread environment (that is C thread, not Lua thread obviously), update it in srand() and rand().
This fixes the performance issue and a state intermixing issue - now rand() is very fast since it does not require synchronization, and different threads do not step on each otherâs toes so they get different random streams.
The problem is, now you need to call srand() in EVERY thread before using the random number generator since it only updates the state of the current thread!
1 is used by OSX.
2 is used by Linux.
3 is used by Windows.
Now, Lua code in ROBLOX runs using a task scheduler that has N threads (where N is your core count) and schedules work on these threads. It does not have a concept of affinity - it can schedule a job on any core assuming there is no concurrent work that conflicts with it.
This means that calling math.randomseed() only initializes ONE random seed for one of the threads.
In their infinite wisdom, Microsoft has set the default RNG seed to 1. The state update function looks like this:
Which happens to return 41 on the first call. math.random() then returns 41 / 32767 = 0.00125.
Now, I am kind of surprised that youâre hitting this problem on the server - the server should only create 1 thread, so math.randomseed() should reset the only RNG state that is used.
The way around this mess is for us to reimplement math.randomseed()/math.random() to stop using braindead C functions.
The only crucial implementation decision being:
Should random state be local to the Lua VM being used?
Should random state be local to the Lua script being ran?
Should random state be local to the Lua thread being ran?
All of these are possible and not too hard to implement, with obvious behavior implications. I feel like #2 would make the most sense, although I see value in #1 as well. I believe @UristMcSparks may have some opinions on this.
For me the optimal implementation for improved randomness would be a PRNG instance (PRNG.new([seed])) with methods to generate random numbers (floats, integers in range, more can be added later which is good), with convenient default instances available so people dont have to understand what a âPRNGâ is (global, per script, per context, whatever).
Then throw a warning whenever math.random() is called without providing line number hehe. No but seriously, if you add your âcustomâ PRNG instance, how math.random() works doesnt really matter as long as existing functionality isnt broken (and given that it already is pretty broken, thats not much of a constraint is it)
If you decide to just âfixâ the lua generator, the best would probably be for the state to be per lua thread, otherwise setting manual seed is pretty useless (if it can just randomly be interfered with by external side effects), at which point theres nothing to fix in the first place (the seed works fine until you yield). (well I guess the default seed being always the same is annoying - cant you fix this by simply setting the seed to some non-constant when a thread starts, though?)
For me, how it works, is that if I set random seed in a script, then after a wait it gets reset back to 1 (if I understood correctly). Not sure if this is a one-time thing around server start or if its consistent (the repro in first post could be modified to check this)
So if I want seed to work I have to set it, do random things without yielding, and then assume that the seed has changed after I yield (=time passes). So anything that relies on the seed must happen within the same frame from setting it (if I wanted 100% correctness).
Apparently that shouldnt happen online so idk why it works like that.
Thanks for taking the time to write these posts, really interesting.
For myself I would really argue against implementing option 2. Say I have a map generator which uses a bunch of modules, which all call math.random a number of times to generate the map. If my map is seeded and I want it to be the same every time I call it with a particular seed, then it would be convenient if option 1 or 3 were the case, so that I donât have to set the seed myself in every module. (I assume this is what option 2 meant anyway)
I would personally prefer option 1, as this is how I imagined the random state was working up til now. Option 3 is not so appealing to me, it seems like its behaviour would just be confusing and not very useful.
The problem with option 3 is that it limits determinism only to the cases where you:
A) Dont yield at all (so a one-time operation)
or
B) Dont have any other use of randomness (which is a global restriction, destroying scalability, and worse, with no checks to inform you if you accidentally break the implicit rule through even a single call to math.random() somewhere else)
Fine for replacing math.random(), not fine for a PRNG designed in the past decade or two.