简体   繁体   English

使用window.crypto.getRandomValues在JavaScript中随机播放扑克牌组

[英]Shuffling a poker deck in JavaScript with window.crypto.getRandomValues

A poker deck has 52 cards and thus 52! 一个扑克牌有52张牌,因此52! or roughly 2^226 possible permutations. 或大约2^226可能的排列。

Now I want to shuffle such a deck of cards perfectly, with truly random results and a uniform distribution, so that you can reach every single one of those possible permutations and each is equally likely to appear. 现在我想要完美地洗牌这样一副牌,真正随机的结果和均匀的分布,这样你就可以达到每一个可能的排列,每个都可能出现。

Why is this actually necessary? 为什么这实际上是必要的?

For games, perhaps, you don't really need perfect randomness, unless there's money to be won. 或许,对于游戏来说,你并不需要完美的随机性,除非有钱可以获胜。 Apart from that, humans probably won't even perceive the "differences" in randomness. 除此之外,人类可能甚至不会察觉到随机性的“差异”。

But if I'm not mistaken, if you use shuffling functions and RNG components commonly built into popular programming languages, you will often get no more than 32 bits of entropy and 2^32 states. 但是如果我没弄错的话,如果你使用通常内置于流行编程语言中的shuffling函数和RNG组件,你通常会获得不超过32位的熵和2^32个状态。 Thus, you will never be able to reach all 52! 因此,你永远无法达到所有52! possible permutations of the deck when shuffling, but only about ... 洗牌时甲板的可能排列,但仅限于......

0.000000000000000000000000000000000000000000000000000000005324900157 % 0.000000000000000000000000000000000000000000000000000000005324900157%

... of the possible permutations. ...可能的排列。 That means a whole lot of all the possible games that could be played or simulated in theory will never actually be seen in practice. 这意味着一大堆的一切在理论上可以播放或模拟永远不会真正在实践中看到的动作类游戏。

By the way, you can further improve the results if you don't reset to the default order every time before shuffling but instead start with the order from the last shuffle or keep the "mess" after a game has been played and shuffle from there. 顺便说一句,如果你在每次洗牌之前没有重置为默认顺序,你可以进一步改善结果,而是从最后一次洗牌的顺序开始,或者在比赛结束后保持“混乱”并从那里进行随机播放。

Requirements: 要求:

So in order to do what is described above, one needs all of the following three components, as far as I have understood: 因此,为了完成上述操作,我需要了解以下三个组件:据我所知:

  1. A good shuffling algorithm that ensures a uniform distribution. 一种良好的混洗算法,可确保均匀分布。
  2. A proper RNG with at least 226 bits of internal state. 具有至少226位内部状态的适当RNG。 Since we're on deterministic machines, a PRNG will be all we'll get, and perhaps this should be a CSPRNG. 由于我们在确定性机器上,PRNG将是我们所能得到的,也许这应该是CSPRNG。
  3. A random seed with at least 226 bits of entropy. 具有至少226位熵的随机种子。

Solutions: 解决方案:

Now is this achievable? 现在这可以实现吗? What do we have? 我们有什么?

  1. Fisher-Yates shuffle will be fine, as far as I can see. 就我所见, Fisher-Yates shuffle会很好。
  2. The xorshift7 RNG has more than the required 226 bits of internal state and should suffice. xorshift7 RNG具有超过所需的226位内部状态,应该足够了。
  3. Using window.crypto.getRandomValues we can generate the required 226 bits of entropy to be used as our seed. 使用window.crypto.getRandomValues我们可以生成所需的226位熵作为我们的种子。 If that still isn't enough, we can add some more entropy from other sources . 如果仍然不够,我们可以从其他来源添加更多的熵。

Question: 题:

Are the solutions (and also the requirements) mentioned above correct? 上述解决方案(以及要求)是否正确? How can you implement shuffling using these solutions in JavaScript in practice then? 那么如何在实践中使用JavaScript中的这些解决方案实现改组呢? How do you combine the three components to a working solution? 如何将这三个组件组合成一个可行的解决方案?

