O GGColors é um jogo de memória de sequências, da mesma família do clássico Genius (Simon, fora do Brasil). O jogo acende uma cor, depois duas, depois três, e você precisa repetir a sequência inteira na ordem certa. Errou uma cor, a partida acaba.
A regra cabe numa frase, mas o jogo tem três decisões técnicas que valem um artigo: não existe nenhum arquivo de áudio, toda a partida é controlada por uma máquina de estados pequena, e o jogo foi pensado para ser jogável sem mouse, sem visão e sem som. Este texto mostra como cada uma delas funciona no código que roda hoje no portal.
Jogue o GGColors aqui no portal antes de ler, porque os detalhes ficam mais claros depois de uma ou duas partidas.
Como uma partida funciona
Cada rodada é uma fase. Na fase 1 a sequência tem uma cor; ao acertar, o jogo acrescenta uma nova cor aleatória no fim da sequência e mostra tudo de novo desde o começo. A sequência só cresce, e é isso que torna o jogo difícil: na fase 10 você precisa lembrar dez cores em ordem.
- Cada cor da sequência acende por 400 ms, e uma nova cor começa a cada 1 segundo.
- Depois da última cor, o jogo espera 600 ms e libera a sua vez.
- Acertar a sequência toca um pequeno arpejo de vitória e, 1,5 s depois, começa a fase seguinte.
- Errar qualquer cor encerra a partida com a mensagem "Game Over" e informa em qual posição você errou ("Errou na cor 4 de 7").
Esses tempos parecem detalhe, mas definem a dificuldade. Um segundo por cor é lento o bastante para uma criança acompanhar e, ainda assim, fica exigente quando a sequência passa de oito ou nove cores.
Uma máquina de estados de cinco estados
Toda a lógica da partida vive numa classe com cinco estados possíveis:
| Estado | O que está acontecendo | O jogador pode clicar? |
|---|---|---|
idle | Antes de começar | Não |
showing | O jogo está exibindo a sequência | Não |
waiting | Vez do jogador | Sim |
success | Sequência completa, próxima fase a caminho | Não |
error | Cor errada, fim de partida | Não |
A regra mais importante do jogo está numa linha só: a entrada do jogador é ignorada em qualquer estado que não seja waiting.
handleInput(color) {
if (this.state !== 'waiting') {
return { success: false, complete: false, message: '' };
}
this.userInput.push(color);
const index = this.userInput.length - 1;
if (color !== this.sequence[index]) return this.handleError(index);
if (this.userInput.length === this.sequence.length) return this.handlePhaseComplete();
return { success: true, complete: false,
message: `Correta! Continue... ${this.userInput.length}/${this.sequence.length}` };
}Sem esse bloqueio, um clique apressado durante a exibição da sequência contaria como resposta, e o jogador perderia sem ter errado nada. Comparar a cor digitada com sequence[index] a cada clique, em vez de esperar a sequência inteira, também permite dar o retorno na hora e dizer exatamente onde foi o erro.
O problema dos timeouts órfãos
A exibição da sequência é uma cadeia de setTimeout: acender a cor, apagar, acender a próxima, liberar a vez do jogador. Se o jogador reinicia a partida no meio dessa cadeia, os timeouts antigos continuam agendados e, segundos depois, acendem cores de uma partida que já não existe.
A solução foi registrar todo timeout numa lista e cancelar a lista inteira sempre que a partida recomeça:
scheduleTimeout(fn, delay) {
const id = setTimeout(() => {
this.timeouts = this.timeouts.filter(t => t !== id);
fn();
}, delay);
this.timeouts.push(id);
}
clearTimeouts() {
this.timeouts.forEach(clearTimeout);
this.timeouts = [];
}start(), reset() e o tratamento de erro chamam clearTimeouts() antes de qualquer outra coisa. É um padrão pequeno que vale para qualquer jogo baseado em temporizadores: se você agenda, você precisa conseguir desagendar.
Todo som é gerado na hora, com Web Audio
O GGColors não carrega nenhum MP3 ou WAV. Cada nota é criada no momento em que toca, com a Web Audio API do navegador: um oscilador gera a onda e um nó de ganho controla o volume.
function playTone(frequency, duration, type = 'sine', volume = 0.3) {
const ctx = getAudioContext();
if (!ctx) return; // navegador sem Web Audio: o jogo segue sem som
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.frequency.value = frequency;
osc.type = type;
const now = ctx.currentTime;
gain.gain.setValueAtTime(0, now);
gain.gain.linearRampToValueAtTime(volume, now + 0.02); // ataque: 20 ms
gain.gain.setValueAtTime(volume, now + duration - 0.05);
gain.gain.exponentialRampToValueAtTime(0.01, now + duration); // saída: 50 ms
osc.start(now);
osc.stop(now + duration);
}Três vantagens aparecem na prática:
- Carregamento instantâneo. Não há arquivos de áudio para baixar antes de jogar.
- Nenhum estalo. Ligar e desligar um oscilador de uma vez produz um "clique" audível. A rampa de 20 ms na entrada e a queda exponencial nos últimos 50 ms eliminam esse ruído. A queda vai até
0.01, e não até zero, porqueexponentialRampToValueAtTimenão aceita zero como alvo. - Falha elegante. Se o navegador não oferece Web Audio, a função retorna sem fazer nada e o jogo continua funcionando, só que em silêncio.
As notas de cada cor
Cada cor tem uma frequência fixa, tocada numa onda senoidal (o timbre mais "limpo" que o oscilador oferece):
| Cor | Frequência | Teclas |
|---|---|---|
| Vermelho | 396 Hz | 1 ou Q |
| Verde | 528 Hz | 2 ou W |
| Azul | 639 Hz | 3 ou E |
| Amarelo | 417 Hz | 4 ou R |
Esses valores vêm de uma série de frequências conhecida como Solfeggio, escolhida aqui por um motivo musical: são quatro notas bem separadas entre si, fáceis de distinguir de ouvido, o que ajuda quem memoriza a sequência pelo som e não pela cor. A série costuma ser associada a efeitos terapêuticos, mas não há evidência científica desses efeitos, e o GGColors não faz nenhuma promessa desse tipo. No jogo, elas são apenas quatro notas agradáveis que não se confundem.
Os outros sons seguem a mesma ideia. O início da partida toca as quatro notas em sequência (396 → 417 → 528 → 639 Hz); acertar uma fase toca um arpejo ascendente (528 → 639 → 741 Hz); errar toca uma nota grave de 174 Hz, bem abaixo das notas das cores, para soar claramente como "fim".
Jogável sem mouse, sem visão e sem som
Um jogo de memória de cores parece, à primeira vista, impossível para quem não enxerga as cores ou não ouve os sons. O GGColors resolve isso oferecendo três canais redundantes, visual, sonoro e textual, para a mesma informação.
Teclado
Todas as ações têm atalho, e os atalhos ficam visíveis na tela:
1a4ouQ,W,E,Rpara as cores;EspaçoouEnterpara começar;Escapepara reiniciar.
Os dois conjuntos de teclas existem por conforto: números para quem joga com o teclado numérico, QWER para quem prefere deixar a mão esquerda parada na posição de jogo.
Leitor de tela
As mensagens do jogo ("Correta! Continue... 3/5", "Fase 4 completada!", "Game Over") ficam numa região com aria-live="polite" e aria-atomic="true". Na prática, o leitor de tela anuncia cada mensagem nova assim que ela aparece, sem interromper o que já estava lendo, e sempre lê a mensagem inteira, e não só o trecho que mudou.
Sem som
Cada nota é acompanhada de um indicador visual que pulsa junto com o som. Quem joga sem áudio, por escolha ou por deficiência auditiva, recebe a mesma informação pelos olhos.
Esses recursos seguem as diretrizes WCAG 2.1, principalmente as que pedem que toda funcionalidade esteja disponível pelo teclado e que nenhuma informação dependa de um único sentido.
O que dá para reaproveitar em outros jogos
- Bloqueie a entrada fora da vez do jogador com um estado explícito, em vez de flags espalhadas.
- Registre e cancele todos os timeouts; timeouts órfãos são uma das fontes mais comuns de bugs em jogos simples.
- Sintetize sons curtos com Web Audio: sem arquivos, sem espera e sem estalos, se você usar rampas de volume.
- Duplique toda informação importante em pelo menos dois canais (cor + som + texto).
O GGColors é pequeno, e justamente por isso é um bom lugar para ver essas ideias funcionando juntas. Jogue uma partida e tente passar da fase 10. Para ver outra forma de animar cartas e peças com CSS, leia também Virando cartas com CSS 3D.
