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.

← Voltar pro blog