Voltar ao arquivo
#ai#agents#coding#llm#aigen

Dei disquetes de Amiga a um agente de IA. Depois de uma noite, Pirates! rodava no Unity

Dei a um agente de programação quatro imagens de disquete com a versão para Amiga de Sid Meier's Pirates!, de 1990, e pedi que ele levasse o jogo para o Unity. Quem trabalhou foi o Claude Opus 5.5 no Claude Code. Depois de uma noite, dava para jogar da tela de título, passando por um duelo, uma cidade e a navegação pelo mapa, até uma batalha naval, com os gráficos e o som originais. Mais importante do que o ritmo é a forma como o resultado foi verificado: o mesmo roteiro de decisões foi executado no port em C# e no código de máquina original rodando em um emulador de processador. Em 802 execuções, cerca de 6 milhões de linhas comparadas, as duas versões mostraram ao jogador a mesma coisa.

Descrevo aqui as etapas desse trabalho porque, juntas, elas formam um método que pode ser repetido com outro jogo e outro modelo. No fim, mostro um segundo projeto feito com o mesmo esquema, um port de Centurion: Defender of Rome do DOS para JavaScript. Os dois são projetos particulares, feitos para aprender: os jogos originais, seus gráficos e seu som continuam protegidos por direitos autorais, e eu não os distribuo.

Tela de título de Pirates! no port para Unity: um galeão com bandeira pirata ao pôr do sol, desenhado com os gráficos originais de 1990.

A tela de título no Unity. A imagem é decodificada em tempo de execução a partir dos arquivos originais do jogo.

Da imagem de disquete ao código descompilado

O agente começou identificando os arquivos. Uma imagem de disquete do Amiga (ADF) é uma cópia bruta dos setores, então algumas dezenas de linhas de Python bastaram para extrair o conteúdo. O programa do jogo revelou-se um executável com overlays: um núcleo fica o tempo todo na memória, e outros três segmentos de código são lidos do disco conforme a necessidade. O Ghidra, um descompilador gratuito, não tem suporte nativo a esse formato. O agente escreveu um script que posiciona todos os segmentos em endereços fixos dentro de uma única imagem e transforma 45 saltos indiretos para os overlays em chamadas comuns. Com isso, o descompilador passou a enxergar cerca de 630 funções como um só programa.

Os dados do jogo trouxeram uma surpresa. Os arquivos com a extensão .rez têm a estrutura dos recursos dos computadores Macintosh, porque a versão para Amiga era um port da versão para Mac. Reconhecido o formato, o agente extraiu 210 textos, 82 listas de strings, os dados das cidades para seis épocas, imagens, animações, sprites e sons. Os textos se mostraram o melhor mapa do código: o número da string passada à rotina "mostrar texto" indica qual tela do jogo uma determinada função trata.

Treze agentes leem o código, nove o reescrevem

O código descompilado tinha quase 29 mil linhas, sem nomes de funções nem de variáveis. O agente principal o cortou em pedaços de cerca de 2 mil linhas e entregou cada um a um agente separado, com o mesmo briefing por escrito. Cada um devia gravar em um arquivo a descrição de todas as funções do seu trecho: fórmulas exatas, constantes, intervalos de sorteio, os números dos textos exibidos e as variáveis globais lidas e gravadas. O briefing continha uma frase que acabou sendo decisiva: uma fórmula errada é pior do que uma lacuna sinalizada. Assim, os agentes tinham de marcar o que não conseguiram determinar e conferir os pontos obscuros no assembly. O descompilador perdia aqui muitos argumentos de função, porque o compilador de 35 anos atrás usava um tipo int de 16 bits.

A segunda etapa teve a mesma divisão. Nove agentes reescreveram as regras do jogo em C#, cada um na sua faixa de endereços. Saíram daí cerca de 16 mil linhas de código, e o estado do jogo foi parar em um array de bytes endereçado exatamente como a memória do original. Não é elegante, mas elimina toda uma classe de erros. O código antigo sobrepõe estruturas umas às outras sem cerimônia e lê tabelas a partir do byte anterior ao seu início. Um port construído sobre a imagem da memória pode ser traduzido linha por linha e comparado com o assembly, e as tabelas de constantes ficam onde o código as procura. O desenho, o som e os controles foram substituídos pelos agentes por chamadas a uma interface que o motor implementa separadamente.

O oráculo: o código original em um emulador de processador

Ler código descompilado não prova que o port se comporta como o original. Por isso o agente construiu uma ferramenta de comparação. O código de máquina original do processador Motorola 68000 roda no emulador Unicorn. As rotinas de sistema, isto é, menus, caixas de texto, leitura dos controles e desenho, foram trocadas por funções em Python. Essas funções respondem a cada pergunta do jogo segundo um gerador de decisões simples e registram tudo o que o jogo mostrou. O port em C# recebe a mesma sequência de decisões por meio de um "jogador roteirizado", que roda fora do Unity. No fim, os dois registros são comparados linha por linha. Os textos contêm ouro, tamanho da tripulação, datas e nomes de cidades, de modo que a coincidência dos registros é um teste forte das regras.

A primeira medição completa deu 800 execuções coincidentes em 802. As duas divergências tinham uma única causa: a aritmética de ponto flutuante. O jogo usa uma biblioteca em software que não arredonda como os processadores de hoje. A raiz quadrada de 81, nela, é 8,999999999999998, e o jogo trunca os resultados para inteiros, de modo que o moral da tripulação saía um ponto alto demais no port em certas proporções entre ouro e tripulação. A correção consistiu em reescrever as rotinas da biblioteca instrução por instrução: 1.572 instruções, 43 rotinas e 21.701 vetores de teste calculados pelo código original, sem nenhuma diferença. Depois dessa mudança, todas as 802 execuções coincidem, e em outras 120 o estado do jogo foi comparado quadro a quadro no duelo, na navegação e nas batalhas naval e terrestre.

A comparação também revelou dois bugs do próprio jogo de 1990: uma leitura do mapa por um ponteiro nulo ao atacar uma cidade na diagonal e uma divisão por zero quando a tripulação cai a zero. Nesses pontos o port precisa escolher um comportamento, e a ferramenta de comparação tem uma chave que obriga o original a seguir o mesmo caminho. O resultado também tem limites. A ferramenta não compara desenho nem som, e caminhos raros, como o casamento, o mapa do tesouro completo ou os postos mais altos, foram pouco exercitados.

A apresentação e os acréscimos que não existiam em 1990

A camada que o jogador vê foi construída pelos agentes no Unity a partir de código, sem cenas prontas. Imagens e animações são decodificadas dos recursos originais, os sprites recebem as cores da paleta da tela atual, e a música foi renderizada em arquivos de áudio por meio da emulação do sequenciador de quatro canais do jogo. O agente controlava o jogo em execução pela linha de comando, tirava capturas de tela e as examinava. Foi assim que apareceram alguns defeitos que nenhum teste de regras pega, como a falta de som na versão para navegador, porque a conversão de áudio exigia o programa ffmpeg, que não estava instalado no sistema.

O mapa do Caribe no port para Unity: ilhas com cidades, o navio do jogador com o indicador de rumo e nuvens de tempestade.

Navegação pelo mapa. O fundo é montado a partir de uma grade de terreno de 160 por 200 células e dos tiles originais.

Depois das primeiras partidas, pedi três mudanças. A pergunta sobre a data de chegada da frota do tesouro era, no original, a proteção contra cópia e exigia o manual impresso; agora o quiz continua, mas a resposta certa aparece nas "anotações do capitão". A lista de cidades mostrava apenas os dados da cidade, porque o jogo pressupunha que o jogador tinha um mapa impresso; agora a cidade escolhida é marcada em um mapa do Caribe. A tela de duelo ganhou uma tabela com a força, o moral e a arma dos dois lados, disposta como no original. Os acréscimos podem ser desligados, e os testes de comparação rodam sem eles, para que os registros continuem batendo com o código de 1990.

Captura de tela do jogo original no Amiga: dois esgrimistas diante de um muro e, embaixo, o título do duelo e uma tabela com Force, Morale e Weapon.

O duelo no original. Fonte da captura de tela: OpenRetro Game Database.

O duelo no port para Unity: esgrimistas no convés de um navio e, embaixo, o título e uma tabela com a força, o moral e a arma dos dois lados.

A mesma tela no port. Os números da tabela são lidos em tempo real do estado do duelo.

Um mapa do Caribe com a cidade de Port-de-Paix marcada, a posição do jogador e um quadro com os dados da cidade e a bandeira francesa.

Um acréscimo em relação ao original: a cidade escolhida em um mapa do Caribe.

Centurion: o mesmo esquema em JavaScript, com jogabilidade ampliada

O segundo projeto é Centurion: Defender of Rome, um jogo para DOS de 1990, levado para JavaScript e para o elemento Canvas, sem emulador. Os passos foram os mesmos, com outros nomes. Primeiro foi preciso descompactar o executável, porque ele estava comprimido com a ferramenta EXEPACK. Cinco módulos do jogo (batalha terrestre, arena de gladiadores, corrida de quadrigas, batalha naval e a introdução) ficavam em um arquivo de overlays e compartilhavam um único segmento de dados. O estado da campanha roda sobre uma cópia viva dos 64 quilobytes desse segmento, porque o motor do jogo depende de estouro de 16 bits, divisão inteira e bytes com sinal, que os números de ponto flutuante do JavaScript não reproduzem sozinhos. O jogo salvo coincide byte a byte com o formato original, o que os testes verificam.

Tela de título de Centurion no port para navegador: a palavra CENTURION sobre um mapa do Mediterrâneo, um painel de créditos e os botões Start Game, Difficulty e Game Options.

A tela de título de Centurion no navegador, com os gráficos originais.

Aqui fui além de um port fiel. Acrescentei uma chave para regras ampliadas, desligada por padrão: fusão de legiões e várias legiões em uma mesma província, vélites de armamento leve, elefantes de guerra e desembarques pelo mar. O código desse port foi escrito por modelos diferentes dos de Pirates: principalmente o Gemini 3.8 Flash e, em parte, o gpt-6.1-sol da OpenAI, executado pelo Codex. A escolha veio das ferramentas que eu tinha à mão, e não de uma comparação entre modelos.

O menu de opções do jogo no port de Centurion: o item Extra Features: OFF, o nível de dificuldade, salvar e carregar e, ao lado, o mandato do Senado com o posto e o saldo do tesouro.

O menu de opções. O primeiro item ativa as regras ampliadas, que não existiam no original.

A tela da província Italia no port de Centurion: uma vista da cidade, o menu de jogos com a construção do anfiteatro e a situação da província: ânimo, tributo, tropas.

A tela da província com o menu de jogos.

Os problemas que o Gemini encontrou em Centurion

A maior parte do tempo foi gasta em pontos nos quais o código descompilado ou os nomes dos arquivos sugeriam algo diferente do que o jogo realmente faz. Cinco exemplos tirados das anotações do projeto:

  • Overlays sem dados. Os cinco módulos do jogo ficam em um único arquivo, OVL0.OVL, e são carregados no mesmo endereço, mas todos usam um segmento de dados compartilhado. Por isso, o código de um overlay aberto sozinho no Ghidra não mostrava nenhuma string nem tabela. Um script juntou cada overlay ao segmento de dados em cinco executáveis sintéticos, e só então o descompilador ligou o código dos minijogos às estruturas da campanha.
  • Fronteiras das províncias conquistadas. Contornar uma província a partir do mapa das regiões clicáveis com o mouse dava linhas serrilhadas, que não combinavam com o mapa desenhado à mão. O código mostrou outro mecanismo: na imagem do mapa, cada província tem um caminho oculto, com um pixel de largura e de cor escura, e uma tabela nos dados fornece um ponto de partida para cada uma das 27 províncias. O jogo repinta esse caminho com a cor do império, começando pelo ponto de partida, e o port faz o mesmo.
  • Quadros que voltam atrás. O loop original do jogo rodava a 9–12 Hz, e um navegador desenha 60 vezes por segundo. Em três pontos, um sprite recuava um quadro por um instante. O globo da introdução gira pelo deslocamento de 14 cores da paleta em 10 passos, mas o primeiro passo da tabela tem valor zero; deslocar a paleta em todos os passos fazia os continentes darem um tranco a cada volta. Nas coortes em marcha e nas investidas dos gladiadores foi preciso reproduzir a ordem com exatidão: primeiro o quadro intermediário, depois a mudança de posição.
  • Nomes de arquivo enganosos. Os bitmaps BB1.CBM e BB2.CBM pareciam faixas de batalha e, desenhados no alto da tela, cobriam o campo de batalha inteiro. O código só os carrega quando o computador não tem placa VGA: são fundos de tela cheia para as placas EGA e CGA, mais antigas. A mesma rotina revelou que o posicionamento antes da batalha tem duas etapas. A escolha da formação substitui a lista de táticas disponíveis, e a manobra de Canas só aparece depois que a cunha é escolhida.
  • Música quase inaudível. Depois de decodificadas as 17 faixas e os 24 instrumentos do driver da placa de som AdLib, as melodias estavam certas, mas tocavam a cerca de −55 dB. A desmontagem do driver mostrou que, a cada nota, ele subtrai um quarto do volume da nota da atenuação gravada no instrumento. Uma linha de código devolveu cerca de 32 dB. Um segundo defeito estava do lado do navegador: em notas muito curtas, o cancelamento das mudanças de volume agendadas na Web Audio API apagava o ataque do som, que ainda estava em andamento, e silenciava a nota.

Uma batalha terrestre no port de Centurion: uma lista de cinco táticas para a formação em cunha, entre elas a Cannae Tactic, e as unidades posicionadas no campo de batalha.

