Para sortear uma frase aleatória em JavaScript, basta uma linha: lista[Math.floor(Math.random() * lista.length)]. O problema aparece com o tempo: num sorteio que se repete a cada poucos segundos, a mesma frase volta muito antes do que a intuição sugere. Para sortear sem repetir, a técnica padrão é embaralhar a lista uma vez, com o algoritmo de Fisher-Yates, e tirar as frases em ordem, como cartas de um baralho.
Este tutorial mostra as duas coisas com um caso real: o sistema de provocações do Navistron, um jogo de nave em canvas e JavaScript puro. A cada 10 segundos de partida, o jogo solta uma frase no meio da tela — "Tá achando fácil?", "Meu avô jogava melhor", "Licença de piloto: cassada". O código é o do arquivo do jogo, conferido em 11/10/2026 (só com a formatação ajustada), e as probabilidades foram calculadas em script.
1. O sistema de provocações do Navistron
O sistema cabe em três peças: uma lista de frases, um temporizador e um elemento de texto. As frases ficam em duas constantes — uma fixa, que abre a sequência, e uma lista de 102 frases para o sorteio:
const TAUNT_FIRST = 'Tá achando fácil?';const TAUNTS = ['Vai levar pedrada', 'Logo esse jogo acaba', 'Você não vai aguentar muito mais tempo', /* ... 102 frases */];let tauntTimer = 10, tauntIndex = 0;
A cada quadro, enquanto a partida está rodando, o laço do jogo desconta o tempo do quadro e, quando o temporizador zera, mostra a próxima frase:
tauntTimer -= dt;if (tauntTimer <= 0) {tauntTimer = 10;const el = document.getElementById('taunt-text');if (el) {const phrase = tauntIndex === 0 ? TAUNT_FIRST : TAUNTS[Math.floor(Math.random() * TAUNTS.length)];tauntIndex++;el.textContent = phrase;el.classList.add('visible');setTimeout(() => { el.classList.remove('visible'); }, 3500);}}
- O temporizador anda com o dt. Ele desconta o tempo real de cada quadro e só roda com a partida em andamento, sem contar a contagem regressiva. Por isso a primeira frase sai aos 10 segundos de partida, e as outras a cada 10 segundos com a nave viva.
- A primeira frase é fixa. Toda partida que passa dos 10 segundos ouve "Tá achando fácil?": uma abertura fixa funciona como assinatura e ensina o tom da brincadeira.
- O resto é sorteio puro. O índice vai de 0 a 101, com a mesma chance para cada um, sem memória do sorteio anterior.
- A frase fica 3,5 segundos. O
setTimeouttira a classevisible, e o CSS anima a entrada e a saída.
2. Como sortear um índice do jeito certo
Math.random() devolve um número de 0 até quase 1: pode dar 0, nunca dá 1. Multiplicado pelo tamanho da lista e arredondado para baixo, vira um índice de 0 até o último, todos com a mesma chance. Três variações comuns parecem equivalentes e não são:
- Math.round no lugar de Math.floor:
Math.round(Math.random() * (n - 1))dá ao primeiro e ao último item metade da chance dos outros, porque cada um só recebe meio intervalo de arredondamento. - Multiplicar por n - 1 com Math.floor:
Math.floor(Math.random() * (n - 1))nunca sorteia o último item. - Math.round com n:
Math.round(Math.random() * n)às vezes devolve n, um índice que não existe, e a frase sai como undefined.
Para frases de jogo, Math.random() basta; para segurança (senhas, tokens, sorteios com prêmio), use crypto.getRandomValues(). E, se o sorteio precisar se repetir igual, num replay ou num vídeo gerado quadro a quadro, use um gerador com semente, como o do artigo sobre animação determinística no canvas.
3. Quanto o sorteio puro repete
Com 102 frases, a chance de repetir parece pequena: 1 em 102 a cada sorteio. Mas o que conta é a partida inteira, e aí entra o paradoxo do aniversário — o mesmo que dá a duas pessoas, numa sala com 23, mais de 50% de chance de fazer aniversário no mesmo dia. Depois de k sorteios, a chance de alguma frase repetida é 1 menos o produto de (102 − i) / 102, para i de 0 a k − 1. Como a primeira frase é fixa, uma partida com p provocações tem p − 1 sorteios:
| Duração da partida | Provocações | Chance de alguma frase repetida | Chance de repetir em seguida |
|---|---|---|---|
| 1:00 | 6 | 9,5% | 3,9% |
| 2:02 (a média do jogo) | 12 | 42,8% | 9,4% |
| 2:20 | 14 | 55,0% | 11,2% |
| 3:00 | 18 | 75,6% | 14,6% |
| 4:00 | 24 | 93,2% | 19,5% |
| 5:14 (a mais longa da história) | 31 | 99,1% | 24,9% |
A média de 2min02 e a partida de 5min14 vêm da telemetria pública do jogo, lida em 11/10/2026; a mais longa é a do piloto VASCO DA GAMA, em 07/10/2026, também o recorde de pontos do ranking. A partir dos 2min20, a repetição já é mais provável do que não; numa partida como a do recorde, é quase certa. Para uma piada, isso pesa: na segunda vez que o jogador lê a mesma provocação, ela perde a graça.
4. Sortear sem repetir: embaralhar e tirar em ordem
A forma mais simples de não repetir até a lista acabar é tratá-la como um baralho: embaralhar uma vez e tirar uma carta por vez. O embaralhamento correto é o de Fisher-Yates, que percorre a lista de trás para a frente e troca cada posição com outra sorteada entre o começo e ela mesma:
function embaralhar(lista) {const a = lista.slice();for (let i = a.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));[a[i], a[j]] = [a[j], a[i]];}return a;}
Todas as ordens possíveis saem com a mesma chance, numa única passada pela lista. Com o baralho pronto, a função que entrega a próxima frase só reembaralha quando ele acaba — a técnica conhecida como saco de sorteio (shuffle bag):
let saco = [], ultima = null;function proximaFrase() {if (saco.length === 0) {saco = embaralhar(TAUNTS);const fim = saco.length - 1;if (saco[fim] === ultima) [saco[0], saco[fim]] = [saco[fim], saco[0]];}ultima = saco.pop();return ultima;}
A troca dentro do if evita que a última frase de um baralho seja a primeira do seguinte (o pop() tira do fim). Num sistema como o do Navistron, bastaria trocar o sorteio direto por proximaFrase(): com 102 frases e uma a cada 10 segundos, um baralho dura 17 minutos de partida, mais de três vezes a partida mais longa já jogada.
Evite embaralhar com lista.sort(() => Math.random() - 0.5): o resultado depende do algoritmo de ordenação do navegador e não dá a mesma chance a todas as ordens.
5. Alternativa: proibir só as últimas frases
O baralho tem um efeito colateral: perto do fim, a próxima frase fica previsível para quem conta. Com 102 frases ninguém conta, mas com listas curtas — as 5 ou 6 falas de um personagem — isso aparece. A alternativa é sortear livremente e só proibir as últimas N frases:
const recentes = [];function proximaFraseLivre(n = 10) {let frase;do { frase = TAUNTS[Math.floor(Math.random() * TAUNTS.length)]; } while (recentes.includes(frase));recentes.push(frase);if (recentes.length > n) recentes.shift();return frase;}
Com N = 10 e 102 frases, o laço acerta de primeira em mais de 90% das vezes, e no ritmo do Navistron passariam pelo menos 110 segundos entre duas aparições da mesma frase. N precisa ser menor que o tamanho da lista, senão o laço nunca termina.
| Método | Repetição | Memória | Quando usar |
|---|---|---|---|
| Sorteio puro | Pode repetir a qualquer momento, até em seguida | Nenhuma | Listas grandes e sessões curtas |
| Saco embaralhado (Fisher-Yates) | Nenhuma até a lista acabar | Uma cópia da lista | Quando nada pode repetir na partida |
| Proibir as últimas N | Nada volta antes de N outras | N itens | Listas curtas e sorteio com cara de livre |
6. Mostrar a frase na tela
A parte visual do sistema do Navistron é só CSS. O texto fica num elemento fixo no meio da tela, com pointer-events: none para não atrapalhar o controle da nave. No estado normal, tem opacidade 0, escala 0,92 e uma transição de 1,2 segundo; com a classe visible, ganha opacidade 1, escala 1 e branco a 70%, com brilho ciano no text-shadow. Basta pôr e tirar a classe, e o navegador anima o resto.
O texto é monoespaçado, em caixa alta, com 3 px entre as letras e white-space: nowrap, com reticências no que passar de 90% da largura da tela. Com fonte de 11 px, cada caractere ocupa cerca de 9,6 px, e numa tela de celular de 390 px cabem uns 36. Se o texto não pode quebrar linha, escreva frases curtas: a média das 102 do sorteio do Navistron é de 23 caracteres.
Outro detalhe: o setTimeout conta tempo real. Para mensagens que precisam acompanhar o jogo, como um aviso de fase, desconte o dt no laço, como o temporizador das provocações faz.
7. Erros comuns
- Arredondar com Math.round: as pontas da lista saem com metade da chance.
- Embaralhar com sort: a ordem sai enviesada; use Fisher-Yates.
- Contar quadros em vez de tempo: o ritmo muda com a taxa de quadros da tela, como mostra o artigo sobre dificuldade progressiva em JavaScript.
- Frases longas sem quebra de linha: o fim da piada some na tela pequena.
Se o seu jogo ainda está no começo, o artigo sobre como criar um jogo 2D em JavaScript com canvas monta o laço de jogo em que esse sistema se encaixa, e o de feedback visual de dano mostra outra forma de o jogo conversar com quem joga. Para ver as provocações ao vivo, é só jogar uma partida — grátis, no navegador, sem login — e passar dos 10 segundos. Os números do jogo saem todo dia no Instagram do Navistron.
FAQ
Como sortear um item aleatório de um array em JavaScript?
Multiplique Math.random() pelo tamanho do array, arredonde para baixo com Math.floor e use o resultado como índice. Como Math.random devolve um número de 0 até quase 1, o índice vai de 0 ao último item, com a mesma chance para cada um.
Como sortear sem repetir em JavaScript?
Embaralhe a lista com o algoritmo de Fisher-Yates e tire os itens em ordem, um por vez. Quando a lista acabar, embaralhe de novo. Nenhum item se repete até todos terem saído.
Por que não embaralhar com sort e Math.random?
Porque o resultado depende do algoritmo de ordenação do navegador e não dá a mesma chance a todas as ordens. O Fisher-Yates é quase tão curto e embaralha de forma uniforme.
Quantas provocações uma partida do Navistron tem?
Uma a cada 10 segundos de partida: a primeira, aos 10 segundos, é sempre "Tá achando fácil?", e as seguintes são sorteadas entre 102 frases. Uma partida de 2min02, a média do jogo em 11/10/2026, tem 12; a mais longa da história, de 5min14, teve 31.
