Shuffling a poker deck in JavaScript with window.crypto.getRandomValues

前端 未结 3 1243
死守一世寂寞
死守一世寂寞 2021-02-05 08:32

A poker deck has 52 cards and thus 52! or roughly 2^226 possible permutations.

Now I want to shuffle such a deck of cards perfectly, with truly

3条回答
  •  走了就别回头了
    2021-02-05 09:01

    Here's a function I wrote that uses Fisher-Yates shuffling based on random bytes sourced from window.crypto. 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 (i.e., whenever i is a power of 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...

    The pseudorandom stream provided by window.crypto.getRandomValues() should be sufficiently random for any purpose, but is generated by different mechanisms in different browsers. According to a 2013 survey:

    • Firefox (v. 21+) uses NIST SP 800-90 with a 440-bit seed. Note: This standard was updated in 2015 to remove the (possibly backdoored) Dual_EC_DRBG elliptic curve PRNG algorithm.

    • Internet Explorer (v. 11+) uses one of the algorithms supported by BCryptGenRandom (seed length = ?)

    • Safari, Chrome and Opera use an ARC4 stream cipher with a 1024-bit seed.


    Edit:

    A cleaner solution would be to add a generic shuffle() method to Javascript's array prototype:

    // 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();
    

提交回复
热议问题