Pitch: Fiz um RPG idle multiplayer para devs 😅, joguei fora metade da arquitetura e o custo caiu: o que aprendi
Eu e uns colegas de trabalho às vezes ficamos com tempo ocioso entre uma task e outra. Pra preencher esses minutos criei a Guilda do Ócio: um RPG idle cooperativo, com tema de desenvolvedor (você é Backend, Frontend, QA, DevOps..., enfrenta bugs e o "chefão" do deploy de sexta, troca de empresa para "prestigiar"). Roda no navegador, sem instalar nada: https://guildadoocio.com

Não vim só mostrar o jogo. Esse projeto virou um laboratório de problemas que muita gente aqui enfrenta: estado compartilhado, confiar no cliente, custo imprevisÃvel em serverless e deploy seguro. Conto o que deu errado e o que mudei.
A stack
- Cloudflare Workers para a API
- D1 (SQLite) para contas, heróis, banca de itens, Arena, ranking
- Durable Objects (hibernáveis) para o chat e um limitador de requisições por IP
- R2 para as imagens das placas de anúncio
- Cliente: um HTML grande, sem framework, gerado por um build simples
1. Primeira versão: um Durable Object por guilda, simulando a 4 Hz
Minha ideia inicial era "multiplayer de verdade": cada guilda tinha um Durable Object que rodava a luta do grupo contra o chefão a 4 atualizações por segundo, com presença por WebSocket. Funcionou, mas o custo de manter objetos acordados o dia inteiro (duração de CPU/WebSocket) crescia com tempo online, não com valor entregue ao jogador. Num jogo idle, em que a aba fica aberta o dia todo, isso é o pior cenário.
2. O que mudei: modelo "solo + social assÃncrono"
Reescrevi em 4 fases, sem derrubar o jogo:
- Cada jogador joga solo no navegador e o progresso vai no save (a cada 30 s com a aba aberta, 2 min em segundo plano).
- O "social" virou assÃncrono: chefão da guilda, banca de itens, raide semanal, Arena e Mentoria passam por contadores no D1, validados quando o save chega. Ninguém precisa estar online ao mesmo tempo.
- Só o chat continua em tempo real (Durable Object hibernável: dorme quando ninguém escreve).
Resultado: o custo passou a depender de saves por jogador, não de tempo conectado. Pelas minhas contas, hoje o projeto custa algo entre US$ 9 e 19 por mês, e a projeção para 1 mil → 10 mil jogadores fica entre US$ 5 e 13 com esse intervalo de save. O primeiro limite técnico real deve ser o banco único (D1), em dezenas de milhares de cadastros.
Lições práticas de custo: amostrar logs (head_sampling_rate), cachear consultas que se repetem (ranking, nomes), gravar em lote (o dano do chefão vai a cada 5 s, não a cada golpe) e limitar o tamanho do corpo do save.
3. Nunca confie no navegador (eu levei isso a sério depois de ver acontecer)
O save vem do cliente, então qualquer campo pode ser editado. Um colega mostrou isso na prática com uma extensão que reescrevia itens e materiais. Em vez de desconfiar de tudo e recusar saves, adotei:
- Orçamentos e tetos no servidor: ouro, XP, materiais, itens e dano aceitos entre dois saves são limitados pelo que o herói poderia ter ganho naquele intervalo (calculado a partir do DPS e da zona validados).
- Corrigir (clamp) em vez de recusar: recusar um save inteiro é perigoso. Se o ponto de comparação nunca avança depois de uma recusa, os saves legÃtimos seguintes continuam sendo comparados com um estado antigo e o jogador fica preso. Só recuso em casos claros (zonas rápidas demais, primeiro save, regressão de estado).
- Escrita otimista do save:
UPDATE ... WHERE save IS <o que li>. Se outra aba/dispositivo gravou no meio, o servidor responde 429 e o cliente tenta de novo, em vez de sobrescrever. - Arena (PvP) determinÃstica no servidor: a luta é simulada a partir do save aceito, com semente. O navegador só reproduz a mesma luta.
4. O bug que mais doeu: o jogador "perdeu tudo"
Um jogador reclamou que perdera o progresso. Investigando, vi duas trocas de empresa (o "prestÃgio", que zera quase tudo em troca de um bônus permanente) em poucas horas, uma delas 2 minutos depois de ele restaurar um backup antigo. Provavelmente um auto clicker rodando na madrugada clicava no botão de troca. A resposta foi em camadas, não em um botão de confirmar:
- confirmação digitando uma palavra, com clique real e janela que se fecha sozinha;
- o servidor recusa regressão (um save normal com menos empresa/ações/zona do que o aceito);
- cópia de segurança automática a cada troca e um "Desfazer a última troca", com limites para não virar duplicação de itens.
Depois, o mesmo jogador pediu para manter itens ao trocar. Virou a "Herança": até N itens atravessam a troca (N cresce com a empresa), mas ficam ligados ao herói e com trava de nÃvel. Dá para dizer que a pergunta de design certa era "o que o jogador quer proteger?" e não "como bloqueio o clique?".
5. Deploy: o staging que quase derrubou a produção
A configuração de staging herdou as rotas de domÃnio da produção, e um deploy de teste podia assumir o domÃnio real. Corrigi deixando as rotas explicitamente vazias no staging. Além disso, o npm run release virou um pipeline com portões: árvore limpa → testes de regras → testes do jogo → build → testes online com servidor local → deploy em staging → e2e em staging → só então produção → confere a versão no ar → tag. Se o e2e do staging falha, nada vai para produção. Também há uma guarda para migrações de banco aplicadas antes do deploy.
Separar nomes de ambiente não basta: confira quais recursos cada deploy realmente modifica.
O que ainda não resolvi
- O D1 único vai virar gargalo se o jogo crescer muito.
- Equilibrar um idle com PvP e economia sem que um jogador novo se sinta atrás de quem trocou de empresa 10 vezes.
- Alertas de custo (hoje confio em projeção e logs, não em um alarme automático).
Para quem tem experiência com isso
Vocês também migraram de "estado vivo em um servidor" para "estado em banco + eventos assÃncronos" em jogos ou apps com muita gente ociosa conectada? E como lidam com cliente não confiável sem punir jogador legÃtimo?
Se quiser testar, é só abrir https://guildadoocio.com. Feedback de jogo e de arquitetura são bem-vindos nos comentários.

Fonte: https://guildadoocio.com/