Voltar para o Blog
6 minutos de leitura·20 de outubro de 2026

Truco online em tempo real com servidor autoritativo

Como eu construí o servidor de truco do PixTruco: motor de regras, Socket.IO, reconexão no celular, matchmaking sem corrida e dinheiro cobrado e pago uma única vez.

EC
Éverton Carvalho
Software Engineer | Software Architect | CTO
Truco online em tempo real com servidor autoritativo

Truco é um jogo de informação escondida e de blefe. Num jogo online com dinheiro, isso vira um requisito de engenharia: o cliente não pode saber nada que o jogador não veria na mesa. No PixTruco eu escrevi o servidor de jogo, o motor de regras, a carteira do truco e o app de jogo que roda dentro do site. Aqui conto as decisões principais.

O servidor manda, o cliente só vê

O cliente nunca recebe o estado bruto da partida. O servidor projeta uma visão por jogador: a própria mão, a contagem de cartas dos outros e as ações permitidas naquele momento, como jogar, pedir truco, responder ou decidir a mão de onze. Uma carta jogada coberta chega mascarada para todos, inclusive na tela de resultado da vaza. Até o prêmio exibido vem do acerto feito no servidor, e não de uma conta no cliente.

As exceções são as que a própria regra prevê: na mão de onze em duplas, quem decide vê a mão do parceiro, e na mão de ferro do Mineiro até a própria mão vem coberta.

sequenceDiagram
  participant A as Jogador A
  participant B as Jogador B
  participant S as Servidor de truco
  participant DB as PostgreSQL
  participant E as Espectadores
  A->>S: play-card
  S->>S: valida vez, carta e fase no motor
  S->>DB: snapshot versionado do estado
  S-->>A: game:state com a sua mão
  S-->>B: game:state só com a contagem de cartas de A
  S-->>E: estado neutro, 3 s depois

O motor de regras

O motor é TypeScript puro, sem framework. Cada ação pública, como jogar uma carta, pedir truco, responder ou desistir, valida tudo antes de alterar qualquer coisa e devolve sucesso ou o motivo do erro. Um bug que corrigi mostra por que isso importa: uma jogada inválida limpava a trava de "trucar duas vezes seguidas" antes de ser rejeitada, e bastava mandar jogadas inválidas em série para escalar a aposta.

As variantes entram como regras plugáveis. No Paulista, a manilha sai do vira e as apostas vão de 1 a 12; no Mineiro, as manilhas são fixas, as apostas são 2, 4, 6, 10 e 12 e existe a mão de ferro às cegas. Uma variante nova é um objeto novo, sem mexer no fluxo da partida. O baralho pode ser limpo ou sujo, e outro bug corrigido calculava a manilha com a ordem do baralho limpo também no sujo.

O tempo para decidir é do servidor: 15 segundos para jogar e 30 para truco ou mão de onze. O relógio é ligado a uma chave da situação da mesa, então reconectar ou receber o mesmo estado de novo não renova o prazo. Como ele entra no snapshot, nem um restart renova.

O protocolo em tempo real

Uso Socket.IO só com websocket, em dois canais: um para lobby, salas e convites, e outro para a partida, espectadores e torneios. Cada estado vai com o horário do servidor, e o cliente calcula o prazo local a partir dele, sem depender do relógio do aparelho. Depois de cada vaza, o servidor segura o resultado por 2 segundos antes de liberar o próximo estado jogável, para os dois lados começarem a contar pela mesma mensagem.

Espectadores ficam numa sala separada e recebem uma visão neutra, sem mãos, capturada no momento da publicação e entregue 3 segundos depois. Sinais e emotes saem de listas fixas; um sinal de equipe nunca vira público e espectador nunca recebe.

Quedas, abas e celular

  • Várias abas: vale um socket ativo por jogador na partida; a aba anterior é avisada de que foi substituída e comandos de sockets velhos são rejeitados.
  • Desconexão: o adversário é avisado e o jogador tem 45 segundos para voltar antes do W.O.; partidas contra bot nunca são abandonadas.
  • Celular em segundo plano: ao voltar, o app pede um token novo e entra na partida de novo de forma explícita, com reconexão infinita e teto de 10 segundos; um token que chega atrasado não abre a partida errada.
  • Timers: todos os callbacks de timer são protegidos para que um erro não derrube o processo, depois de um incidente em que um timer de revanche fez exatamente isso.
  • Ausência: se o jogador some, o servidor joga por ele de forma determinística e conservadora: carta mais à esquerda, corre do truco e nunca pede truco, porque não se automatiza aposta com dinheiro de quem está ausente.

Cada mudança de estado grava um snapshot versionado, com concorrência otimista para uma escrita atrasada nunca regredir o estado. Uma partida em andamento é restaurada sem redistribuir cartas, e no fim o buffer de eventos vira o replay.

Matchmaking sem corrida

A fila é persistida no banco, com um índice único parcial que impede o mesmo jogador de esperar duas vezes. Formar a partida acontece numa transação com advisory lock por sala: ler quem espera, escolher e marcar como pareado. Em duplas, a seleção prefere grupos de parceiros completos.

Dinheiro cobrado e pago uma vez

  • Buy-in: só é debitado quando todos conectam, na mesma transação que coloca a partida em jogo, com lock da conta e ordem fixa entre os jogadores para não haver deadlock.
  • Incidente: em produção, uma mesma partida gerou dois débitos por jogador porque uma partida encerrada era reaberta; hoje partidas encerradas ou anuladas devolvem o resultado antes de qualquer motor ser criado.
  • Acerto: o prêmio desconta o rake, e a última guarda é um UPDATE condicionado ao status em jogo; se nada for atualizado, a transação volta, o que impede pagar duas vezes mesmo entre réplicas.
  • Anulação: primeiro reivindica o status de anulada e só depois estorna, então nunca corre contra o acerto.
  • Reconciliação: a cada 10 minutos, partidas presas há mais de 2 horas sem estado vivo são anuladas com estorno.

Abrir a partida com segurança

O site abre a partida num app de jogo embutido. O lançamento gera um token de jogo de curta duração, separado do token de acesso e amarrado à sessão central: revogar a sessão derruba o socket. A ponte entre o site e o iframe aceita só a origem exata, confere quem mandou a mensagem, valida o payload com Zod e nunca envia para qualquer origem. O jogo só reporta eventos; quem decide sobre carteira e fila é o site.

Testes

Os testes do motor montam estados exatos e os reidratam, para serem determinísticos sem depender do embaralhamento, que usa Fisher-Yates com crypto.randomInt. Eles cobrem manilhas, desempates, escalada do truco, mão de onze e de ferro e o isolamento de informação. Outros cobrem o prazo na reconexão, o atraso do espectador, sockets velhos, buy-in cobrado uma vez mesmo com entradas concorrentes e a revanche que não derruba o processo. No app de jogo, há testes para token atrasado, volta do celular e a ponte com o iframe.

O que ainda falta

  • Hoje o servidor roda em uma instância: o estado vivo fica em memória. O adapter do Socket.IO e os snapshots no Redis, para escalar horizontalmente, estão planejados.
  • Uma semente auditável para o embaralhamento, no estilo commit-reveal, ainda é plano.
  • O bot ainda só conhece a heurística do Paulista.
Artigo de autoria própria
Éverton Carvalho|Software Engineer | Software Architect | CTO

Construído com Next.js 15, NestJS 11, PostgreSQL e Prisma. Arquitetura desacoplada e pragmática.

© 2026 Éverton Carvalho. Todos os direitos reservados.

Portfolio V2 · Zero Shared Runtimes