Um ranking online transforma qualquer jogo casual em uma competição global. A diferença entre "jogar e fechar" e "jogar, salvar, comparar e voltar para tentar de novo" é enorme — e essa diferença é o leaderboard. Neste guia, mostramos como construir um sistema de ranking completo para jogos web, usando a implementação real do Navistron como referência passo a passo.

O sistema que vamos construir inclui: API REST para salvar e buscar scores, MongoDB como banco de dados, sanitização no servidor, rankings filtrados por período (geral/semanal/mensal), perfis individuais de jogador, e páginas com ISR para performance — tudo funcionando em produção com centenas de jogadores.

Arquitetura Geral do Sistema de Ranking

Antes de escrever código, é fundamental entender a arquitetura. O ranking do Navistron segue um fluxo simples e robusto:

  1. Jogo termina → o cliente coleta os dados da partida (score, tier, tempo, boosts, spread, dificuldade)
  2. Jogador digita nome → campo de texto com máximo de 20 caracteres (ou "ANONYMOUS" se vazio)
  3. POST /api/scores → os dados são enviados para a API REST como JSON
  4. API sanitiza e salva → valida tipos, limpa strings, gera timestamp no servidor, grava no MongoDB
  5. GET /api/scores → busca os top 100 scores ordenados por pontuação decrescente
  6. Leaderboard renderizado → tabela com ranking, medalhas para top 3, score do jogador destacado

Para jogadores que não querem salvar (pulam o nome), os dados da partida são enviados para uma coleção separada de sessões anônimas — garantindo que a telemetria capture 100% das partidas, não apenas as do ranking.

Modelo de Dados: O Que Salvar em Cada Score

O registro de score no MongoDB do Navistron armazena 9 campos por partida:

CampoTipoDescriçãoExemplo
nameStringNome do piloto (uppercase, max 20 chars)"STARFIGHTER"
scoreNumberPontuação total (inteiro)4280
tierNumberÍndice do tier alcançado (0–6)3
tierNameStringNome legível do tier"TIER IV"
boostsNumberTotal de boosts coletados14
spreadNumberNível de spread no momento da morte3
timeNumberTempo sobrevivido em segundos (inteiro)87
difficultyNumberMultiplicador de dificuldade na morte5.42
playedAtDateTimestamp gerado pelo servidor2025-04-20T...

Decisão importante: o campo playedAt é gerado pelo servidor (new Date() na API), nunca pelo cliente. Isso impede manipulação de datas e garante consistência.

API REST: Endpoints GET e POST

O ranking inteiro funciona com apenas dois endpoints na mesma rota:

GET /api/scores — busca o top 100:

  • Query MongoDB: find({}).sort({ score: -1 }).limit(100)
  • Serializa _id para string (id) e playedAt para ISO string
  • Retorna JSON array com status 200, ou { error: '...' } com status 500

POST /api/scores — salva um novo score:

  • Recebe JSON body do cliente
  • Sanitiza cada campo com conversão de tipo e fallbacks: String(name).toUpperCase().slice(0, 20), Number(score) || 0
  • Adiciona playedAt: new Date() (timestamp do servidor)
  • Executa insertOne(record) no MongoDB
  • Retorna o registro criado com status 200

Essa simplicidade é intencional. Dois endpoints, sem autenticação complexa, sem OAuth, sem JWT — para um jogo arcade casual, essa abordagem funciona perfeitamente.

Sanitização no Servidor: Proteção Essencial

Mesmo sem autenticação, a API precisa se proteger de dados malformados. A sanitização do Navistron segue 4 regras:

  1. Conversão de tipo forçada — Todo campo numérico passa por Number() com fallback || 0. Se alguém enviar "abc" como score, vira 0
  2. String normalizada — O nome é convertido para maiúsculas e truncado em 20 caracteres. Se vazio, recebe "ANONYMOUS"
  3. Timestamp do servidor — O cliente não controla a data. playedAt é sempre new Date() gerado no momento da inserção
  4. Sem campos extras — O record é construído campo a campo pela API, não copiado direto do body. Campos desconhecidos são ignorados

