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.

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
MatchRunnerdo 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:
- 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.
- Aleatoriedade só com seed: nada de
Math.randomna partida. 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.
- Gera ~60 chutes candidatos (botões perto da bola, mirando no gol ou em direções aleatórias, três forças).
- Simula cada um com o mesmo
simulateShotda partida. - Dá nota: gol, gol contra, bola avançada, rival perto da bola depois.
- Testa os melhores de novo com cada carta que pode jogar.
- 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.

- 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.