Seeding the random number generator in Javascript

后端 未结 13 1404
野趣味
野趣味 2020-11-22 09:28

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

13条回答
  •  名媛妹妹
    2020-11-22 10:10

    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 good quality numbers.

    First of all, take care to initialize your PRNGs properly. Most of the generators below have no built-in seed generating procedure (for sake of simplicity), but accept one or more 32-bit values as the initial state of the PRNG. Similar seeds (e.g. a simple seed of 1 and 2) can cause correlations in weaker PRNGs, 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 seed.

    Thankfully, hash functions are very good at generating seeds for PRNGs from short strings. A good hash function will generate very different results even when two strings are similar. Here's an example based on MurmurHash3's mixing function:

    function xmur3(str) {
        for(var i = 0, h = 1779033703 ^ str.length; i < str.length; i++)
            h = Math.imul(h ^ str.charCodeAt(i), 3432918353),
            h = h << 13 | h >>> 19;
        return function() {
            h = Math.imul(h ^ h >>> 16, 2246822507);
            h = Math.imul(h ^ h >>> 13, 3266489909);
            return (h ^= h >>> 16) >>> 0;
        }
    }
    

    Each subsequent call to the return function of xmur3 produces a new "random" 32-bit hash value to be used as a seed in a PRNG. Here's how you might use it:

    // Create xmur3 state:
    var seed = xmur3("apples");
    // Output four 32-bit hashes to provide the seed for sfc32.
    var rand = sfc32(seed(), seed(), seed(), seed());
    
    // Output one 32-bit hash to provide the seed for mulberry32.
    var rand = mulberry32(seed());
    
    // Obtain sequential random numbers like so:
    rand();
    rand();
    

    Alternatively, simply choose some dummy data to pad the seed with, and advance the generator a few times (12-20 iterations) to mix the initial state thoroughly. This is often seen 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();
    

    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.

    Another thing to note are the limitations of JS. Numbers can only represent whole integers up to 53-bit resolution. And when using bitwise operations, this is reduced to 32. This makes it difficult to implement algorithms written in C or C++, that use 64-bit numbers. Porting 64-bit code requires shims that can drastically reduce performance. So for the sake of simplicity and efficiency, I've only considered algorithms that use 32-bit math, as it is directly compatible with JS.

    Now, onward to the the generators. (I maintain the full list with references 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;
        }
    }
    

    Mulberry32

    Mulberry32 is a simple generator with a 32-bit state, but is extremely fast and has good quality (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 fails some tests in TestU01 (particularly LinearComp and BinaryRank). In practice, it should not cause issues when floats are used (such as these implementations), but may cause issues if relying on the raw low bits.

    JSF (Jenkins' Small Fast)

    This is JSF or 'smallprng' by Bob Jenkins (2007), the guy who made ISAAC and SpookyHash. It passes PractRand tests and should be quite fast, although not as fast as SFC.

    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;
        }
    }
    

    LCG (aka Lehmer/Park-Miller RNG or MCG)

    LCG is extremely fast and simple, but the quality of its randomness is so low, that improper use can actually cause bugs in your program! Nonetheless, it is significantly better than some answers suggesting to use Math.sin or Math.PI! It's a one-liner though, which is nice :).

    var LCG=s=>()=>(2**31-1&(s=Math.imul(48271,s)))/2**31;
    

    This implementation is called the minimal standard RNG as proposed by Park–Miller in 1988 & 1993 and implemented in C++11 as minstd_rand. Keep in mind that the state is 31-bit (31 bits give 2 billion possible states, 32 bits give double that). This is the very type of PRNG that others are trying to replace!

    It will work, but I wouldn't use it unless you really need speed and don't care about randomness quality (what is random anyway?). Great for a game jam or a demo or something. LCGs suffer from seed correlations, so it is best to discard the first result of an LCG. And if you insist on using an LCG, adding an increment value may improve results, but it is probably an exercise in futility when much better options exist.

    There seems to be other multipliers offering a 32-bit state (increased state-space):

    var LCG=s=>()=>(s=Math.imul(741103597,s)>>>0)/2**32;
    var LCG=s=>()=>(s=Math.imul(1597334677,s)>>>0)/2**32;
    

    These LCG values are from: P. L'Ecuyer: A table of Linear Congruential Generators of different sizes and good lattice structure, April 30 1997.

提交回复
热议问题