可种子化的JavaScript随机数生成器

189
JavaScript的 Math.random() 函数会返回一个0到1之间的随机数值,其种子基于当前时间自动生成(类似于Java)。但是,我认为没有任何方法可以设置您自己的种子。

如何制作一个随机数生成器,以便我可以提供自己的种子值,从而使其产生可重复的(伪)随机数序列?


2
注意:为了保持问题的简短和重点,我已将上面问题中的代码拆分到下面的社区Wiki答案中。 - Ilmari Karonen
2
请参见 https://dev59.com/QHRB5IYBdhLWcg3wxZ1K - Palimondo
这个回答解决了你的问题吗?在Javascript中种子化随机数生成器 - ggorlen
12个回答

156

35
David Bau的seedrandom已经足够受欢迎,以至于他在github上维护它。遗憾的是,ECMAScript已经被边缘化很长时间了,以至于像这样的东西没有包含在语言中。说真的,没有种子!!! - Eat at Joes
2
@EatatJoes,JS的耻辱和荣耀在于这是既必要又可能的。你可以包含一个文件并使对Math对象进行向后兼容的更改,这真的很酷。 Brendan Eich,10天的工作还不错。 - Bruno Bronosky
5
针对寻找此项目的npm页面的任何人:https://www.npmjs.com/package/seedrandom - Kip

30
如果您不需要种子功能,只需使用Math.random()并围绕它构建辅助函数(例如randRange(start, end))。
我不确定您正在使用哪个RNG,但最好了解并记录它,以便您了解其特性和限制。
像Starkii所说,Mersenne Twister是一个很好的PRNG,但实现起来并不容易。如果您想自己尝试,请尝试实现LCG - 这非常简单,具有良好的随机性能(不如Mersenne Twister好),并且您可以使用一些流行的常数。
编辑:请考虑在此答案中提供的出色选项,包括一个LCG选项,用于短种子RNG实现。

function RNG(seed) {
  // LCG using GCC's constants
  this.m = 0x80000000; // 2**31;
  this.a = 1103515245;
  this.c = 12345;

  this.state = seed ? seed : Math.floor(Math.random() * (this.m - 1));
}
RNG.prototype.nextInt = function() {
  this.state = (this.a * this.state + this.c) % this.m;
  return this.state;
}
RNG.prototype.nextFloat = function() {
  // returns in range [0,1]
  return this.nextInt() / (this.m - 1);
}
RNG.prototype.nextRange = function(start, end) {
  // returns in range [start, end): including start, excluding end
  // can't modulu nextInt because of weak randomness in lower bits
  var rangeSize = end - start;
  var randomUnder1 = this.nextInt() / this.m;
  return start + Math.floor(randomUnder1 * rangeSize);
}
RNG.prototype.choice = function(array) {
  return array[this.nextRange(0, array.length)];
}

var rng = new RNG(20);
for (var i = 0; i < 10; i++)
  console.log(rng.nextRange(10, 50));

var digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
for (var i = 0; i < 10; i++)
  console.log(rng.choice(digits));


2
取模数不应该是2^31吗?我从维基百科上读到了这个算法。 - Trantor Liu
3
请注意,从数学意义上讲,这并不是“正确”的,因为它不能输出符合数学规律的结果。换句话说,能够处理这些大数字的编程语言将会有不同的结果。JavaScript 在处理大数字时会出现错误,并且缺乏精度(毕竟它们是浮点数)。 - DDS
6
这个LCG实现在JavaScript中超出了精确整数的限制,因为this.a * this.state 很可能会导致一个比2^53大的数字。结果是输出范围受限,对于一些种子可能会有非常短的周期。通常使用2的幂次方作为m会产生一些非常明显的模式,当你进行模运算而不是简单截断时,没有理由不使用质数。 - aaaaaaaaaaaa

23

如果您想要指定种子,只需替换对getSeconds()getMinutes()的调用即可。您可以传递一个整数,并使用其一半模60作为秒值,另一半模60作为其他部分。

