Seeding the random number generator in Javascript
Solution 1
No, it is not possible to seed Math.random()
, but it's fairly easy to write your own generator, or better yet, use an existing one.
Check out: this related question.
Also, see David Bau's blog for more information on seeding.
Solution 2
No, it is not possible to seed Math.random()
. The ECMAScript specification is intentionally vague on the subject, providing no means for seeding nor require that browsers even use the same algorithm. So such a function must be externally provided, which thankfully isn't too difficult.
I've implemented a number of good, short and fast Pseudorandom number generator (PRNG) functions in plain JavaScript. All of them can be seeded and provide high quality numbers. These are not intended for security purposes--if you need a seedable CSPRNG, look into ISAAC.
First of all, take care to initialize your PRNGs properly. To keep things simple, the generators below have no built-in seed generating procedure, but accept one or more 32-bit numbers as the initial seed state of the PRNG. Similar or sparse seeds (e.g. a simple seed of 1 and 2) have low entropy, and can cause correlations or other randomness quality issues, sometimes resulting in the output having similar properties (such as randomly generated levels being similar). To avoid this, it is best practice to initialize PRNGs with a well-distributed, high entropy seed and/or advancing past the first 15 or so numbers.
There are many ways to do this, but here are two methods. Firstly, hash functions are very good at generating seeds from short strings. A good hash function will generate very different results even when two strings are similar, so you don't have to put much thought into the string. Here's an example hash function:
function cyrb128(str) {
let h1 = 1779033703, h2 = 3144134277,
h3 = 1013904242, h4 = 2773480762;
for (let i = 0, k; i < str.length; i++) {
k = str.charCodeAt(i);
h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
}
h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
return [(h1^h2^h3^h4)>>>0, (h2^h1)>>>0, (h3^h1)>>>0, (h4^h1)>>>0];
}
Calling cyrb128
will produce a 128-bit hash value from a string which can be used to seed a PRNG. Here's how you might use it:
// Create cyrb128 state:
var seed = cyrb128("apples");
// Four 32-bit component hashes provide the seed for sfc32.
var rand = sfc32(seed[0], seed[1], seed[2], seed[3]);
// Only one 32-bit component hash is needed for mulberry32.
var rand = mulberry32(seed[0]);
// Obtain sequential random numbers like so:
rand();
rand();
Note: If you want a slightly more robust 128-bit hash, consider MurmurHash3_x86_128, it's more thorough, but intended for use with large arrays.
Alternatively, simply choose some dummy data to pad the seed with, and advance the generator beforehand a few times (12-20 iterations) to mix the initial state thoroughly. This has the benefit of being simpler, and is often used in reference implementations of PRNGs, but it does limit the number of initial states:
var seed = 1337 ^ 0xDEADBEEF; // 32-bit seed with optional XOR value
// Pad seed with Phi, Pi and E.
// https://en.wikipedia.org/wiki/Nothing-up-my-sleeve_number
var rand = sfc32(0x9E3779B9, 0x243F6A88, 0xB7E15162, seed);
for (var i = 0; i < 15; i++) rand();
Note: the output of these PRNG functions produce a positive 32-bit number (0 to 232-1) which is then converted to a floating-point number between 0-1 (0 inclusive, 1 exclusive) equivalent to Math.random()
, if you want random numbers of a specific range, read this article on MDN. If you only want the raw bits, simply remove the final division operation.
JavaScript numbers can only represent whole integers up to 53-bit resolution. And when using bitwise operations, this is reduced to 32. Modern PRNGs in other languages often use 64-bit operations, which require shims when porting to JS that can drastically reduce performance. The algorithms here only use 32-bit operations, as it is directly compatible with JS.
Now, onward to the the generators. (I maintain the full list with references and license info here)
sfc32 (Simple Fast Counter)
sfc32 is part of the PractRand random number testing suite (which it passes of course). sfc32 has a 128-bit state and is very fast in JS.
function sfc32(a, b, c, d) {
return function() {
a >>>= 0; b >>>= 0; c >>>= 0; d >>>= 0;
var t = (a + b) | 0;
a = b ^ b >>> 9;
b = c + (c << 3) | 0;
c = (c << 21 | c >>> 11);
d = d + 1 | 0;
t = t + d | 0;
c = c + t | 0;
return (t >>> 0) / 4294967296;
}
}
You may wonder what the | 0
and >>>= 0
are for. These are essentially 32-bit integer casts, used for performance optimizations. Number
in JS are basically floats, but during bitwise operations, they switch into a 32-bit integer mode. This mode is processed faster by JS interpreters, but any multiplication or addition will cause it to switch back to a float, resulting in a performance hit.
Mulberry32
Mulberry32 is a simple generator with a 32-bit state, but is extremely fast and has good quality randomness (author states it passes all tests of gjrand testing suite and has a full 232 period, but I haven't verified).
function mulberry32(a) {
return function() {
var t = a += 0x6D2B79F5;
t = Math.imul(t ^ t >>> 15, t | 1);
t ^= t + Math.imul(t ^ t >>> 7, t | 61);
return ((t ^ t >>> 14) >>> 0) / 4294967296;
}
}
I would recommend this if you just need a simple but decent PRNG and don't need billions of random numbers (see Birthday problem).
xoshiro128**
As of May 2018, xoshiro128** is the new member of the Xorshift family, by Vigna & Blackman (professor Vigna was also responsible for the Xorshift128+ algorithm powering most Math.random
implementations under the hood). It is the fastest generator that offers a 128-bit state.
function xoshiro128ss(a, b, c, d) {
return function() {
var t = b << 9, r = a * 5; r = (r << 7 | r >>> 25) * 9;
c ^= a; d ^= b;
b ^= c; a ^= d; c ^= t;
d = d << 11 | d >>> 21;
return (r >>> 0) / 4294967296;
}
}
The authors claim it passes randomness tests well (albeit with caveats). Other researchers have pointed out that it fails some tests in TestU01 (particularly LinearComp and BinaryRank). In practice, it should not cause issues when floats are used (such as in these implementations), but may cause issues if relying on the raw lowest order bit.
JSF (Jenkins' Small Fast)
This is JSF or 'smallprng' by Bob Jenkins (2007), who also made ISAAC and SpookyHash. It passes PractRand tests and should be quite fast, although not as fast as sfc32.
function jsf32(a, b, c, d) {
return function() {
a |= 0; b |= 0; c |= 0; d |= 0;
var t = a - (b << 27 | b >>> 5) | 0;
a = b ^ (c << 17 | c >>> 15);
b = c + d | 0;
c = d + t | 0;
d = a + t | 0;
return (d >>> 0) / 4294967296;
}
}
Solution 3
NOTE: Despite (or rather, because of) succinctness and apparent elegance, this algorithm is by no means a high-quality one in terms of randomness. Look for e.g. those listed in this answer for better results.
(Originally adapted from a clever idea presented in a comment to another answer.)
var seed = 1;
function random() {
var x = Math.sin(seed++) * 10000;
return x - Math.floor(x);
}
You can set seed
to be any number, just avoid zero (or any multiple of Math.PI).
The elegance of this solution, in my opinion, comes from the lack of any "magic" numbers (besides 10000, which represents about the minimum amount of digits you must throw away to avoid odd patterns - see results with values 10, 100, 1000). Brevity is also nice.
It's a bit slower than Math.random() (by a factor of 2 or 3), but I believe it's about as fast as any other solution written in JavaScript.
Solution 4
No, but here's a simple pseudorandom generator, an implementation of Multiply-with-carry I adapted from Wikipedia (has been removed since):
var m_w = 123456789;
var m_z = 987654321;
var mask = 0xffffffff;
// Takes any integer
function seed(i) {
m_w = (123456789 + i) & mask;
m_z = (987654321 - i) & mask;
}
// Returns number between 0 (inclusive) and 1.0 (exclusive),
// just like Math.random().
function random()
{
m_z = (36969 * (m_z & 65535) + (m_z >> 16)) & mask;
m_w = (18000 * (m_w & 65535) + (m_w >> 16)) & mask;
var result = ((m_z << 16) + (m_w & 65535)) >>> 0;
result /= 4294967296;
return result;
}
Solution 5
Please see Pierre L'Ecuyer's work going back to the late 1980s and early 1990s. There are others as well. Creating a (pseudo) random number generator on your own, if you are not an expert, is pretty dangerous, because there is a high likelihood of either the results not being statistically random or in having a small period. Pierre (and others) have put together some good (pseudo) random number generators that are easy to implement. I use one of his LFSR generators.
https://www.iro.umontreal.ca/~lecuyer/myftp/papers/handstat.pdf
Phil Troy
Admin
Updated on August 02, 2022Comments
-
Admin almost 2 years
Is it possible to seed the random number generator (
Math.random
) in JavaScript? -
Nathan Breit over 10 yearsIs there a way to prove this RNG generate numbers that are uniformly distributed? Experimentally it seems to: jsfiddle.net/bhrLT
-
Justin over 10 yearsHas anyone tested this function for its randomness?
-
A.M.K about 10 years6,000,000 ops/second is pretty fast, I don't plan on generating more than ~3,000,000 per click. Kidding, this is brilliant.
-
A.M.K about 10 yearsActually, the smaller the number is, the faster it runs. A Math.random decimal gets ~20,000,000 ops/second. I just updated the jsperf. Either way, this probably won't be a bottleneck in your application.
-
spencer nelson almost 10 years-1, This isn't a uniform sampler at all - it is quite biased towards 0 and 1 (see jsfiddle.net/bhrLT/17, which may take a while to compute). Consecutive values are correlated - every 355 values, and even more so every 710, are related. Please use something more carefully thought-out!
-
Jason Goemaat almost 10 years@kharybdis change the 10000 to 1000000 and it looks better
-
spencer nelson almost 10 years@JasonGoemaat, that's just taking a few extra decimals from the sin function - it's not resolving the core problem, which is that the decimals of sin aren't uniformly distributed. It looks better, but thats just because you need more datapoints to see the nonuniformity.
-
Jason Goemaat almost 10 yearsThe question's not about creating a cryptographically secure random number generator, but something that works in javascript, useful for quick demos, etc. I'll take something quick and simple that gives a good looking distribution over a million random numbers for that purpose.
-
Michael_Scharf almost 10 yearsThis is the multiply-with-carry (MWC) random generator with a pretty long period. Adapted from wikipedia Random Number Generators
-
Tomas Kubes over 9 yearsWhen I use it with my random color generator (HSL), it generates only green and cyan colors. The original random generator generates all colors. So, it is not same or it does not work.
-
realh almost 9 yearsLooking at the histogram spencer nelson posted, it looks like the uniformity could easily be improved by discarding results outside the range, say [0.1,0.9], and normalising the remaining results from that range to [0,1].
-
danbo over 8 yearsJason Goemaat : Actualy any mod operation can fit the answer in that case, and for that matter only using sin(x) or mod(x,...) is the same. That's not random number, you can also use permutation to even go faster. The question is then misleading if tricks like this are accepted. I am downvoting it. A random generator should show any correlation, nor frequency pattern, it's stupid.
-
ESL over 8 years@Michael_Scharf 1) The seed change
m_w
, notm_z
. 2) Bothm_w
andm_z
are change BASED on their previous values, so it does modify the result. -
Obiwahn over 8 yearsBe careful. Math.sin() can give different results on client and server. I use Meteor (uses javascript on client & server).
-
undefined about 8 yearsThis produces very similar results at the beginning of the sequence with different seeds. For example,
Math.seed(0)()
returns0.2322845458984375
, andMath.seed(1)()
returns0.23228873685002327
. Changing bothm_w
andm_z
according to the seed seems to help.var m_w = 987654321 + s; var m_z = 123456789 - s;
produces a nice distribution of first values with different seeds. -
Jorge Fuentes González over 7 years@realh Exactly! jsfiddle.net/Llorx/bhrLT/93 Still not 100% secure for crypto, but the result is more normalized.
-
Martin Omander about 7 yearsWhen I used this code I did not get well-distributed results. Regardless of seed, the output sequence was very similar. This made it not helpful for my game.
-
Nikolay Fominyh about 7 yearsGreat answer, but not related to javascript :)
-
user2383235 about 7 yearsThe code for implementing Professor L'Ecuyer's work is publicly available for java and readily translatable by most programmers into Javascript.
-
bryc about 7 yearsI'm no expert, but sequential seeds follow a constant pattern. Colored pixels are >= 0.5. I am guessing its just iterating over the radius over and over?
-
Qix - MONICA WAS MISTREATED almost 7 years@Justin this is several years too late, but with several seeds at millions of runs, it's almost perfectly distributed. Variance in distribution was around
3.469446951953614e-18
- i.e. extremely well distributed. This is the script I used to test distribution. Just needs an object with anext()
method. -
Ray Foss over 6 yearsThis does create different results on different browsers... chrome, safari firefox all have different results. so if the purpose is to use the seed for consistency, this won't work.
-
bryc over 6 yearsChanging the seed does not result in a completely different sequence of random numbers, there is some sort of underlying pattern. This is the XOR of two bit graphs with seed 1 and 55. White pixels are <0.5, black pixels are >0.5. XORing two outputs with different seeds show a bias towards >0.5.
-
Jack G about 6 yearsThe question is about seeding
Math.random
such that wheneverMath.random
is seeded with the same seed, it will produce the same successive series of random numbers. This question is not, per say, about the actual usage/demonstration ofMath.random
. -
Gabriel almost 6 yearsThe sequences of numbers produced by this don't really approximate the properties of sequences of random numbers. Generate 15 numbers with it and the resulting string almost always begins with a 7 for nearly any key, for example.
-
Rz Mk over 5 yearsNitpicking this answer for what it is, is not right.
-
bryc over 5 yearsI provided an edit to address some serious issues in the code. It was not being properly seeded, and the workaround for negative numbers was causing bias. This is very similar to the MWC1616 algorithm which has a bad reputation as far as quality goes.
-
bryc about 5 years@undefined the issue you described is fixed as of the last edit, it was a bug in the MWC implementation.
-
Chad von Nau about 5 yearsAre these all inclusive of 0 and exclusive of 1?
-
Lachmanski almost 5 yearsI believe the values you quoted from "Tables of Linear Congruential Generators..." by Pierre L’ecuyer could exceed the maximum integer size in Javascript. The max seed of (2^32-1) * 741103597 ≈ 3e18, which is greater than JavaScript's max int size of ≈ 9e15. I think the following values from Pierre's book have the largest period within native limits:
seed = (seed * 185852 + 1) % 34359738337
. -
bryc almost 5 years@Lachmanski true, but those are bound by 32-bits (and the Park-Miller 31-bits). Using
Math.imul
allows it to overflow as it would when using multiplication in C on 32-bit integers. What you're suggesting is an LCG utilizing the full range of JS's integer space, which is definitely an interesting area to explore as well. :) -
user334639 over 4 yearsThis is awesome! Can I just copy your sfc32 into an LGPL program?
-
bryc over 4 yearsSure, feel free to use the code for whatever purpose :)
-
user334639 over 4 yearsIt’s far from uniform and far from independent. Mathematically speaking, it is ergodic but not even mixing. It will fail the most basic tests involving LLN and CLT.
-
Admin over 4 yearsWould be nice to have these examples on a page that can be cited without SO in the url (for attribution)
-
bryc over 4 years@blobber2 not sure what you mean, but the original code is from here (with others): github.com/bryc/code/blob/master/jshash/PRNGs.md. more or less a gist inside a repository :-)
-
Eureka over 4 yearsWorking nicely now, as of Jan 2020. Seed with 0, get 0.7322976540308446. Seed with 1, 0.16818441334180534, with 2: 0.6040864314418286, with 3: 0.03998844954185188. Thank you both!
-
karpada about 4 yearsThere's more correlation to 355 then 710 because it a continued fraction of pi: en.wikipedia.org/wiki/… @spencernelson
-
DrLightman about 4 yearsthanks for the very useful post! are the a >>>= 0 and a |= 0 some fast ways of casting to 32 bit int?
-
bryc about 4 years@DrLightman correct - forcing it early, or keeping it always in 32-bit, appears to speed things up. Although now I forget how exactly I measured them. But it could easily be obsolete in the future as JS engines might get smarter. But even now, this example shows that fnv1a hash is 70% slower without
| 0
, in firefox: jsbench.me/afk9jv36oz/1. -
Luc about 4 yearsThanks for the research that went into this, bryc! It still took me a bit to figure out which to use and how to generate an integer 0-n (first used Math.round, tested it, didn't give good results, then used Math.floor, that worked). Maybe add to the "apples" example:
n = Math.floor(rand() * X); // generate random integer below X
? For choosing an algorithm, e.g. JSF says it performs "well" (others perform "without issue", is that better?) and "should" be fast. I see in git you have the numbers, it might help to be more concrete here. Either way, your answer helped me, thanks for the work! -
bryc about 4 years@Luc Thanks, glad it was a help! Some valid criticisms here - I'll add link to MDN for doing ranged numbers. JSF and SFC both pass PractRand, but JSF is a bit slower - I just wrote general observations. I don't want the answer to be TOO long. But I will see if I can make it less confusing. :)
-
Luc about 4 years@bryc I see you did quite some work on the post! The MDN reference is a good idea, I didn't know of that page (would have saved me the trial-and-error :) ). One question: "these are all 32-bit generators" and a bit later "sfc32 has a 128-bit state". Isn't that a contradiction (probably leftover from the previous revision)?
-
bryc about 4 years@Luc No, that's correct. These algorithms are ported from 32-bit C code, intended for older 32-bit CPUs. "128-bit state" is just the internal memory size, and implies a collection of four 32-bit words acting as one unit. In JS, bitwise operations are bound by 32-bit words, which is why 32-bit C code can be ported to JS efficiently. Hope that explains it
-
Luc about 4 years@bryc So if I understand it correctly, the period will be 2^128 and the "they're all 32-bit" is meant to convey that the algorithms can efficiently be ported from their original C to JavaScript? Because what I guess people care about is the period, not so much how hard the porting work was. Thanks again, by the way :)
-
bryc about 4 years@Luc The state size doesn't define the period, only theoretical max. A minimum period of 2^32 is sufficient for non-cryptographic use. JSF and SFC have an avg period of 2^127 (according to PractRand) which is quite good. And I mention "32-bit" because it is important to understand. Something like PCG by its very nature, is inefficient/slow in JS (my goal here is efficiency/speed). It takes more code to reimplement 64-bit multiplication than any of the above PRNGs, and end up running exponentially slower.
-
Vroo over 3 yearsThanks for this code. I do question the use of xmur3 to initialize sfc32. You're initializing 128 bits of seed with only 32 bits of entropy. I think it's better to use a hash that produces a larger result.
-
bryc over 3 years@Vroo I'm not that concerned, PRNG quality > seed quality, and it is somewhat unavoidable. Due to JS bitwise limitations, 32-bit results must be mixed together. Have a look at MurmurHash3_x86_128 - four uncorrelated 32-bit hashes sufficiently mixed produce valid 128-bit hashes. If improved mixing of component hashes does not increase entropy, as used in MurmurHash3_x86_128, then nothing can be done about it~
-
Vroo over 3 yearsMy point @bryc was that you're not using "four uncorrelated 32-bit hashes," you're using four correlated hashes since xmur3 only has 32 bits of internal state. I'm using your port of sfc32 (thanks!) and am initializing it with a 128 bit hash instead.
-
bryc over 3 years@Vroo
xmur3
is indeed correlated, there's no denying that - I was referring to the linked 128-bit hash implementation which was uncorrelated. Unfortunately fixing xmur3 would require quadrupling the code size. Which 128-bit hash are you using? I have not seen much of those in JS. -
penduDev over 3 yearsThis function is biased as abs(Math.sin(random_number)) is itself a function biased around the point where slope of sin function is zero. Here's the code that can be run in js console for experiment: (pastebin.com/kJyHaQYY)
-
Searle over 2 yearsIn xmur3, maybe put the first two lines of the for loop in braces for clarity? (I misread the comma for a semicolon and was confused, but maybe it's only me)
-
Flex Elektro Deimling almost 2 yearsThanks a lot for this very good overview !
-
Ted almost 2 yearsCan I use this in crypto context?
-
Ted almost 2 yearsIs this method secure enough? Can I use this function in crypto context?
-
bryc almost 2 years@Ted No, these are not secure PRNGs. Use the built-in
Crypto.getRandomValues()
for secure random values, but note it cannot be seeded (AFAIK). If you absolutely must have repeatable results for some reason, try ISAAC, but it wont be as fast and take up more code. -
bryc almost 2 years@Vroo 2 years later, I've now replaced
xmur3
with an intermixed 128-bit hash. Hopefully its not much worse thanMurmurHash3_x86_128
. Sorry I didn't quite grasp your entropy concern before, I now realize it was misleading to suggest it for initializing a 128-bit state. -
jdbertron almost 2 yearsAnd where does this cyrb128 come from ?
-
bryc almost 2 years@jdbertron It's a hash function I made, similar to cyrb53 but outputs 128 bits instead of 53.