A segunda etapa do posicionamento antes da batalha: as táticas disponíveis para a formação em cunha.

As corridas de quadrigas no Circus Maximus foram um trabalho à parte. Ali, a quadriga não é uma imagem única. Bitmaps separados para cada combinação de três classes de quadriga, quatro cores de condutor, quatro ações, dezesseis inclinações na curva e três quadros de galope dariam mais de 2.300 quadros, então os autores do jogo montaram na memória uma árvore de nós de 16 bytes. Cada nó aponta para o próximo elemento do mesmo nível e para o seu primeiro filho, e guarda um índice de quadro, uma tabela de deslocamentos para os sucessivos quadros da animação e uma flag de visibilidade. A raiz são os quatro cavalos. A caixa do carro tem um deslocamento horizontal adicional, no qual a física da curva grava uma inclinação de 0 a 15 pixels, e o condutor é seu filho e herda essa posição. É por isso que, na curva, o condutor se inclina junto com a caixa, enquanto os cavalos continuam correndo no eixo da pista. Os ramos com o eixo que se parte e com os destroços capotando existem o tempo todo; apenas estão com a flag de visibilidade desligada.

Diagrama da árvore de sprites da quadriga em Centurion: o nó com os quatro cavalos, o nó da caixa em três classes, os nós dos estados de acidente, quatro animações do condutor e a quadriga montada sem inclinação e com inclinação de 12 pixels.

A árvore de sprites da quadriga reconstruída a partir dos dados do jogo. O diagrama foi feito no projeto com os recursos originais decodificados.

A pista também não é montada com tiles. O jogo expande 72 pares de coordenadas gravados em 368 pontos de controle e divide a posição transversal na pista em 16 faixas. Da faixa depende a velocidade segura na curva, calculada como (número da faixa + 15) × 4: junto à parede interna ela é 60, junto à barreira externa, 116–120. A faixa interna é o caminho mais curto, mas entrar no grampo acima desse limite inclina a caixa, empurra a quadriga para fora e dispara um sorteio de acidente, tanto mais provável quanto maior o excesso. O marcador vermelho na barra de velocidade mostra esse limite ao jogador.

A geometria da pista do Circus Maximus: retas, zonas de frenagem e grampos marcados em cores sobre a imagem do estádio e um gráfico da velocidade segura subindo de 60 na faixa interna a 120 na externa.

Os 368 pontos da pista e a velocidade segura na curva conforme a faixa.

O que fica desse trabalho além dos jogos

Registrei o método como uma skill, isto é, um conjunto de instruções que o agente carrega quando recebe uma tarefa parecida. Ela contém três princípios tirados dos dois projetos. As regras do jogo são portadas com fidelidade, e a apresentação é substituída por uma interface. O port mantém o layout de memória do original. Não se confia no resultado enquanto ele não passar por uma comparação com o código original. A skill inclui ainda os briefings para os agentes de análise e de portabilidade e os scripts do projeto Pirates.

A conclusão mais interessante diz respeito aos fãs, não aos agentes. Quem tem um jogo de que gosta e sente falta de alguma coisa nele há anos pode hoje refazê-lo do seu jeito, de forma rápida e barata, sem o código-fonte. Em Pirates, o quiz que exigia o manual impresso agora dá a resposta, e apareceu um mapa que o jogo nunca teve. Em Centurion, acrescentei regras que o original não tinha: fusão de legiões, elefantes de guerra e desembarques pelo mar. Depois que as regras do jogo viram código comum em C# ou JavaScript, a próxima mudança é uma alteração de código comum, e não um patch no binário. Do mesmo modo dá para levar um jogo para um motor mais rápido ou para outra plataforma: Centurion roda no navegador, e Pirates é compilado para Linux e para o navegador. O método também não está preso a jogos antigos. Um descompilador, um emulador de processador e a comparação dos registros funcionam do mesmo jeito para um programa mais recente; mais código significa mais trabalho, não outra abordagem.

Fontes

  • Materiais próprios dos dois projetos: anotações da análise do código, briefings para os agentes, o relatório da ferramenta de comparação (802 execuções, a lista das diferenças encontradas) e a descrição do port de Centurion com suas especificações. Não estão publicados.
  • National Security Agency, Ghidra, o pacote de engenharia reversa usado para descompilar os dois jogos.
  • Unicorn Engine, o emulador de processadores usado para executar o código 68000 original.
  • OpenRetro Game Database, a página de Pirates! para Amiga, fonte da captura de tela do jogo original.

Uso conteúdo gerado por IA como parte do meu processo diário de aprendizado.