话虽如此,这种方法看起来很糟糕。正确生成随机数非常困难。显而易见的问题是随机数种子基于秒和分钟。猜测种子并重新创建随机数流仅需要尝试3600个不同的秒和分钟组合。这也意味着只有3600个可能的种子。这是可以纠正的,但我从一开始就对这个RNG持怀疑态度。

如果您想使用更好的RNG,请尝试Mersenne Twister。它是经过充分测试且相当强大的RNG,具有巨大的轨道和出色的性能。

编辑:我真的应该正确地将其称为伪随机数生成器或PRNG。

“使用算术方法生成随机数的人处于罪恶状态。”                                                                                                                                                            --- 约翰·冯·诺伊曼

1
Mersenne Twister的JS实现链接:http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/VERSIONS/JAVASCRIPT/java-script.html - orip
1
@orip 你有关于初始状态3600的来源吗?Mersenne Twister是由32位数种子初始化,因此PRNG应该有40亿个初始状态——但前提是初始种子是真正随机的。 - Tobias P.
2
@TobiasP。我指的是建议使用getSeconds()和getMinutes()的组合进行种子生成,有3600个可能的初始状态,而不是指Mersenne Twister。 - orip
3
好的,我会尽力进行翻译。您说的是梅森旋转算法,下一句话提到了初始状态。 - Tobias P.
2
提问者并没有提到他们需要“适当”的随机数生成器来进行任何类型的密码学敏感应用。虽然所有答案都是正确的,但只有第一段与所提出的问题实际相关。也许可以添加一个建议解决方案的代码片段。 - V. Rubinetti
显示剩余3条评论

15

我使用了 Mersenne Twister 的 JavaScript 版本: https://gist.github.com/300494。这个版本可以让你手动设置种子。另外,正如其他答案中提到的那样,Mersenne Twister 是一个真正好的伪随机数生成器。


9
您提供的代码看起来有点像Lehmer RNG。如果是这种情况,那么2147483647是最大的32位有符号整数,2147483647是最大的32位质数,48271是一个全周期乘法器,用于生成数字。
如果是这样,您可以修改RandomNumberGenerator以接受额外的参数seed,然后将this.seed设置为seed;但您必须小心确保种子会产生良好的随机数分布(Lehmer可能很奇怪)--但大多数种子都没问题。

8
以下是可以使用自定义种子的伪随机数生成器。调用SeedRandom将返回一个随机生成器函数。可以不带参数调用SeedRandom以使用当前时间来初始化返回的随机函数,或者可以使用1个或2个非负整数作为参数来初始化它。由于浮点精度的原因,仅使用1个值进行初始化会使生成器只能初始为2^53个不同状态之一。
返回的随机生成器函数接受名为limit的1个整数参数,限制必须在1到4294965886的范围内,该函数将返回0到limit-1的数字。
function SeedRandom(state1,state2){
    var mod1=4294967087
    var mul1=65539
    var mod2=4294965887
    var mul2=65537
    if(typeof state1!="number"){
        state1=+new Date()
    }
    if(typeof state2!="number"){
        state2=state1
    }
    state1=state1%(mod1-1)+1
    state2=state2%(mod2-1)+1
    function random(limit){
        state1=(state1*mul1)%mod1
        state2=(state2*mul2)%mod2
        if(state1<limit && state2<limit && state1<mod1%limit && state2<mod2%limit){
            return random(limit)
        }
        return (state1+state2)%limit
    }
    return random
}

使用示例:

var generator1=SeedRandom() //Seed with current time
var randomVariable=generator1(7) //Generate one of the numbers [0,1,2,3,4,5,6]
var generator2=SeedRandom(42) //Seed with a specific seed
var fixedVariable=generator2(7) //First value of this generator will always be
                                //1 because of the specific seed.

该生成器具有以下特性:

  • 它有大约2^64个不同的可能内部状态。
  • 它的周期大约为2^63,比任何实际需要在JavaScript程序中使用的周期都要长得多。
  • 由于mod值是质数,因此无论选择哪个限制,输出中都没有简单的模式。这与一些较简单的伪随机数生成器不同,后者展现出一些相当系统化的模式。
  • 它会丢弃一些结果以获得完美的分布,无论限制如何。
  • 它相对较慢,在我的机器上大约每秒运行10,000,000次。

