Skip to content

Draw Algorithm (version v1)

How Spin the Wheel draws: a 32-byte seed, HMAC-SHA256 by counter, uniform integers by rejection and Fisher-Yates, with code to redo a draw.

This page describes version v1 of the algorithm, the same one that runs on the server and in your browser's verification. Changing any detail would create a v2; v1 is never altered, and every result states the version used.

1. The seed

Before each draw, the server generates 32 random bytes with the system's cryptographic generator. The seed is written as lowercase hexadecimal, 64 characters long. It is stored with the result and shown on the result link.

2. The byte stream

All the chance in a draw comes from a byte stream derived from the seed. Block number i, starting at zero, is the HMAC-SHA256 that uses the 32 seed bytes as the key and the counter i, written as 8 bytes in big-endian order, as the message. Each block has 32 bytes and is read as unsigned 32-bit big-endian integers, four bytes at a time. When a block runs out, the next one is used.

3. Uniform integers by rejection

To get an integer in [0, n), compute limit = floor(2^32 / n) * n. Read one 32-bit integer from the stream. If it is greater than or equal to the limit, discard it and read another. Otherwise, the result is the value modulo n. Rejection removes the bias of the remainder: every value has exactly the same chance.

4. Participant list

The list is normalized: each line is trimmed at both ends and empty lines are dropped. The order entered is preserved. The list fingerprint is the hexadecimal SHA-256 of the normalized lines joined by newlines.

5. Each tool

Coin flip: draw an integer in [0, 2). Zero is heads; one is tails.

Numbers with repeats: for each requested number, the result is the minimum plus a uniform integer in [0, size), where size = maximum - minimum + 1.

Numbers without repeats: a partial Fisher-Yates over the range, with swaps kept in a sparse map. For i from 0 to quantity - 1, draw j = i + a uniform integer in [0, size - i), swap position i with j, and the value at position i, added to the minimum, is the next number.

Names and wheel: the normalized list is shuffled with Fisher-Yates. For i from the last index down to 1, draw j in [0, i + 1) and swap positions i and j. For names, the winners are the first items of the shuffled list, in the requested quantity. For the wheel, the winner is the first item. The animation does not influence the result.

Teams: the list is shuffled the same way and dealt in rotation: the participant at position i goes to team number i modulo the number of teams. Teams differ by at most one participant.

6. Reference code

The code below redoes a draw from the seed, the list and the parameters published on the result link. It runs in browsers and in Node.js 20 or later, which provide crypto.subtle. It is a trimmed-down version of the Spin the Wheel source.

const unhex = (s: string) => Uint8Array.from(s.match(/../g) ?? [], (h) => parseInt(h, 16));

async function makeStream(seedHex: string) {
  const key = await crypto.subtle.importKey(
    'raw', unhex(seedHex), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
  );
  let counter = 0n;
  let block = new Uint8Array(0);
  let pos = 0;
  const u32 = async () => {
    if (pos + 4 > block.length) {
      const msg = new Uint8Array(8);
      new DataView(msg.buffer).setBigUint64(0, counter++); // big-endian
      block = new Uint8Array(await crypto.subtle.sign('HMAC', key, msg));
      pos = 0;
    }
    const v = new DataView(block.buffer, block.byteOffset + pos, 4).getUint32(0);
    pos += 4;
    return v;
  };
  // Uniform integer in [0, n) by rejection.
  return async (n: number) => {
    const limit = Math.floor(2 ** 32 / n) * n;
    let v = await u32();
    while (v >= limit) v = await u32();
    return v % n;
  };
}

// Fisher-Yates shuffle over the normalized list.
async function shuffle(list: string[], seedHex: string) {
  const next = await makeStream(seedHex);
  const a = list.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = await next(i + 1);
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

// Example: names (2 winners), teams (3 teams) and coin flip.
async function example(seedHex: string, list: string[]) {
  const shuffled = await shuffle(list, seedHex);
  const winners = shuffled.slice(0, 2);
  const teams: string[][] = Array.from({ length: 3 }, () => []);
  shuffled.forEach((p, i) => teams[i % 3].push(p)); // rotation
  const side = (await (await makeStream(seedHex))(2)) === 0 ? 'heads' : 'tails';
  return { winners, teams, side };
}

7. How to check

Open the result link, copy the seed, the participant list and the parameters, and run the code. If the tool, the algorithm version and the data are the same, the winners will be identical. The result page does this calculation for you with the verification button.

8. What the method guarantees, and what it does not

It guarantees that the result is reproducible by anyone and that, without knowing the seed beforehand, nobody can predict the winner. It does not guarantee that the list is honest: if someone leaves a participant out, the draw is fair only among those who remained. That is why the result link stores the list fingerprint and offers the name check.