Seeding the random number generator in Javascript

275,315

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

Share:
275,315
Admin
Author by

Admin

Updated on August 02, 2022

Comments

  • Admin
    Admin almost 2 years

    Is it possible to seed the random number generator (Math.random) in JavaScript?

  • Nathan Breit
    Nathan Breit over 10 years
    Is there a way to prove this RNG generate numbers that are uniformly distributed? Experimentally it seems to: jsfiddle.net/bhrLT
  • Justin
    Justin over 10 years
    Has anyone tested this function for its randomness?
  • A.M.K
    A.M.K about 10 years
    6,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
    A.M.K about 10 years
    Actually, 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
    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
    Jason Goemaat almost 10 years
    @kharybdis change the 10000 to 1000000 and it looks better
  • spencer nelson
    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
    Jason Goemaat almost 10 years
    The 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
    Michael_Scharf almost 10 years
    This is the multiply-with-carry (MWC) random generator with a pretty long period. Adapted from wikipedia Random Number Generators
  • Tomas Kubes
    Tomas Kubes over 9 years
    When 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
    realh almost 9 years
    Looking 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
    danbo over 8 years
    Jason 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
    ESL over 8 years
    @Michael_Scharf 1) The seed change m_w, not m_z. 2) Both m_w and m_z are change BASED on their previous values, so it does modify the result.
  • Obiwahn
    Obiwahn over 8 years
    Be careful. Math.sin() can give different results on client and server. I use Meteor (uses javascript on client & server).
  • undefined
    undefined about 8 years
    This produces very similar results at the beginning of the sequence with different seeds. For example, Math.seed(0)() returns 0.2322845458984375, and Math.seed(1)() returns 0.23228873685002327. Changing both m_w and m_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
    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
    Martin Omander about 7 years
    When 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
    Nikolay Fominyh about 7 years
    Great answer, but not related to javascript :)
  • user2383235
    user2383235 about 7 years
    The code for implementing Professor L'Ecuyer's work is publicly available for java and readily translatable by most programmers into Javascript.
  • bryc
    bryc about 7 years
    I'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
    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 a next() method.
  • Ray Foss
    Ray Foss over 6 years
    This 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
    bryc over 6 years
    Changing 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
    Jack G about 6 years
    The question is about seeding Math.random such that whenever Math.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 of Math.random.
  • Gabriel
    Gabriel almost 6 years
    The 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
    Rz Mk over 5 years
    Nitpicking this answer for what it is, is not right.
  • bryc
    bryc over 5 years
    I 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
    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
    Chad von Nau about 5 years
    Are these all inclusive of 0 and exclusive of 1?
  • Lachmanski
    Lachmanski almost 5 years
    I 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
    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
    user334639 over 4 years
    This is awesome! Can I just copy your sfc32 into an LGPL program?
  • bryc
    bryc over 4 years
    Sure, feel free to use the code for whatever purpose :)
  • user334639
    user334639 over 4 years
    It’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
    Admin over 4 years
    Would be nice to have these examples on a page that can be cited without SO in the url (for attribution)
  • bryc
    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
    Eureka over 4 years
    Working 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
    karpada about 4 years
    There's more correlation to 355 then 710 because it a continued fraction of pi: en.wikipedia.org/wiki/… @spencernelson
  • DrLightman
    DrLightman about 4 years
    thanks for the very useful post! are the a >>>= 0 and a |= 0 some fast ways of casting to 32 bit int?
  • bryc
    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
    Luc about 4 years
    Thanks 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
    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
    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
    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
    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
    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
    Vroo over 3 years
    Thanks 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
    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
    Vroo over 3 years
    My 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
    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
    penduDev over 3 years
    This 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
    Searle over 2 years
    In 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
    Flex Elektro Deimling almost 2 years
    Thanks a lot for this very good overview !
  • Ted
    Ted almost 2 years
    Can I use this in crypto context?
  • Ted
    Ted almost 2 years
    Is this method secure enough? Can I use this function in crypto context?
  • bryc
    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
    bryc almost 2 years
    @Vroo 2 years later, I've now replaced xmur3 with an intermixed 128-bit hash. Hopefully its not much worse than MurmurHash3_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
    jdbertron almost 2 years
    And where does this cyrb128 come from ?
  • bryc
    bryc almost 2 years
    @jdbertron It's a hash function I made, similar to cyrb53 but outputs 128 bits instead of 53.