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:
- Jogo termina → o cliente coleta os dados da partida (score, tier, tempo, boosts, spread, dificuldade)
- Jogador digita nome → campo de texto com máximo de 20 caracteres (ou "ANONYMOUS" se vazio)
- POST /api/scores → os dados são enviados para a API REST como JSON
- API sanitiza e salva → valida tipos, limpa strings, gera timestamp no servidor, grava no MongoDB
- GET /api/scores → busca os top 100 scores ordenados por pontuação decrescente
- 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:
| Campo | Tipo | Descrição | Exemplo |
|---|---|---|---|
name | String | Nome do piloto (uppercase, max 20 chars) | "STARFIGHTER" |
score | Number | Pontuação total (inteiro) | 4280 |
tier | Number | Índice do tier alcançado (0–6) | 3 |
tierName | String | Nome legível do tier | "TIER IV" |
boosts | Number | Total de boosts coletados | 14 |
spread | Number | Nível de spread no momento da morte | 3 |
time | Number | Tempo sobrevivido em segundos (inteiro) | 87 |
difficulty | Number | Multiplicador de dificuldade na morte | 5.42 |
playedAt | Date | Timestamp gerado pelo servidor | 2025-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
_idpara string (id) eplayedAtpara 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:
- 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 - String normalizada — O nome é convertido para maiúsculas e truncado em 20 caracteres. Se vazio, recebe "ANONYMOUS"
- Timestamp do servidor — O cliente não controla a data.
playedAté semprenew Date()gerado no momento da inserção - 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:
| Índice | Direção | Para quê |
|---|---|---|
score | Descendente (-1) | Ordenação do ranking global (sort by score DESC) |
playedAt | Descendente (-1) | Filtro por período (semanal, mensal) e jogos recentes |
name | Ascendente (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:
- A primeira visita gera a página com dados do MongoDB e a cacheia
- Nos próximos 5 minutos, todas as visitas recebem a versão cacheada (instantâneo)
- Após 5 minutos, a próxima visita dispara uma regeneração em background
- 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:
| Coluna | Conteúdo | Destaque Visual |
|---|---|---|
| # | Posição no ranking | 🥇🥈🥉 para top 3 (com classes CSS gold/silver/bronze) |
| Piloto | Nome (link para perfil) | Texto clicável para /player/NOME |
| Score | Pontuação formatada | Cor ciano (#7df4ff), alinhado à direita |
| Tempo | Duração (M:SS) | Alinhado à direita |
| Tier | Nome do tier | Cor específica do tier (ciano→verde→amarelo→...→branco) |
| Boosts | Total coletado | Alinhado à direita |
| Spread | Nível alcançado | Alinhado à direita |
| Data | Dia e mês | Formato 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:
- "Salvar & Ranking" — Jogador digita nome, clica salvar. O registro vai para
scoresvia POST. O leaderboard abre com o score destacado e auto-scrollado até a posição - "Skip" — Jogador não quer aparecer no ranking. O registro vai para
anonymous_sessionscomtype: 'Unregistered'. O leaderboard abre sem destaque - Abandonar — Jogador fecha a aba ou começa novo jogo sem clicar nada. O registro é salvo via
navigator.sendBeacon()comtype: 'Unknown'— uma API que funciona mesmo durantebeforeunload
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.