I guess I have to replace the usage of Math.random in the example of the Fisher-Yates shuffle with a call to xorshift7. 我想我必须在调用xorshift7的Fisher-Yates shuffle示例中替换Math.random的用法。 But that RNG outputs a value in the [0, 1) float range and I need the [1, n] integer range instead. 但是RNG在[0, 1)浮点范围内输出一个值,而我需要[1, n]整数范围。 When scaling that range, I don't want to lose the uniform distribution. 缩放该范围时,我不想失去均匀分布。 Moreover, I wanted about 226 bits of randomness. 而且,我想要大约226位的随机性。 If my RNG outputs just a single Number , isn't that randomness effectively reduced to 2^53 (or 2^64) bits because there are no more possibilities for the output? 如果我的RNG仅输出一个Number ,那么随机性是否有效地降低到2 ^ 53(或2 ^ 64)位,因为输出没有更多的可能性?

In order to generate the seed for the RNG, I wanted to do something like this: 为了生成RNG的种子,我想做这样的事情:

var randomBytes = generateRandomBytes(226);

function generateRandomBytes(n) {
    var data = new Uint8Array(
        Math.ceil(n / 8)
    );
    window.crypto.getRandomValues(data);

    return data;
}

Is this correct? 它是否正确? I don't see how I could pass randomBytes to the RNG as a seed in any way, and I don't know how I could modify it to accep this. 我不知道如何以任何方式将randomBytes作为种子传递给RNG,我不知道如何修改它以接受它。

Here's a function I wrote that uses Fisher-Yates shuffling based on random bytes sourced from window.crypto . 这是我写的一个函数,它使用基于来自window.crypto随机字节的Fisher-Yates shuffling。 Since Fisher-Yates requires that random numbers are generated over varying ranges, it starts out with a 6-bit mask ( mask=0x3f ), but gradually reduces the number of bits in this mask as the required range gets smaller (ie, whenever i is a power of 2). 由于Fisher-Yates要求在不同的范围内生成随机数,因此它以6位掩码( mask=0x3f )开始,但随着所需范围变小(即每当i变小),逐渐减少此掩码中的位数是2)的力量。

function shuffledeck() {
    var cards = Array("A♣️","2♣️","3♣️","4♣️","5♣️","6♣️","7♣️","8♣️","9♣️","10♣️","J♣️","Q♣️","K♣️",
                      "A♦️","2♦️","3♦️","4♦️","5♦️","6♦️","7♦️","8♦️","9♦️","10♦️","J♦️","Q♦️","K♦️",
                      "A♥️","2♥️","3♥️","4♥️","5♥️","6♥️","7♥️","8♥️","9♥️","10♥️","J♥️","Q♥️","K♥️",
                      "A♠️","2♠️","3♠️","4♠️","5♠️","6♠️","7♠️","8♠️","9♠️","10♠️","J♠️","Q♠️","K♠️");
    var rndbytes = new Uint8Array(100);
    var i, j, r=100, tmp, mask=0x3f;

    /* Fisher-Yates shuffle, using uniform random values from window.crypto */
    for (i=51; i>0; i--) {
        if ((i & (i+1)) == 0) mask >>= 1;
        do {
            /* Fetch random values in 100-byte blocks. (We probably only need to do */
            /* this once.) The `mask` variable extracts the required number of bits */
            /* for efficient discarding of random numbers that are too large. */
            if (r == 100) {
                window.crypto.getRandomValues(rndbytes);
                r = 0;
            }
            j = rndbytes[r++] & mask;
        } while (j > i);

        /* Swap cards[i] and cards[j] */
        tmp = cards[i];
        cards[i] = cards[j];
        cards[j] = tmp;
    }
    return cards;
}

An assessment of window.crypto libraries really deserves its own question, but anyway... window.crypto库的评估确实值得拥有自己的问题,但无论如何......

The pseudorandom stream provided by window.crypto.getRandomValues() should be sufficiently random for any purpose, but is generated by different mechanisms in different browsers. window.crypto.getRandomValues()提供的伪随机流应该足够随机用于任何目的,但是由不同浏览器中的不同机制生成。 According to a 2013 survey : 根据2013年的一项调查

  • Firefox (v. 21+) uses NIST SP 800-90 with a 440-bit seed. Firefox (v.21 +)使用带有440位种子的NIST SP 800-90 Note: This standard was updated in 2015 to remove the (possibly backdoored) Dual_EC_DRBG elliptic curve PRNG algorithm. 注意:此标准于2015年更新,以删除(可能是后向的) Dual_EC_DRBG椭圆曲线PRNG算法。

  • Internet Explorer (v. 11+) uses one of the algorithms supported by BCryptGenRandom (seed length = ?) Internet Explorerv.11 + )使用BCryptGenRandom支持的算法之一 (种子长度=?)

  • Safari, Chrome and Opera use an ARC4 stream cipher with a 1024-bit seed. Safari,Chrome和Opera使用带有1024位种子的ARC4流密码。


