Onde eu conto como e porque fiz o Mystery Narrative Game
Eu já estava ensaiando fazer alguns projetos pessoais de produto há algum tempo. Um pouco pra testar novas ferramentas, um pouco pra aprender coisas novas, um pouco pra voltar ao básico e construir coisas legais com minhas próprias mões (mais ou menos). Fui juntando várias ideias, mas não tinha realmente investido em nada até que um conceito FINALMENTE grudou na minha cabeça.
E se eu fizesse um jogo em que toda a interação do jogador com o mundo fosse via LLM?
Fiquei instigado, e com isso eu já poderia testar várias coisas:
- Usar LLM como motor de um produto
- Usar RAGs como forma de organizar o conhecimento e os problemas para um modelo
- Claude Code pra construir tudo
O ponto mais importante nesse caso foi: usar IA no core do produto porque faz sentido, e não por conta do hype.
A primeira versão da ideia, como sempre, era enorme e complexa. Envolvia um cenário de investigação enorme, um caso difícil e interação via voz em um app mobile. Mas pra esse momento o melhor seria um microcosmo disso, uma prova de conceito que me colocasse no caminho certo mas que também permita iterar mais rápido.
A solução estava num dos meus party games favoritos: Black Stories. Nesse jogo, o jogador ouve um cenário e, através de perguntas de sim ou não deve entender o acontecimento completo, conhecido apenas pelo narrador, que responde às perguntas.

Passo 1: Planejar
Armado dessa ideia e de nenhum juízo, convoquei Claudinho e ligamos o modo de planejamento. Ele leu as ideias que já estavam escritas e começou as perguntas para preencher as lacunas. Qual o tipo de interface? Qual o stack? Como vamos usar LLMs nesse caso?
Fiquei tentado a usar Ruby no backend (amo Ruby e irei defendê-la) mas fomos de Python. Ainda vou tentar um app Rails que use IA como motor de valor, mas por enquanto não.
No final as escolhas foram:
Stack: Python+Flask daria menos fricção para uma PoC rápida. A API da Anthropic para usar o Claude Haiku dá um modelo mais barato mas bom o suficiente pra compreender e julgar as perguntas feitas. Aqui já deixei uma porta aberta para trocar de modelo no futuro, caso queira escalar esse joguinho.
Banco de dados: Sem banco de dados. Os mistérios ficam em arquivos .yaml que podem ser adicionados de forma independente de código.
RAGs: Ficaram pra depois! A v0 simplificou tanto essa parte que não foi necessário. Uma injeção direta do YAML já resolveu.
Chamadas pra API: a primeira versão sugerida pelo meu parça Claudio separava em duas chamadas, uma para perguntas (que dava uma resposta seca: Sim/Não/Irrelevante) e uma para o palpite, que comparava com a parte secreta do mistério e dizia se tinha resolvido o caso. Mas não deu pra ficar desse jeito não.
Passo 2: Corrigir e melhorar
Usabilidade
Essa separação as duas chamadas foi implementada de um jeito que, no front, eram necessários dois inputs de texto separados, mesmo que para o usuário as duas coisas (perguntas e palpites) fossem quase indistinguíveis. Ou seja: a usabilidade ficou uma merda.
Unificamos o campo de perguntas, deixando a interação mais parecida com um chat.
Confiabilidade
Alerta de tecniquês, siga por sua conta e risco
Porém a separação em duas chamadas tinha uma vantagem que eu não percebi até tentar tirar ela: a rede de segurança. No “modo pergunta”, eu forçava a resposta da API a ter no máximo 10 tokens (max_tokens=10). Isso não é só economia, é uma trava física: não importa o que desse errado no prompt, a LLM fisicamente não conseguia devolver um parágrafo, um spoiler ou qualquer texto livre pro jogador. O erro máximo possível já vinha limitado pelo tamanho da resposta. Quando unifiquei tudo num campo de chat só, isso significou uma chamada por turno em vez de duas, e a LLM passou a devolver um JSON completo com um campo type (question, guess, give_up_confirm…) decidindo sozinha o que o jogador quis dizer. Ganhei uma interface mais simples, mas perdi a trava de tamanho: agora tinha um JSON inteiro onde texto livre podia vazar.
Resolvi isso validando a resposta contra um dicionário fechado de respostas permitidas (nunca aceitando texto solto vindo da LLM) e com um fallback seguro pra “Irrelevante” sempre que o parsing falhasse. E o backend nunca ficou responsável por confiar cegamente na LLM na parte que mais importava: quem revela a solução do caso é o próprio servidor, depois de conferir sozinho que correct == True. A LLM decide o tom da resposta, não decide se o jogador ganhou.
O próprio fluxo de desistência de um mistério (pedir resposta → confirmar → revelar) não tem máquina de estados no backend. A LLM lê o histórico, incluindo o JSON que ela mesma gerou no turno anterior, pra saber “eu já pedi confirmação?”
Acessibilidade
Pedi pro Claude testar a acessibilidade geral do jogo e: não havia acessibilidade. A forma como o chat estava sendo atualizado deixava ele ilegível pra leitores de tela. A correção foi simples e rápida, o que mostra que não dá pra justificar essas negligências com o básico do front-end.
Conteúdo
Usei 8 casos de domínio público, pra não pegar nada que fosse proprietário do prórprio Black Stories, e também desviei de casos que a solução tivesse a ver com suicídio. Na hora de organizar tudo o Claude deixou alguns títulos (que são visíveis pro usuário) entregando a resposta final. Um puxão de orelha e ficou tudo certo, mas fica claro que não dá pra deixar o garoto solto, tem que revisar bem o trabalho.
Passo 3: Os próximos
Alguns pontos ficaram em aberto para o futuro. Ideias como fazer um app mobile, usar interface de voz, trocar a API do Claude por uma LLM local, ter um modelo de monetização, ter um BD. Não sei quanto vou explorar deles ainda, pra ser sincero, mas está tudo no radar.
E claro, partir para o jogo mais complexo com motor de LLM ainda é uma ideia bem forte.
Lições aprendidas
A experiência de codar tudo com o Claude é ótima, mas não dá pra confiar cegamente. Tem que saber revisar e ser crítico com as escolhas que ele faz sozinho. Não dá pra começar pensando em arquitetura e esquecer de usabilidade, ou correr pro oba oba e não pensar nas decisões de engenharia por trás do produto. Cookie, acessibilidade, nome de arquivo que entrega spoiler: nada disso tem a ver com o modelo que o produto usa, mas tudo isso decide se o produto funciona. A pergunta que fica pra próxima iteração não é qual LLM local eu vou usar. É se eu topo aplicar esse mesmo nível de rigor num escopo bem maior.

Deixe um comentário