Math.random() returns same number every time after a delay from setting randomseed

So, I do the following:

  1. Set randomseed to os.time()
  2. Call math.random() 157 times to make sure the RNG heats up properly. I print math.random() and its different every time as expected.
  3. There is a delay when I do some datastore update
  4. 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)

In script, run place in Test->Start server

4 Likes

If you don’t mind a short (PostAsync) delay whilst fetching a random number, you could use the method I posted here to achieve (almost) truly random numbers everytime without math.randomseed affecting it: http://devforum.roblox.com/t/better-results-from-random-numbers/25133/14?u=vaeb

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.

1 Like

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

This prints different values every time for me.

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

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.

If I print it continuously in a script I get this:

0.3502914517655 0.89596240119633 0.82284005249184
0.80874050111393 0.58500930814539 0.47987304300058
0.3502914517655 0.89596240119633 0.82284005249184
0.80874050111393 0.58500930814539 0.47987304300058
0.80874050111393 0.58500930814539 0.47987304300058
0.4109317300943 0.016724143192846 0.55265968810083
0.3502914517655 0.89596240119633 0.82284005249184
0.4109317300943 0.016724143192846 0.55265968810083
0.7466048158208 0.17410809656056 0.85894344920194
0.7466048158208 0.17410809656056 0.85894344920194
0.4109317300943 0.016724143192846 0.55265968810083
0.77895443586535 0.41578417310099 0.60463270973846
0.3502914517655 0.89596240119633 0.82284005249184
0.4109317300943 0.016724143192846 0.55265968810083
0.4109317300943 0.016724143192846 0.55265968810083
0.77895443586535 0.41578417310099 0.60463270973846
0.4109317300943 0.016724143192846 0.55265968810083

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:

0.3502914517655 0.89596240119633 0.82284005249184
0.4109317300943 0.016724143192846 0.55265968810083
0.7466048158208 0.17410809656056 0.85894344920194
0.77895443586535 0.41578417310099 0.60463270973846
0.80874050111393 0.58500930814539 0.47987304300058

Yeah I noticed that weird repetition as well.

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)

math.randomseed() is global, but I do not know of any ROBLOX scripts that would set it to a constant.

It’s the same for all LocalScripts only I thought, I think CoreScripts have their own math environment.

Okay I also get that number for the same code in Test Server, that’s weird.

EDIT: although sometimes it is a different number, but a majority of the time it’s 0.0012512588885159

There is now reproduction instructions in main post, in case you didnt notice already

Great find.

Any estimate on when this bug might be fixed? (just making sure its at least on some bug list somewhere…)

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

  1. 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.

  2. 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().

  3. 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:

return ((result = result * 214013 + 2531011) >> 16) & 32767

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.

13 Likes

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.

1 Like

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

Wait, does that mean the seed for math.random is always 1 on windows unless you use math.randomseed in the same thread?

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.