Construí um puzzle open source dirigindo uma equipe de agentes de IA. Esta é a arquitetura, camada por camada
Olá! Eu sou a Álli. Nos dois textos que publiquei por aqui, contei como montei uma equipe de agentes de IA para construir um jogo e o que aprendi quando percebi que o trabalho difícil começava depois do código gerado. Os dois eram protótipos — experimentos com começo, meio e aprendizado.
Desta vez é diferente: terminei e publiquei um jogo completo. O Decanta é um puzzle de ordenar líquidos coloridos em garrafas de vidro (o clássico water sort). Ele roda no navegador, no desktop e no celular, instala como aplicativo (PWA) e funciona offline depois da primeira visita. Tem fases infinitas, três modos de jornada, chefões que embaralham as cores, desafio diário e uma loja de cosméticos em que as moedas se ganham jogando — sem anúncio e sem compra. Três idiomas. E o código está aberto, com licença MIT.
Mas eu não vim só mostrar o jogo. Vim mostrar a arquitetura, porque ela foi desenhada em camadas que você pode reaproveitar no seu próximo jogo. E vou fazer do jeito que eu gosto: passando por cada camada com calma, contando o porquê de cada escolha — inclusive as que eu tentei antes e não me convenceram. Este jogo trocou de engine três vezes, e sobreviveu bem. Já chego lá.
O mapa geral
src/core/ → estado, regras, solver e gerador de fases (TypeScript puro, zero render)
src/render/ → o desenho do tabuleiro (PixiJS v8)
src/ui/ → menus, ajustes e modais (React)
src/audio/ → música e efeitos
src/i18n/ → português, inglês e espanhol
Uma regra sustenta o resto: o núcleo não sabe que existe uma tela. Vem comigo que eu explico o porquê de cada fronteira estar onde está.
Camada 1: o núcleo puro
O jogo de verdade mora em src/core/: o estado do tabuleiro, as regras de despejo, o solver e o gerador de fases. Nada aqui importa render, DOM ou engine — é TypeScript puro. Se você abrir essa pasta, não encontra nenhuma pista de que existe uma tela.
Por que eu sou tão rígida com essa fronteira? Porque tudo que é puro é barato de testar. São 46 testes automatizados cobrindo essa lógica, e eles rodam em segundos — sem abrir navegador, sem esperar animação. E aqui vai uma percepção que mudou meu jeito de trabalhar: quando o teste é barato, você escreve mais testes; quando é caro, você começa a se convencer de que "depois eu testo". A fronteira decide isso por você, antes de você perceber.
E tem a parte que eu considero o coração do projeto: toda fase tem solução garantida. Funciona assim: o gerador embaralha um tabuleiro novo; o solver tenta resolvê-lo, como um jogador incansável que testa os caminhos possíveis; se não existe caminho até a vitória, a fase é descartada ali mesmo — ela nem chega a você. Para mim, fase impossível é o pior bug que um puzzle pode ter, porque quem paga é a pessoa do outro lado, achando que o problema é ela. Vencibilidade não é questão de sorte — é invariante.
Um atalho que anda junto: os números de balanceamento (quantas cores, quantos recipientes, o ciclo de dificuldade) moram em dados de configuração, não espalhados pelo código. Ajustar a dificuldade é editar valores, não caçar constantes perdidas.
Camada 2: o trabalho pesado roda fora da tela
Gerar e validar uma fase custa processamento — e o custo cresce com o tamanho do tabuleiro; nas fases altas, custa bastante. Se essa conta rodar no mesmo fio de execução da interface, a tela congela: botão que não responde, animação que engasga.
A solução tem nome e é menos assustadora do que parece: Web Worker — um segundo fio de execução do navegador, separado da tela. O gerador e o solver moram lá. A interface pede "prepara a fase 40 para mim" e segue fluida enquanto a resposta não chega.
Repare como isso conversa com a camada 1: como o núcleo é TypeScript puro, movê-lo para o worker não exigiu reescrever nada — worker não tem DOM, e o núcleo nunca precisou de DOM. As fronteiras se pagam umas às outras. Se o seu jogo gera conteúdo, essa separação melhora a experiência em celulares com menos poder de processamento mais do que qualquer otimização de desenho.
Camada 3: o render é descartável — e eu provei isso três vezes
Aqui mora a história que mais mudou meu jeito de pensar. O Decanta passou por três engines de render.
A primeira foi o Phaser, a mesma do meu primeiro jogo. É uma engine com quem eu já tinha intimidade, e ótima para começar. Mas fui acumulando incômodos no caminho — e o que mais me marcou foi texto e arte borrados no celular. Aproveito para te entregar o atalho, porque ele vale para qualquer jogo em canvas: telas de celular têm 2 ou 3 pixels físicos para cada pixel lógico (o famoso devicePixelRatio). Se o canvas nasce no tamanho lógico e é esticado por CSS, tudo vira borrão — o texto primeiro. A saída é criar o canvas em resolução física e deixar a exibição no tamanho lógico. Segunda armadilha da mesma família: a opção pixelArt: true só serve para pixel art de verdade — em arte vetorial, ela serrilha tudo. Nada disso é defeito da ferramenta; são contornos documentados. Mas, somando os contornos, entendi que eu queria outra base.
A segunda tentativa foi o extremo oposto: Canvas 2D à mão, sem engine nenhuma. Aprendi demais — desenhar cada quadro me obrigou a entender o que as engines fazem pela gente. Mas o acabamento ficou caro: cada animação, cada brilho, cada transição era eu que construía e mantinha, e eu vivia ajustando um detalhe e desajustando outro. Controle total tem preço total.
A versão atual usa PixiJS v8: renderiza em WebGL, tem uma API de desenho que eu gosto e — o que pesou de verdade na escolha — é biblioteca, não framework. Ela cuida do tabuleiro e não disputa o projeto comigo: React segue na interface, GSAP nas animações, Vite no build. Cada ferramenta no seu lugar.
Agora, repare no que importa: o núcleo atravessou as três mudanças praticamente intacto, junto com os testes. Eu comecei este projeto achando que escolher engine era um casamento — decisão solene, difícil de desfazer. Terminei entendendo que, com a lógica na camada certa, engine é peça de encaixe: quando uma deixa de servir, você troca a peça, não o jogo.
Camada 4: interface de jogo em HTML
Menus, ajustes e modais são React por cima do canvas — não elementos desenhados dentro dele. Parece detalhe, mas essa escolha aposenta uma família inteira de bugs: botão desenhado no canvas obriga você a reimplementar área de clique, estado de foco, rolagem — e cada reimplementação é um bug em potencial (o clássico "cliquei e não aconteceu nada"). Botão de verdade, no DOM, traz tudo isso de série, e o navegador faz esse trabalho muito bem há décadas.
A divisão que ficou: o canvas cuida do que é jogo (tabuleiro, líquidos, despejo); o HTML cuida do que é aplicação (menu, ajustes, loja). Cada um no que faz de melhor.
Se você quiser começar o seu jogo por aqui
A receita que eu levaria para qualquer projeto do gênero:
- Comece pelo núcleo puro, com testes antes de qualquer pixel. É a decisão que protege todas as outras.
- Se o conteúdo é gerado, valide com um solver: fase sem solução é bug, não dificuldade.
- Escolha a engine por último. Ela é a camada mais fácil de trocar, desde que você a mantenha na borda.
E mais alguns atalhos que me custaram caro até virarem aprendizado:
- Quem sai de cena precisa liberar memória. Em render WebGL, remover um objeto da tela não devolve a memória de GPU — no PixiJS,
removeChildren()deixa textura e geometria vivas; é preciso destruir explicitamente. Vazamento assim não quebra nada na hora: ele aparece como lentidão misteriosa depois de muitas fases. - Nada de criar objeto gráfico a cada frame. Efeito contínuo (o jato de líquido, por exemplo) usa um objeto persistente que se limpa e redesenha — não sessenta objetos novos por segundo. O coletor de lixo agradece, e o celular também.
- PWA é distribuição sem loja. Com um plugin de build (
vite-plugin-pwa), o jogo instala no celular e funciona offline. E um detalhe que eu descobri na prática: no iPhone, o Safari não permite tela cheia de canvas — o modo standalone do PWA é justamente o contorno. - Música e efeitos sonoros em canais separados, destravados no primeiro toque (o navegador só libera áudio depois de um gesto de quem usa). Quem joga agradece poder silenciar a música sem perder os efeitos.
E o repositório está aberto exatamente para isso — entre sem cerimônia. Puxe o solver, copie a estrutura de pastas, adapte o que fizer sentido para a sua ideia. Se alguma parte não estiver clara, me pergunta: explicar também me ajuda a entender melhor o que eu construí.
Como eu trabalhei
Construí o Decanta dirigindo uma equipe de agentes de IA — papéis que eu defini, com a montagem contada no primeiro texto desta série. Mas quero ser honesta sobre a divisão do trabalho: as decisões de produto foram minhas. Sem anúncio e sem compra, porque eu queria um jogo que respeitasse o tempo de quem joga. Fluidez durante a partida como regra inegociável, porque engasgo no meio da jogada me incomoda até como jogadora. A IA acelera muito o percurso; o critério do que entra, do que sai e do que está bom continua sendo trabalho meu — e, sendo sincera, é a parte de que eu mais gosto. Vou confessar: amei trabalhar em cada parte deste jogo, do solver ao som — e espero que isso transpareça quando você jogar.
Para fechar
Se você jogar e encontrar qualquer problema, uma issue no repositório me ajuda de verdade — foi o feedback de quem jogou que já melhorou o jogo desde o lançamento. E se alguma dessas ideias entrar no seu projeto, vou adorar ver o que você construir com elas.
Sigo compartilhando esses experimentos por aqui e, em primeira mão, no meu LinkedIn — é lá que eu trago os resultados conforme eles acontecem. Se quiser acompanhar de perto, vai ser um prazer ter você junto.
E fico com uma pergunta de verdade, que vale para o seu projeto atual, jogo ou não: que parte dele sobreviveria a uma troca de engine (ou de framework)? Me conta nos comentários — sou curiosa.