
Um agente jogou Slay the Spire 2 sozinho — e o que desbloqueou foi o jogo dizendo 'não'
Eu fiz um agente jogar Slay the Spire 2 sozinho — e o que desbloqueou foi o jogo dizendo 'não'
Em 08/09/2026, um agente jogou um combate solo inteiro de Slay the Spire 2 sem intervenção humana: 7 turnos, 35 ações, 0 rejeições. Antes de qualquer entusiasmo, o escopo honesto: este é um registro de sessão, não um benchmark auditado — eu não tenho o log bruto daquela luta arquivado, e a reprodução passa por um suporte sintético + artefatos versionados com md5 (detalhes na seção QA).
E o detalhe que impulsiona este texto: nenhuma das 35 ações foi decidida por um LLM. Cada decisão de jogo foi uma política gananciosa local baseada em pontuação rodando dentro da ponte Python. O LLM (Hermes, que opera o sistema) estava fora do caminho crítico. E os dois problemas mais difíceis neste projeto não foram resolvidos com "IA mais inteligente" — eles foram resolvidos quando o jogo disse "não" e o agente aprendeu com isso.
O que você levará
- O padrão de feedback de rejeição (o framework): como um erro de ambiente se torna um estado persistente que filtra as próximas ações do agente — sem re-treinamento, sem LLM, sem re-prompt. Copiável para qualquer agente ↔ integração de sistema síncrona (jogo, motor, hardware).
- A regra "onde colocar a decisão": determinística, local passo a passo para o que é repetível; LLM para o que precisa de contexto aberto. Saber onde o LLM não deve estar é uma decisão de engenharia, não uma falta de ousadia.
- As duas falhas instrutivas: um deadlock de thread principal (
task.Wait()no Godot →Task.WhenAnycom timeout) e patches que quebram em cada atualização de Acesso Antecipado (saída: reflexão defensiva).
A cena de abertura: o jogo já sabia como jogar sozinho
Tudo começou com engenharia reversa. StS2 roda em um fork do Godot com a lógica em C#/.NET — a DLL principal é sts2.dll (9,3 MB). Decompilando com ilspycmd no meu PC, encontrei um sistema interno AutoSlay (MegaCrit.Sts2.Core.AutoSlay): um AutoSlayer que orquestra toda a execução (mapa, combate, recompensa, loja) apenas para testes, com um seletor de cartas aleatório — e não está exposto na UI do jogador.
Sem reivindicação de novidade: um mod de autoslay público já existe na comunidade (STS2AutoSlayMod no Nexus, desde 21/03/2026). O que importa para este projeto é outra coisa: se o jogo tem um autoslayer, então um gancho de seleção de cartas oficial existe em algum lugar. Eu o encontrei: ICardSelector, em MegaCrit.Sts2.Core.TestSupport (namespace de teste, mas público), com GetSelectedCards(options, minSelect, maxSelect) e GetSelectedCardReward(...). O mesmo gancho que o jogo usa para descartar, recompensar, atualizar e remover — um seletor lida com tudo (detalhe na seção EA).
A tese: isso não é um problema de LLM
Todo mundo assume que "um agente de IA jogando um jogo" = LLM chamando LLM em cada ação. A parte contraintuitiva deste projeto:
Fazer um agente jogar um jogo real não é um problema de LLM — é um problema de canal de comando e um problema de feedback. O jogo precisa receber a ação e precisa dizer quando falhou.
Decisões de combate vivem em uma política local determinística — _autopilot_decide(gate, trigger), inline na ponte desde a v0.1.3, presente na v0.2.7 na linha 396 de server/sts2_bridge.py. É gananciosa por pontuação: dano/custo com bônus de morte, bloqueio preventivo e de desespero, poderes escalonados jogados cedo em lutas longas, veneno quando a mão não pode matar, alvo escolhido pela intenção do inimigo. Zero chamadas de LLM no loop (verificado por grep no zip).
A regra que usei, e que se aplica a qualquer projeto de agente:
| Tipo de decisão | Onde vive | Por quê |
|---|---|---|
| Passo a passo repetível (qual carta, qual alvo) | Política local determinística (pontuação) | Determinismo, latência, custo zero por decisão |
| Contexto aberto (o que está acontecendo, o que mudou) | LLM como operador/observador | Julgamento, linguagem natural, explicação |
O LLM opera o sistema: liga/desliga o piloto automático, lê o estado, vê rejeições, ajusta a política. Ele não é o cérebro da jogada — e vender "um agente LLM jogando" seria a mentira por omissão que mata a credibilidade na hora.
Arquitetura: o canal de comando
O pipeline completo (código verificado nos artefatos com md5, seção QA):
Slay the Spire 2 (EA, C#/Godot)
└─ mod (BaseLib + Harmony, MainFile.cs) ← compila e patcha o repositório público
│ POST http://127.0.0.1:5000/update_state ← loopback, fire-and-forget
▼
bridge Python/FastMCP (sts2_bridge.py v0.2.7)
│ Resposta HTTP a CADA push = canal de comando
│ {"Type":"PlayCard","HandIndex":N,"TargetIndex":M} | {"Type":"EndTurn"} | OK
▼
MCP (10 ferramentas) → Hermes (operador/observador)
Três pontos que valem a pena destacar:
-
Somente loopback. O mod só fala com
127.0.0.1:5000(MainFile.csv0.3.2, linha 404:_aiServerUrl = "http://127.0.0.1:5000/update_state";_rejectionUrlna linha 27). A ponte rejeita qualquer origem que não seja localhost (route_update_state, linha 645;route_rejection, linha 704). O jogo nunca abre uma porta — ele apenas POSTa. -
A resposta HTTP é o comando. Cada push de estado do jogo recebe uma resposta; se o corpo for JSON de comando, o mod o executa (
TryManualPlay, linha 74;combatManager.SetReadyToEndTurn, linha 131). Se forOK, ele não faz nada. Um único canal, síncrono por construção — sem fila, sem polling. -
10 ferramentas MCP (
sts2_bridge.py, linhas 761–908):sts2_status,sts2_get_state,sts2_get_combat,sts2_history,sts2_rejections,sts2_autopilot,sts2_set_hold,sts2_play_card,sts2_end_turn,sts2_clear_pending.
O mod base é público: Manuelbbl/Communication_Mod_STS2 — "Um poderoso mod de API que exporta o estado completo do jogo ao vivo de Slay the Spire 2 para um servidor local para treinamento de IA e desenvolvimento de bots", com BaseLib como um requisito estrito. É a base que compilo e patcho; o que é meu (ponte, piloto automático, suporte) é público em github.com/brmarcosbr/sts2-mcp-bridge; o que permanece apenas nos zips versionados são os patches em cima do mod (seletor v3, canal de rejeição), porque o repositório base não tem licença que permita redistribuí-los (veja Limites).
O padrão de feedback de rejeição (o cerne deste texto)
O mod é fire-and-forget: quando o jogo recusa uma jogada, o comando falhou silenciosamente — o jogo não trava, ele simplesmente não executa. Sem feedback, o agente tentaria a mesma carta para sempre. A solução foi um canal de erro dedicado:
1. Fire-and-forget com um sep
O artigo ilustra como a interação entre agentes de IA e jogos pode ser otimizada, o que pode ser aplicado em empresas que desenvolvem jogos ou sistemas interativos. A compreensão do feedback e da tomada de decisão local pode melhorar a eficiência de agentes em ambientes complexos.