MongoDB: Índices para Performance

Um ranking que consulta o top 100 por score precisa de índices otimizados. Sem eles, o MongoDB faz scan completo da coleção em cada busca. O Navistron cria 3 índices na coleção scores:

ÍndiceDireçãoPara quê
scoreDescendente (-1)Ordenação do ranking global (sort by score DESC)
playedAtDescendente (-1)Filtro por período (semanal, mensal) e jogos recentes
nameAscendente (1)Busca de perfis de jogador e listagem de pilotos

Esses 3 índices cobrem 100% dos padrões de consulta do sistema: ranking geral, ranking por período, perfil de jogador, e listagem de nomes únicos. O script de seed (npm run seed) cria automaticamente todos os índices.

Rankings por Período: Semanal e Mensal

Um ranking "de todos os tempos" favorece jogadores antigos. Rankings filtrados por período renovam a competição — quem não pode ser o #1 global pode tentar ser o #1 da semana.

A implementação é elegante: a mesma função de busca aceita um parâmetro period que adiciona um filtro de data:

  • "weekly"{ playedAt: { $gte: 7 dias atrás } }
  • "monthly"{ playedAt: { $gte: 30 dias atrás } }
  • "all" → sem filtro (todos os tempos)

O índice playedAt: -1 garante que essas queries filtradas sejam rápidas. No ranking do Navistron, o jogador navega entre as abas "Geral", "Semanal" e "Mensal" com um clique.

Perfis de Jogador: Além do Ranking

Um ranking mostra posição. Um perfil de jogador mostra a história completa. No Navistron, cada piloto tem uma página individual com 9 estatísticas calculadas por aggregation pipelines do MongoDB:

  • Melhor Score$max: '$score'
  • Ranking Global — Conta quantos jogadores têm best score maior, +1 = sua posição
  • Total de Jogos$sum: 1
  • Score Médio$avg: '$score'
  • Melhor Tier$max: '$tier'
  • Tempo Total de Jogo$sum: '$time'
  • Tempo Médio por Partida$avg: '$time'
  • Total de Boosts$sum: '$boosts'
  • Máximo Spread$max: '$spread'

O cálculo do ranking global é o mais interessante: uma aggregation agrupa todos os jogadores pelo best score, conta quantos têm score maior que o do jogador atual, e soma 1. Isso dá a posição exata sem manter uma tabela de ranking separada.

ISR: Performance de Ranking com Dados Frescos

Rankings precisam ser rápidos (ninguém espera 3 segundos para ver um leaderboard) e atualizados (um score salvo há 1 minuto deve aparecer). ISR (Incremental Static Regeneration) do Next.js resolve esse paradoxo.

As páginas de ranking do Navistron usam revalidate = 300 (5 minutos). Isso significa:

  1. A primeira visita gera a página com dados do MongoDB e a cacheia
  2. Nos próximos 5 minutos, todas as visitas recebem a versão cacheada (instantâneo)
  3. Após 5 minutos, a próxima visita dispara uma regeneração em background
  4. A versão atualizada substitui o cache — sem downtime

Perfis de jogador usam revalidate = 3600 (1 hora) porque mudam com menos frequência. E até 500 perfis são pré-gerados no build via generateStaticParams — os primeiros 500 nomes de piloto recebem páginas estáticas prontas.

Componente de Tabela: Medalhas, Cores e Links

O componente RankingTable renderiza 8 colunas para cada score:

ColunaConteúdoDestaque Visual
#Posição no ranking🥇🥈🥉 para top 3 (com classes CSS gold/silver/bronze)
PilotoNome (link para perfil)Texto clicável para /player/NOME
ScorePontuação formatadaCor ciano (#7df4ff), alinhado à direita
TempoDuração (M:SS)Alinhado à direita
TierNome do tierCor específica do tier (ciano→verde→amarelo→...→branco)
BoostsTotal coletadoAlinhado à direita
SpreadNível alcançadoAlinhado à direita
DataDia e mêsFormato pt-BR ("20 abr")

Cada tier tem sua própria cor mapeada: TIER I = #7df4ff (ciano), TIER II = #80ff90 (verde), TIER III = #ffdd00 (amarelo), TIER IV = #ff8c00 (laranja), TIER V = #ff4488 (rosa), TIER VI = #cc44ff (roxo), TIER VII = #ffffff (branco).

Fluxo do Game Over: Salvar, Pular ou Abandonar

O momento de salvar o score precisa cobrir 3 cenários:

  1. "Salvar & Ranking" — Jogador digita nome, clica salvar. O registro vai para scores via POST. O leaderboard abre com o score destacado e auto-scrollado até a posição
  2. "Skip" — Jogador não quer aparecer no ranking. O registro vai para anonymous_sessions com type: 'Unregistered'. O leaderboard abre sem destaque
  3. Abandonar — Jogador fecha a aba ou começa novo jogo sem clicar nada. O registro é salvo via navigator.sendBeacon() com type: 'Unknown' — uma API que funciona mesmo durante beforeunload

Resultado: 100% das partidas são capturadas. Scores nomeados vão para o ranking; anônimos e abandonados vão para a telemetria. Nenhum dado se perde.

Telemetria: Dados Além do Ranking

Um ranking mostra quem é o melhor. A telemetria mostra tudo que acontece no jogo. A API de stats do Navistron executa 10 queries paralelas (Promise.all) para calcular:

  • Total de partidas (registradas + anônimas)
  • Pilotos únicos e jogadores ativos na semana
  • Score médio, score mais alto, tempo médio
  • Jogos por dia (gráfico de 30 dias, com gap-fill para dias sem partidas)
  • Horários de pico (distribuição por hora, 0-23h)
  • Distribuição de tiers alcançados
  • Distribuição de scores em faixas (0-50, 50-100, ..., 10K+)
  • Top 10 jogadores com stats agregadas
  • Últimas 10 partidas

Esses dados são públicos no Navistron — qualquer pessoa pode acompanhar o crescimento do jogo, a distribuição de habilidade dos jogadores e os horários mais movimentados.

FAQ — Perguntas Frequentes sobre Ranking Online em Jogos

Preciso de autenticação para um ranking de jogo web?

Para jogos arcade casuais, não necessariamente. O Navistron opera sem autenticação — o jogador digita um nome de até 20 caracteres e salva. A sanitização no servidor (tipo forçado, timestamp do servidor, truncamento de string) oferece proteção suficiente. Para jogos com economia real ou prêmios, autenticação é recomendada.

MongoDB ou PostgreSQL para rankings?

Ambos funcionam. O MongoDB é excelente para rankings porque: documentos flexíveis (adicionar campos como "arma usada" ou "mapa" sem migração), aggregation pipelines poderosas para stats, e o driver nativo para Node.js é leve. PostgreSQL é melhor se você precisa de transações complexas ou relações entre tabelas.

Como evitar scores falsos sem autenticação?

Sanitização no servidor é a primeira defesa: tipos forçados, timestamps gerados pela API, e campos construídos individualmente (não copiados do body). Para proteção adicional, considere: rate limiting por IP, validação de plausibilidade (score máximo teórico baseado em tempo), e honeypot fields.

O que é ISR e por que usar no ranking?

ISR (Incremental Static Regeneration) gera páginas estáticas que se atualizam periodicamente em background. No ranking do Navistron, revalidate = 300 significa que a página é cacheada por 5 minutos — carrega instantaneamente, mas ainda mostra dados recentes. É o equilíbrio perfeito entre performance e frescor dos dados.

Como calcular o ranking global de um jogador específico?

O Navistron usa uma aggregation pipeline elegante: agrupa todos os jogadores pelo melhor score, filtra os que têm score maior que o do jogador consultado, e conta. Posição = contagem + 1. Isso funciona sem manter uma tabela de ranking separada e está sempre atualizado. Veja como funciona na explicação detalhada do ranking.