Edit: 编辑:

A cleaner solution would be to add a generic shuffle() method to Javascript's array prototype: 更简洁的解决方案是将一个通用的shuffle()方法添加到Javascript的数组原型中:

// Add Fisher-Yates shuffle method to Javascript's Array type, using
// window.crypto.getRandomValues as a source of randomness.

if (Uint8Array && window.crypto && window.crypto.getRandomValues) {
    Array.prototype.shuffle = function() {
        var n = this.length;

        // If array has <2 items, there is nothing to do
        if (n < 2) return this;
        // Reject arrays with >= 2**31 items
        if (n > 0x7fffffff) throw "ArrayTooLong";

        var i, j, r=n*2, tmp, mask;
        // Fetch (2*length) random values
        var rnd_words = new Uint32Array(r);
        // Create a mask to filter these values
        for (i=n, mask=0; i; i>>=1) mask = (mask << 1) | 1;

        // Perform Fisher-Yates shuffle
        for (i=n-1; i>0; i--) {
            if ((i & (i+1)) == 0) mask >>= 1;
            do {
                if (r == n*2) {
                    // Refresh random values if all used up
                    window.crypto.getRandomValues(rnd_words);
                    r = 0;
                }
                j = rnd_words[r++] & mask;
            } while (j > i);
            tmp = this[i];
            this[i] = this[j];
            this[j] = tmp;
        }
        return this;
    }
} else throw "Unsupported";

// Example:
deck = [ "A♣️","2♣️","3♣️","4♣️","5♣️","6♣️","7♣️","8♣️","9♣️","10♣️","J♣️","Q♣️","K♣️",
         "A♦️","2♦️","3♦️","4♦️","5♦️","6♦️","7♦️","8♦️","9♦️","10♦️","J♦️","Q♦️","K♦️",
         "A♥️","2♥️","3♥️","4♥️","5♥️","6♥️","7♥️","8♥️","9♥️","10♥️","J♥️","Q♥️","K♥️",
         "A♠️","2♠️","3♠️","4♠️","5♠️","6♠️","7♠️","8♠️","9♠️","10♠️","J♠️","Q♠️","K♠️"];

deck.shuffle();

Combining this answer from here with this answer from another question , it seems the following could be a more general and modular (though less optimized) version: 结合这个答案从这里用这个答案另一个问题 ,似乎下面可能是一个更普遍的和模块化的(虽然少了优化)版本:

// Fisher-Yates
function shuffle(array) {
    var i, j;

    for (i = array.length - 1; i > 0; i--) {
        j = randomInt(0, i + 1);
        swap(array, i, j);
    }
}

// replacement for:
//     Math.floor(Math.random() * (max - min)) + min
function randomInt(min, max) {
    var range = max - min;
    var bytesNeeded = Math.ceil(Math.log2(range) / 8);
    var randomBytes = new Uint8Array(bytesNeeded);
    var maximumRange = Math.pow(Math.pow(2, 8), bytesNeeded);
    var extendedRange = Math.floor(maximumRange / range) * range;
    var i, randomInteger;

    while (true) {
        window.crypto.getRandomValues(randomBytes);
        randomInteger = 0;

        for (i = 0; i < bytesNeeded; i++) {
            randomInteger <<= 8;
            randomInteger += randomBytes[i];
        }

        if (randomInteger < extendedRange) {
            randomInteger %= range;

            return min + randomInteger;
        }
    }
}

function swap(array, first, second) {
    var temp;

    temp = array[first];
    array[first] = array[second];
    array[second] = temp;
}

I personally think you could move outside the box a little bit. 我个人认为你可以稍微移出盒子。 If you're that worried about randomness, you could look into an API key from random.org ( https://api.random.org/json-rpc/1/ ) or parse it out of a link like this: https://www.random.org/integer-sets/?sets=1&num=52&min=1&max=52&seqnos=on&commas=on&order=index&format=html&rnd=new . 如果您担心随机性,可以从random.org( https://api.random.org/json-rpc/1/ )查看API密钥,或者通过以下链接解析它: https: //www.random.org/integer-sets/?sets=1&num=52&min=1&max=52&seqnos=on&commas=on&order=index&format=html&rnd=new

Sure, your datasets could be intercepted, but if you get a few hundred thousand sets of them then shuffle those sets you would be fine. 当然,您的数据集可能会被拦截,但如果您获得了数十万套数据集,那么随机播放这些数据集就可以了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM