Ir para o conteúdo

Algoritmo de sorteio (versão v1)

Como o Sorteador sorteia: semente de 32 bytes, HMAC-SHA256 por contador, inteiros uniformes por rejeição e Fisher-Yates, com código para refazer.

Esta página descreve a versão v1 do algoritmo, a mesma que roda no servidor e na verificação do seu navegador. Mudar qualquer detalhe criaria uma versão v2; o v1 nunca é alterado, e cada resultado informa a versão usada.

1. A semente

Antes de cada sorteio, o servidor gera 32 bytes aleatórios com o gerador criptográfico do sistema. A semente é escrita em hexadecimal minúsculo, com 64 caracteres. Ela é gravada no resultado e exibida no link do resultado.

2. O fluxo de bytes

Todo o acaso do sorteio vem de um fluxo de bytes derivado da semente. O bloco de número i, começando em zero, é o HMAC-SHA256 que usa os 32 bytes da semente como chave e o contador i, escrito em 8 bytes na ordem big-endian, como mensagem. Cada bloco tem 32 bytes e é lido em inteiros de 32 bits sem sinal, big-endian, de quatro em quatro bytes. Quando o bloco acaba, passa-se ao seguinte.

3. Inteiros uniformes por rejeição

Para obter um inteiro em [0, n), calcula-se limite = floor(2^32 / n) * n. Lê-se um inteiro de 32 bits do fluxo. Se ele for maior ou igual ao limite, é descartado e outro é lido. Caso contrário, o resultado é o valor módulo n. A rejeição elimina o viés do resto da divisão: todos os valores têm exatamente a mesma chance.

4. Lista de participantes

A lista é normalizada: cada linha perde os espaços das pontas e as linhas vazias são descartadas. A ordem informada é preservada. A impressão digital da lista é o SHA-256, em hexadecimal, das linhas normalizadas unidas por quebras de linha.

5. Cada ferramenta

Cara ou coroa: sorteia-se um inteiro em [0, 2). Zero é cara; um é coroa.

Números com repetição: para cada número pedido, o resultado é o mínimo mais um inteiro uniforme em [0, tamanho), em que tamanho = máximo - mínimo + 1.

Números sem repetição: Fisher-Yates parcial sobre o intervalo, com as trocas guardadas num mapa esparso. Para i de 0 até quantidade - 1, sorteia-se j = i + inteiro uniforme em [0, tamanho - i), troca-se a posição i com a j, e o valor da posição i, somado ao mínimo, é o próximo número.

Nomes e roleta: a lista normalizada é embaralhada por Fisher-Yates. Para i do último índice até 1, sorteia-se j em [0, i + 1) e trocam-se as posições i e j. Em nomes, os vencedores são os primeiros itens da lista embaralhada, na quantidade pedida. Na roleta, o vencedor é o primeiro item. A animação não influencia o resultado.

Times: a lista é embaralhada do mesmo modo e distribuída em rodízio: o participante de posição i vai para o time de número i módulo a quantidade de times. Os times diferem em no máximo um participante.

6. Código de referência

O código abaixo refaz um sorteio a partir da semente, da lista e dos parâmetros publicados no link do resultado. Roda em navegadores e no Node.js 20 ou superior, que têm crypto.subtle. É uma versão enxuta do código-fonte do Sorteador.

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

async function criarFluxo(semente: string) {
  const chave = await crypto.subtle.importKey(
    'raw', unhex(semente), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
  );
  let contador = 0n;
  let bloco = new Uint8Array(0);
  let pos = 0;
  const u32 = async () => {
    if (pos + 4 > bloco.length) {
      const msg = new Uint8Array(8);
      new DataView(msg.buffer).setBigUint64(0, contador++); // big-endian
      bloco = new Uint8Array(await crypto.subtle.sign('HMAC', chave, msg));
      pos = 0;
    }
    const v = new DataView(bloco.buffer, bloco.byteOffset + pos, 4).getUint32(0);
    pos += 4;
    return v;
  };
  // Inteiro uniforme em [0, n) por rejeição.
  return async (n: number) => {
    const limite = Math.floor(2 ** 32 / n) * n;
    let v = await u32();
    while (v >= limite) v = await u32();
    return v % n;
  };
}

// Embaralhamento Fisher-Yates sobre a lista normalizada.
async function embaralhar(lista: string[], semente: string) {
  const proximo = await criarFluxo(semente);
  const a = lista.slice();
  for (let i = a.length - 1; i > 0; i--) {
    const j = await proximo(i + 1);
    [a[i], a[j]] = [a[j], a[i]];
  }
  return a;
}

// Exemplo: nomes (2 vencedores), times (3 times) e cara ou coroa.
async function exemplo(semente: string, lista: string[]) {
  const emb = await embaralhar(lista, semente);
  const vencedores = emb.slice(0, 2);
  const times: string[][] = Array.from({ length: 3 }, () => []);
  emb.forEach((p, i) => times[i % 3].push(p)); // rodízio
  const lado = (await (await criarFluxo(semente))(2)) === 0 ? 'cara' : 'coroa';
  return { vencedores, times, lado };
}

7. Como conferir

Abra o link do resultado, copie a semente, a lista de participantes e os parâmetros, e execute o código. Se a ferramenta, a versão do algoritmo e os dados forem os mesmos, os vencedores serão idênticos. A página do resultado faz esse cálculo por você no botão de verificação.

8. O que o método garante, e o que não garante

Garante que o resultado é reproduzível por qualquer pessoa e que, sem conhecer a semente de antemão, ninguém prevê o vencedor. Não garante que a lista seja honesta: se alguém deixa um participante de fora, o sorteio é justo apenas entre os que ficaram. Por isso o link do resultado guarda a impressão digital da lista e a conferência de nome.