奖励: typescript版本


2
为什么会产生这种模式?for (var i = 0; i < 400; i++) { console.log("input: (" + i * 245 + ", " + i * 553 + ") | output: " + SeedRandom(i * 245, i * 553)(20)); } - Timothy Kanski
@TimothyKanski 因为你使用方法不正确。我不是专家,但这是因为你在每次迭代中都初始化生成器,只看到了基于种子的第一个值,并且没有迭代生成器的后续数字。我相信这将发生在任何未在指定间隔内哈希种子的伪随机数生成器中。 - bryc
@bryc - 我认为 @TimothyKanski 是在测试不同的种子是否会产生不同的随机数,结果似乎出现了一个模式 - 这很奇怪。我也进行了测试,几乎任何东西都会产生模式:let count = 0; setInterval(() => { console.log(SeedRandom(count++,count++)(10)); },500);会重复输出3、5、7、9、1。 - Dan Zen
@DanZen 重点在于,测试方法存在缺陷,无法证明随机性的质量。不是说 SeedRandom 函数是“好”的,它很可能不是。但由于熵不足,许多优秀的 PRNG 将在此测试中失败。使用不同的算法进行您的测试,我可以使一个不良函数通过,而使一个良好的函数失败:https://paste2.org/AkhJfgvh。坏函数只有使用大乘数才能通过。 - bryc
我明白了,你的意思是种子需要一些随机性,而不是每次增加1或类似的操作。我之前没有意识到这点。谢谢。 - Dan Zen

7

如果您使用Typescript编程,我已经将Christoph Henkelmann在这个帖子中提供的Mersenne Twister实现适配为Typescript类:

/**
 * copied almost directly from Mersenne Twister implementation found in https://gist.github.com/banksean/300494
 * all rights reserved to him.
 */
export class Random {
    static N = 624;
    static M = 397;
    static MATRIX_A = 0x9908b0df;
    /* constant vector a */
    static UPPER_MASK = 0x80000000;
    /* most significant w-r bits */
    static LOWER_MASK = 0x7fffffff;
    /* least significant r bits */

    mt = new Array(Random.N);
    /* the array for the state vector */
    mti = Random.N + 1;
    /* mti==N+1 means mt[N] is not initialized */

    constructor(seed:number = null) {
        if (seed == null) {
            seed = new Date().getTime();
        }

        this.init_genrand(seed);
    }

