Ensinar uma Engine de Xadrez em Kotlin a Diagnosticar Seus Próprios Problemas
Dois fontes independentes de verdade como juízes, um agente de codificação como o mecânico — como arquitetar qualquer módulo lógico para que um agente possa testá-lo, localizar bugs e corrigi-los autonomamente, com as restrições incorporadas na construção, não no prompt.
O Problema: As Falhas Silenciosas do Estado do Jogo
Uma aplicação móvel moderna pode parecer perfeitamente correta na tela enquanto a lógica de negócios subjacente está comprometida de maneira sutil. Em um aplicativo de xadrez, uma peça pode deslizar suavemente sobre uma interface Compose UI, mas um caso raro — como um movimento especial como en passant interagindo com uma peça presa ou castling através de um quadrado atacado — pode falhar silenciosamente. Nenhuma exceção é lançada; um estado ilegal simplesmente é aceito.
Para garantir a integridade do nosso motor de xadrez Kotlin Multiplatform (KMP), precisamos de uma fonte de respostas corretas que viva fora do nosso próprio código. Os desenvolvedores de motores de xadrez têm um há décadas: perft (abreviação para performance test). Perft é a técnica padrão de depuração da comunidade para verificar um gerador de movimentos: a partir de uma posição dada, conte cada sequência legal de movimento até uma profundidade fixa e compare o seu motor com números publicados e independentemente verificados. Da posição inicial em profundidade 5, a resposta é exatamente 4.865.609 — sempre. Se o seu motor disser qualquer coisa diferente, tem um bug. É aritmética, não opinião.
Perft em si é conhecimento estabelecido da comunidade. O que este artigo aborda é a engenharia personalizada que colocamos por cima disso:
- Três camadas de verificação construídas em duas oráculos independentes — os contagens perft publicados, cruzados com um motor de xadrez completamente separado
- Infraestrutura de teste que roda automaticamente em cada PR e noite
- Ferramentas que permitem a um agente de codificação AI executar os testes, ler as falhas e corrigir o motor sem intervenção humana
Chamamos tudo isso de perft rig. "Rig" no sentido do laboratório: uma estação de teste engenhada para um propósito rigoroso. Não é um conjunto casual de testes — é um sistema com múltiplas verificação independentes, projetado para ser à prova de balas e controlável por um agente.
Os Oráculos e o Localizador
O padrão generaliza além do xadrez. Qualquer módulo lógico determinístico pode adotá-lo. Você precisa de três coisas:
Um oráculo determinístico — uma fonte de respostas corretas. Para nós, isso é o número de nós perft publicados. Para um motor tributário, são as publicações de teste do IRS. Para um motor de layout, são as medidas de um navegador de referência. O oráculo só precisa ser um número que você pode consultar em uma fonte na qual confia mais do que no seu próprio código.
Um segundo oráculo independente — uma implementação completamente separada da mesma lógica. Para nós, isso é o Stockfish, o motor de xadrez de código aberto mais forte do mundo. Ele existe em um idioma diferente (C++), roda em um processo diferente e não pode compartilhar um bug com nosso gerador Kotlin. Um segundo oráculo torna inútil a tentativa de enganar o primeiro — mesmo que você edite a contagem esperada, o motor independente ainda discordará. O nosso também faz uma comparação contra o Stockfish em uma caminhada aleatória com semente para posições do meio-jogo e final — semente fixa, então uma caminhada falhada reproduzirá sempre o mesmo percurso — capturando bugs em posições que ninguém pensou em codificar manualmente.
Um localizador — a peça que transforma “o total está errado” em “aqui está o exato ponto de entrada onde está errado.” O perft tem uma variante padrão chamada divide: ao invés de relatar um total geral, reporte uma contagem separada para cada primeiro movimento legal e compare cada uma contra o oráculo. Quando a contagem de um movimento difere, faça esse movimento e re-divida a partir da posição resultante — repetindo até chegar à exata posição e movimento onde sua lógica e o oráculo divergem. Nosso localizador escreve essa trilha em um arquivo: a exata posição onde os motores divergem — codificada como uma string FEN (uma notação de texto única para um tabuleiro de xadrez) — o movimento divergente, as contagens de ambos os motores e uma quebra mais profunda de um pôquer que restringe a pesquisa. Um agente ou humano sabe exatamente onde procurar e pode formular imediatamente uma hipótese.
Uma Função Pura no Centro
O coração puro que o rig chama milhões de vezes é uma única função — applyMove() — que recebe o estado atual do jogo, um índice de peça, um destino quadrado e uma promoção opcional e retorna um novo estado do jogo. (O rig também pede ao módulo para gerar os movimentos legais a serem alimentados; applyMove é a pura transição no centro da loop quente.)
Não era sempre assim. A lógica de geração de movimentos estava originalmente entrelaçada dentro de um GameViewModel, dependente das emissões vivas do StateFlow. Extraímos isso em funções puras, top-level Kotlin — applyMove e applyWinConditions — em um módulo dedicado :chess-core, deixando o ViewModel para lidar apenas com efeitos colaterais (estado da UI, autosaves).
Mas uma função não é pura apenas porque parece ser. Uma função top-level em um módulo que depende transitivamente de StateFlow, autosave e spawn de processo ainda pode alcançar e tocar o mundo. O verdadeiro enabler não foi a refatoração — foi a fronteira do módulo.
A Fronteira Que Torna a Pureza Enforcável
O diagrama acima mostra como a fronteira do módulo é o principal enabler. A movimentação de ajuda (getAllLegalMoves, getCastlingMoves, getEnPassantMoves, checkCheck) é marcada como internal — invisível fora do módulo. Isso esconde-as de consumidores externos: o aplicativo React Native/JS e o módulo UI :app veem apenas a superfície pública (applyMove, applyWinConditions). O rig perft é deliberadamente não externo — ele vive no conjunto de fontes de teste do próprio módulo, então pode alcançar o gerador interno para enumerar movimentos legais. Essa é a razão inteira pela qual os auxiliares são internal e não private: privado bloquearia os próprios testes do módulo. A camada sem nenhuma dependência de código em :chess-core é o servidor MCP — ele executa Gradle e Stockfish, então nada da engine pode se infiltrar nele.
Com um conjunto de testes robusto e um localizador de divergência em funcionamento, o próximo passo lógico como engenheiro focado em IA era remover a pessoa do loop de depuração.
O Servidor MCP: Menos Componentes em Movimento para o Agente
O rig é projetado para ser controlado por um loop de agente autônomo — drivers como feature-dev ou ralph-loop — sem uma pessoa no loop. O contrato do loop é um prompt curto e estrito, e segue mais ou menos assim:
- Execute a porta. Se verde, pare.
- Se vermelho, leia
build/perft-divergence.txtpara a posição exata (FEN) e o movimento divergente. - Forme uma hipótese sobre qual regra está implicada — o resumo do loop inclui um resumo dos suspeitos habituais, na ordem de como frequentemente eles mordem geradores escritos à mão (fan-out de promoção, pin de captura en passant, legalidade e rastreamento de direitos de roque, detecção de xeque).
- Corrija
Move.ktem:chess-core. Nunca edite o oráculo. - Reexecute. Repita até verde.
Podemos ter deixado o agente executar comandos de shell brutos e analisar a saída do Gradle. Em vez disso, construímos um servidor MCP (Model Context Protocol) fino (:perft-mcp) usando a SDK oficial Kotlin. Ele expõe a ferramenta local da cadeia de fornecimento do repositório — tarefas Gradle, o binário Stockfish — como ferramentas estruturadas para qualquer agente conectado (por exemplo, Claude Code, OpenCode).
O enquadramento importante: as ferramentas não permitem que o agente faça nada novo. Elas tornam o loop mais rápido e confiável ao remover a análise de texto e o boilerplate de subprocessos. O servidor existe para fazer três coisas:
- Padronizar a interface — em vez de analisar texto ou manipular comandos de shell, o agente chama ferramentas estruturadas com entradas e saídas claras.
- Enforce the contract — a descrição de cada ferramenta incorpora as regras difíceis literalmente (nunca edite o oráculo, nunca enfraqueça uma afirmação, nunca toque na cola da plataforma). O agente não pode chamar uma ferramenta sem ler primeiro.
- Ficar decoupled — o servidor depende nem
:appnem:chess-core, então não pode acidentalmente recolocar a ligação com o gerador sob teste.
Ele expõe três ferramentas, e o padrão mapeia limpo para qualquer módulo de lógica com um conjunto de testes:
run_perft_gate — execute o conjunto de testes. Não recebe entrada específica de xadrez; ele executa Gradle com o filtro correto e retorna pass/fail mais a cauda da saída. Para seu domínio, isso é "execute os testes e me diga se eles estão verdes".
stockfish_divide — pergunte ao oráculo independente. Dada uma entrada específica (aqui, uma posição de tabuleiro), ele retorna a resposta do segundo oráculo para que o agente possa comparar. Este é o recurso que um agente usa quando tem uma hipótese e deseja a verdadeira condição para um caso específico.
read_divergence — leia o relatório de localização. Retorna o arquivo que o localizador escreveu, com a entrada exata onde a lógica divergiu. Este é o recurso que transforma

