2

Pitch: Fiz um futebol de botão online em TypeScript: física determinística, bot sem IA treinada e uma campanha feita só de config

Quem cresceu no Brasil provavelmente jogou futebol de botão na mesa da cozinha. Eu queria esse jogo no celular, online e com cartas de poder (Super Chute, Congelamento, Paredão, Bomba de Várzea...). Daí saiu o Peteleco Cards, e aqui conto as decisões técnicas que mais me ensinaram.

Partida com o Super Chute

A regra de ouro: o app nunca decide o resultado

Monorepo em TypeScript com quatro partes:

  • core: regras, cartas e o simulador de chute em Matter.js. Não importa Phaser nem DOM, então roda igual no navegador e no Node.
  • app: Phaser 3 + Vite (e Capacitor no Android). Só desenha e lê o toque, não roda física.
  • server: Node + Socket.io, rodando o mesmo MatchRunner do core como autoridade. API em Hono, Postgres com Drizzle.
  • admin: painel próprio com métricas, replays e ajustes do jogo.

O app manda uma intenção ("botão 3, nessa direção, com essa força") e o core devolve uma gravação do chute, quadro a quadro. O Phaser só toca a gravação. Online, isso roda no servidor; contra o computador, o mesmo código roda no navegador.

Física determinística

Para validar partidas e rever replays antigos, o mesmo chute precisa dar sempre o mesmo resultado:

  1. Passo fixo, fora da tela: o chute inteiro é simulado de uma vez em passos de 1/60 s, antes da animação. O FPS do celular não muda nada.
  2. Aleatoriedade só com seed: nada de Math.random na partida.
  3. simulatorVersion: qualquer mudança que altere um resultado incrementa a versão, e cada gravação guarda a sua.

O bônus: numa partida contra o computador, o app manda só a seed, os times e as jogadas. O servidor não confia no placar: joga a partida de novo e só marca como verificada se bater.

A física (quique, freio, peso, força) fica em inteiros numa versão de "ajustes" que o painel publica sem deploy. Cada partida guarda os ajustes com que começou, então o replay não quebra.

Um bot sem rede neural

Não tenho partidas suficientes para treinar nada, então o bot faz o que um bom jogador de botão faz: testa na cabeça antes de chutar. Só que a cabeça dele é o próprio simulador.

  1. Gera ~60 chutes candidatos (botões perto da bola, mirando no gol ou em direções aleatórias, três forças).
  2. Simula cada um com o mesmo simulateShot da partida.
  3. Dá nota: gol, gol contra, bola avançada, rival perto da bola depois.
  4. Testa os melhores de novo com cada carta que pode jogar.
  5. Erra de propósito, conforme o nível (Fácil: 12° de mira e 20% de força; Difícil: 1° e 3%).

O planejamento é dividido em tarefas pequenas para não travar celular fraco.

Desafio do dia que nunca sai impossível

Jogo online com pouca gente tem o problema de você entrar e não ter com quem jogar. O desafio do dia dá um motivo para voltar: todo dia, uma jogada montada, até 5 chutes, ranking por menos chutes.

Desafio do dia

  • A seed sai de um hash da data, então servidor e admin geram a mesma jogada.
  • Antes de publicar, o próprio bot resolve a jogada sem erro de mira. Se precisa de mais de 3 chutes, descarta; se resolve em 1, descarta por ser fácil demais.
  • O app manda os chutes, não o resultado. O servidor re-simula e só então grava no ranking.

Uma campanha de 48 missões sem código por missão

A Rota do Brasil atravessa o país do Norte ao Sul, e cada vitória libera o time que você venceu. Cada parada é uma linha de config:

{ region: 'nordeste', rival: 'timbu', field: 'gramado', objective: 'gol_com_carta', level: 'medio', card: 'super_chute' }

Cada objetivo vira um MatchSetup que muda o ponto de partida: gol único, começar perdendo de 1 a 0, jogar sem cartas, rival com energia cheia, 90 segundos sem gol de ouro. O MatchRunner nem sabe que existe campanha.

Os campos terrão e molhado não têm simulador próprio: são porcentagens sobre os ajustes de física (terrão: freio da bola a 240%; molhado: 60%). Como o campo entra nos ajustes da partida, o servidor re-simula igual sem nenhuma linha nova de validação.

Testes e deploy

Vitest no core (regras, cartas, simulador, replay, bot, desafio) rodando num hook de pre-push. Vários testes vieram de relatos de testadores, como a bola que atravessava a quina do campo.

Deploy numa VPS pequena com Docker Compose + Caddy, imagens buildadas no GitHub Actions e todos os workflows manuais. Um workflow de teste de carga com robôs jogando partidas reais definiu o teto de salas.

Conclusão

A decisão que mais simplificou tudo foi manter física e regras num core determinístico e compartilhado: online, offline, bot, replay, validação, desafio do dia e campos da campanha são o mesmo simulador usado de jeitos diferentes.

Dá para jogar de graça no navegador: https://petelecocards.com.br

Se jogar, me conta qual carta mais usou ou o que te fez perder para o computador no Difícil. Críticas técnicas também são muito bem-vindas.

Carregando publicação patrocinada...