    private init_genrand(s:number) {
        this.mt[0] = s >>> 0;
        for (this.mti = 1; this.mti < Random.N; this.mti++) {
            var s = this.mt[this.mti - 1] ^ (this.mt[this.mti - 1] >>> 30);
            this.mt[this.mti] = (((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253)
                + this.mti;
            /* See Knuth TAOCP Vol2. 3rd Ed. P.106 for multiplier. */
            /* In the previous versions, MSBs of the seed affect   */
            /* only MSBs of the array mt[].                        */
            /* 2002/01/09 modified by Makoto Matsumoto             */
            this.mt[this.mti] >>>= 0;
            /* for >32 bit machines */
        }
    }

    /**
     * generates a random number on [0,0xffffffff]-interval
     * @private
     */
    private _nextInt32():number {
        var y:number;
        var mag01 = new Array(0x0, Random.MATRIX_A);
        /* mag01[x] = x * MATRIX_A  for x=0,1 */

        if (this.mti >= Random.N) { /* generate N words at one time */
            var kk:number;

            if (this.mti == Random.N + 1)   /* if init_genrand() has not been called, */
                this.init_genrand(5489);
            /* a default initial seed is used */

            for (kk = 0; kk < Random.N - Random.M; kk++) {
                y = (this.mt[kk] & Random.UPPER_MASK) | (this.mt[kk + 1] & Random.LOWER_MASK);
                this.mt[kk] = this.mt[kk + Random.M] ^ (y >>> 1) ^ mag01[y & 0x1];
            }
            for (; kk < Random.N - 1; kk++) {
                y = (this.mt[kk] & Random.UPPER_MASK) | (this.mt[kk + 1] & Random.LOWER_MASK);
                this.mt[kk] = this.mt[kk + (Random.M - Random.N)] ^ (y >>> 1) ^ mag01[y & 0x1];
            }
            y = (this.mt[Random.N - 1] & Random.UPPER_MASK) | (this.mt[0] & Random.LOWER_MASK);
            this.mt[Random.N - 1] = this.mt[Random.M - 1] ^ (y >>> 1) ^ mag01[y & 0x1];

            this.mti = 0;
        }

        y = this.mt[this.mti++];

        /* Tempering */
        y ^= (y >>> 11);
        y ^= (y << 7) & 0x9d2c5680;
        y ^= (y << 15) & 0xefc60000;
        y ^= (y >>> 18);

        return y >>> 0;
    }

    /**
     * generates an int32 pseudo random number
     * @param range: an optional [from, to] range, if not specified the result will be in range [0,0xffffffff]
     * @return {number}
     */
    nextInt32(range:[number, number] = null):number {
        var result = this._nextInt32();
        if (range == null) {
            return result;
        }

        return (result % (range[1] - range[0])) + range[0];
    }

    /**
     * generates a random number on [0,0x7fffffff]-interval
     */
    nextInt31():number {
        return (this._nextInt32() >>> 1);
    }

    /**
     * generates a random number on [0,1]-real-interval
     */
    nextNumber():number {
        return this._nextInt32() * (1.0 / 4294967295.0);
    }

    /**
     * generates a random number on [0,1) with 53-bit resolution
     */
    nextNumber53():number {
        var a = this._nextInt32() >>> 5, b = this._nextInt32() >>> 6;
        return (a * 67108864.0 + b) * (1.0 / 9007199254740992.0);
    }
}

你可以按照以下方式使用它:
var random = new Random(132);
random.nextInt32(); //return a pseudo random int32 number
random.nextInt32([10,20]); //return a pseudo random int in range [10,20]
random.nextNumber(); //return a a pseudo random number in range [0,1]

请查阅源代码了解更多方法。


3

这里有一个非常有效但简单的JavaScript PRNG函数,我很喜欢使用:

// The seed is the base number that the function works off
// The modulo is the highest number that the function can return

function PRNG(seed, modulo) {
    str = `${(2**31-1&Math.imul(48271,seed))/2**31}`
    .split('')
    .slice(-10)
    .join('') % modulo

    return str
}

我希望这正是你在寻找的。


谢谢,这不是我在寻找的东西,但还是很有趣。据我所知,它会返回给定种子的“随机”数字,在指定的模数范围内。例如,PRNG(37, 1000000); 总是返回 863796,而 PRNG(24, 1000000); 总是返回 911652...现在想知道这可能有什么用处...嗯... - scunliffe
这非常适合简单的用例,您只需要一个数字而不是一系列数字!谢谢。 - JohnDoe
经过仔细检查,似乎它并不是完全均匀的。 - JohnDoe

0

谢谢,@aaaaaaaaaaaa(采纳答案)

我真的需要一个好的非库解决方案(更容易嵌入)

所以...我制作了这个类来存储种子并允许类似Unity的“下一个”...但保留了最初基于整数的结果

class randS {
    constructor(seed=null) {
        if(seed!=null) {
            this.seed = seed;
        } else {
            this.seed = Date.now()%4645455524863;
        }
        this.next = this.SeedRandom(this.seed);
        this.last = 0;
    }
    Init(seed=this.seed) {
        if (seed = this.seed) {
            this.next = this.SeedRandom(this.seed);
        } else {
            this.seed=seed;
            this.next = this.SeedRandom(this.seed);
        }
    }
    SeedRandom(state1,state2){
        var mod1=4294967087;
        var mod2=4294965887;
        var mul1=65539;
        var mul2=65537;
        if(typeof state1!="number"){
            state1=+new Date();
        }
        if(typeof state2!="number"){
            state2=state1;
        }
        state1=state1%(mod1-1)+1;
        state2=state2%(mod2-1)+1;
        function random(limit){
            state1=(state1*mul1)%mod1;
            state2=(state2*mul2)%mod2;
            if(state1<limit && state2<limit && state1<mod1%limit && state2<mod2%limit){
                this.last = random;
                return random(limit);
            }
            this.last = (state1+state2)%limit;
            return (state1+state2)%limit;
        }
        this.last = random;
        return random;

    }
}

然后使用这些进行了检查... 似乎可以很好地与随机(但可查询)种子值(如Minecraft)配合使用,甚至存储了上次返回的值(如果需要)

var rng = new randS(9005646549);
console.log(rng.next(20)+' '+rng.next(20)+' '+rng.next(20)+' '+rng.next(20)+' '+rng.next(20)+' '+rng.next(20)+' '+rng.next(20));
console.log(rng.next(20) + ' ' + rng.next(20) + ' ' + rng.last);

这应该输出(对于每个人)

6 7 8 14 1 12 6
9 1 1

编辑:我让init()函数可以工作,如果你需要重新生成随机数种子或者测试值的话(在我的情况下也是必要的)


0
注意:此代码最初包含在以上问题中。为了保持问题简洁和重点突出,我将其移动到此社区Wiki答案中。
我发现这个代码运行良好,可以获取一个随机数,然后使用后面的种子,但是我不太确定逻辑如何工作(例如2345678901、48271和2147483647数字的来源)。

function nextRandomNumber(){
  var hi = this.seed / this.Q;
  var lo = this.seed % this.Q;
  var test = this.A * lo - this.R * hi;
  if(test > 0){
    this.seed = test;
  } else {
    this.seed = test + this.M;
  }
  return (this.seed * this.oneOverM);
}

function RandomNumberGenerator(){
  var d = new Date();
  this.seed = 2345678901 + (d.getSeconds() * 0xFFFFFF) + (d.getMinutes() * 0xFFFF);
  this.A = 48271;
  this.M = 2147483647;
  this.Q = this.M / this.A;
  this.R = this.M % this.A;
  this.oneOverM = 1.0 / this.M;
  this.next = nextRandomNumber;
  return this;
}

function createRandomNumber(Min, Max){
  var rand = new RandomNumberGenerator();
  return Math.round((Max-Min) * rand.next() + Min);
}

//Thus I can now do:
var letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
var numbers = ['1','2','3','4','5','6','7','8','9','10'];
var colors = ['red','orange','yellow','green','blue','indigo','violet'];
var first = letters[createRandomNumber(0, letters.length)];
var second = numbers[createRandomNumber(0, numbers.length)];
var third = colors[createRandomNumber(0, colors.length)];
    
alert("Today's show was brought to you by the letter: " + first + ", the number " + second + ", and the color " + third + "!");

/*
  If I could pass my own seed into the createRandomNumber(min, max, seed);
  function then I could reproduce a random output later if desired.
*/


3
哇,RandomNumberGeneratornextRandomNumber函数实际上可以追溯到1996年。它应该是一种Lehmer / LCG随机数生成器。它使用一些巧妙的数学方法来对32位整数执行模算术运算,否则这些中间值将太小而无法容纳。问题在于,JavaScript没有实现32位整数,而是使用64位浮点数。由于除法不是这段代码所假定的整数除法,因此结果并不是Lehmer生成器。它确实会产生一些看起来随机的结果,但Lehmer生成器的保证不适用。 - aaaaaaaaaaaa
1
createRandomNumber函数是后来添加的,它几乎做了所有错误的事情,最明显的是每次调用时都会实例化一个新的RNG,这意味着快速连续调用将使用相同的浮点数。在给定的代码中,'a'几乎不可能与除'1''red'以外的任何东西配对。 - aaaaaaaaaaaa

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接