02 de outubro de 2026
O melhor agente de truco que treinei não foi o último
O truco-rl é um motor de regras de Truco Gaúcho 1v1 em Python e um agente que aprende a jogar contra cópias de si mesmo, sem partida humana e sem estratégia escrita à mão. Treinei por 5 milhões de passos, uma hora de CPU num notebook, e o modelo que saiu no fim é pior do que um que ele tinha sido no meio do caminho.
Este post é sobre como eu percebi isso, porque a métrica que o treino imprime não ia me contar.
A métrica de treino é zero por construção
No self-play, o adversário é uma cópia do próprio agente, atualizada a cada 20 mil passos. A última linha do log de treino diz:
ep_rew_mean | 0.0045
Recompensa média perto de zero. Faz sentido: um jogador contra ele mesmo ganha metade das partidas. E isso é verdade no passo 250 mil e no passo 5 milhões. O número que o treino mostra não sobe quando o agente melhora, porque o adversário melhora junto.
Ou seja, pra saber se o agente está aprendendo alguma coisa, eu precisava de um adversário que não mudasse.
Contra quem medir
O primeiro candidato óbvio era um jogador aleatório, que escolhe qualquer ação legal com a mesma probabilidade. O agente ganha dele por volta de 80 a 90%. Parece bom, até você lembrar que o aleatório responde às apostas no sorteio e pede truco sem olhar a mão. Ganhar dele diz pouco.
Então escrevi um segundo adversário, um bot de regras que joga como um iniciante cauteloso: só pede envido com pontuação boa, só pede truco com carta forte ou com uma rodada já ganha, e joga a carta mais barata que vence cada rodada. Ele ganha do aleatório 89% das vezes. Nenhum dos dois apareceu durante o treino.
Pra ter uma referência do que é empate: aleatório contra aleatório dá 51%, e o bot contra ele mesmo dá 50,5%. Cada número abaixo é sobre 2.000 partidas, com margem de uns 2 pontos pra cima ou pra baixo.
O bot de regras achou um bug antes de medir qualquer coisa
A primeira vez que coloquei o bot pra jogar, uma partida não terminou.
Nada no motor limitava os aumentos do envido. Um jogador pedia Real Envido, o outro respondia com Real Envido, e assim pra sempre. O bot, quando tinha pontuação boa, sempre aumentava, e os dois lados tinham pontuação boa. O ambiente não tem limite de passos por episódio, então o treino por self-play podia cair no mesmo laço e eu não ia ver, só ia ficar com um treino travado.
A correção foi fazer a cadeia só subir: Envido no máximo duas vezes, Real Envido uma, e Falta Envido encerra. A regra fica num lugar só, e a máscara de ações do agente só oferece os aumentos que ainda são legais.
O que me incomoda é que o aleatório nunca achou esse bug. Ele aumenta às vezes e aceita às vezes, então a cadeia sempre acabava por acaso. Foi preciso um adversário com uma estratégia, mesmo boba, pra seguir o mesmo caminho até o fim.
A curva
Salvei um checkpoint a cada 250 mil passos e avaliei todos contra os dois adversários fixos:
| Checkpoint | vs aleatório | vs bot de regras |
|---|---|---|
| 250 mil passos | 89,6% | 38,0% |
| 750 mil | 88,4% | 54,1% |
| 1,25 milhão | 83,5% | 63,9% |
| 2,25 milhões | 83,9% | 73,1% |
| 3,0 milhões | 81,5% | 60,1% |
| 3,5 milhões | 79,0% | 52,6% |
| 5,0 milhões (final) | 80,2% | 62,8% |
O agente passa o bot de regras por volta dos 750 mil passos e chega no pico de 73% em 2,25 milhões. Depois disso ele piora. Cai pra 53%, quase empate com um bot de iniciante, e se recupera pra uns 63% no fim.
Se eu tivesse pegado só o modelo final, que é o que o script de treino salva com nome de “final”, ia reportar 63% e achar que era o teto. O melhor modelo estava 10 pontos acima, dois terços do caminho pra trás.
Por que piora
Isso é o problema clássico do self-play ingênuo. O adversário é sempre a versão mais recente do agente, então o agente aprende a ganhar daquele adversário. Quando ele muda de estratégia, a versão nova não precisa mais lembrar como ganhar das versões antigas, e esquece. As estratégias podem andar em círculo: A ganha de B, B ganha de C, C ganha de A, e o treino segue rodando com a recompensa em zero o tempo todo.
Tem um sinal disso na outra coluna da tabela. Contra o aleatório, o agente começa em 90% e termina em 80%, pior do que o próprio bot de regras contra o mesmo aleatório. A minha leitura é que ele foi se especializando em jogar contra um adversário que joga bem, e um adversário que responde às apostas sem critério deixou de ser alguém que ele lembra como enfrentar. Não testei isso direito, é hipótese.
A correção conhecida é uma liga de adversários: em vez de jogar sempre contra a versão mais nova, o agente joga contra um sorteio entre versões antigas e bots fixos, e não pode esquecer nenhuma delas. Não implementei. É o próximo passo óbvio, e está no README como não feito.
O que eu tiraria disso
Duas coisas, e nenhuma é específica de truco.
A primeira é que a métrica que o treino otimiza e a métrica que diz se o modelo presta podem ser coisas diferentes, e no self-play elas são diferentes por definição. Sem um adversário fixo de fora, eu teria um log de treino bonito e nenhuma ideia do que estava acontecendo.
A segunda é que o adversário de avaliação precisa ser bom o bastante pra perder de verdade. O aleatório não achou o bug do envido e não mostra a queda: contra ele, o checkpoint que parece melhor é o de 250 mil passos, justamente o que perde do bot de regras 62% das vezes. Foi um bot de regras de iniciante que mostrou as duas coisas.
O código, os resultados e como reproduzir estão no repositório.