corewars

CoreWar

"Saiba como otimização de código assembly pode ser divertido"

<<<Voltar

Introdução

CoreWar é um jogo um pouco diferente dos demais, e definitivamente direcionado para um publico bem específico. Apresentado ao público inicialmente por A. K. Dewdney em um artigo da Scientific American de 1984... um pouquinho antigo - você deve estar pensando - para um jogo de computador.

Não existem muitos iniciantes interessados no jogo CoreWar. É claro, isto é bem natural - quase ninguém considera otimização de código Assembler divertido - mas uma das razões para a existência deste documento para iniciantes é a dificuldade de se encontrar informações sobre o básico deste jogo. Principalmente em português (provavelmente este sejá o mais completo ou até o único documento em português sobre o assunto), porém em inglês existem vários documentos, sites dedicados e sociedades do jogo. Apesar de se tratar do básico do jogo, é importante que o leitor tenha alguma noção do que sejá "programação", muito raciocínio lógico abstrato será requisitado também!

O que é CoreWar?

CoreWar é um jogo aonde jogadores (programadores) escrevem programas e os colocam em uma arena virtual que é na verdade uma parte da memória do computador, depois de carregados, os jogadores não podem interferir em suas criações, apenas observá-las se desenvolvendo e executando as funções para qual foram programadas. O objetivo de cada programa é ao mesmo tempo sobreviver no ambiente hostil da memória e "matar" os outros programas, porém não existem armas para isso, existem maneiras muito mais inteligentes de faze-lo. Um programa pode lançar "bombas" de código inválido para destruir seu oponente (divisão por zero, por exemplo), reprograma-los para trabalharem para você ou até captura-los e confina-los em sua própria armadilha obrigando-os lhe ajudar a aniquilar o restante dos inimigos, seu programa poderá se multiplicar pela memória se auto copiando, assim, se uma cópia é destruída, ainda restam outras... Poderá infectar código inimigo como um vírus se aproveitando das rotinas escritas por outros programadores para seu beneficio. Seu programa poderá manter uma rotina de auto reparo para consertar danos causados por ataques inimigos e até sofrer metamorfoses mudando de estratégia dependendo da necessidade.

Esses programas são escritos numa linguagem chamada RedCode, para quem tem noção de programação, é uma espécie de assembler bem agressivo, depois de escritos, são compilados e executados por um programa chamado MARS (Memory Array Redcode Simulator), eu criei as minhas versões do MARS (em português é claro), mais adiante explicarei todas suas funções e como organizar campeonatos.

Ambos, RedCode e MARS são muito abstratos e simples se comparados com a verdadeira linguagem de programação e a arquitetura dos computadores reais, isso é bom já que o código é escrito para performance e não para claridade de leitura. Poderíamos utilizar o assembler comum, mas ai provavelmente só haveriam umas duas ou três pessoas no mundo capazes de escrever um programa CoreWar e mesmo eles não entenderiam totalmente o código. seria interessante, mas provavelmente levaria anos para se atingir um nível de habilidade moderada em CoreWar.

Como funciona?

O lugar em que acontece a "guerra" dos programas é bastante simples, é uma memória ou também chamada de Core, composta por unidades chamadas "instruções" - contrapondo a memória comum que é composta por bytes - cada unidade da memória, ou instrução, pode ser dividida em três partes(mas em geral as tratamos como uma unidade indivisível), mais adiante aprofundaremos no estudo das instruções. A memória (Core) é circunavegável, assim, após a última instrução vem a primeira e assim por diante.

Na verdade não há como os programas saberem aonde a memória acaba ou inicia, nem a posição absoluta em que se encontram, já que os endereços não são absolutos e sim relativos, isto é, a instrução zero não significa a primeira instrução da memória, mas sim a instrução que contém o endereço, a próxima instrução é um, e a anterior obviamente é -1.

Os programas guerreiros - chamarei também de warriors(para não perder a originalidade americana) - também são compostos por instruções, exatamente iguais as que a memória contém, é claro que o número de instruções de cada programa é muito menor que o total da memória, portanto, esses programas no inicio de cada round são "carregados" para a memória, escritas suas instruções em um local aleatório da memória e, é claro, com uma distância respeitável entre eles.

Geralmente os compiladores estabelecem limites quanto ao tamanho máximo de um programa guerreiro, comumente 100 instruções - acredite, você dificilmente escreverá um programa que chegue na metade desse limite - A memória também tem o seu limite de tamanho, geralmente 8000 instruções, e no inicio da partida a memória é carregada com instruções "vazias", chamadas de instruções DAT, estudaremos melhor os tipos de instrução depois, porém, adiantando um pouco, se o seu guerreiro cair em uma instrução DAT ele morre; você deve concluir que o core(memória) é um ambiente bem hostil.

Como você pode ver, as unidades básicas da memória e dos programas são as instruções, e não os bytes como de costume. Aprofundando melhor, já disse que cada instrução é composta por três partes: O código operacional(que especifica a ação desejáda, veremos mais adiante os tipos), o endereço fonte(campo A) e o endereço de destino(campo B).

Cada programa começa com um processo ou "cabeça", na verdade as instruções que você escreveu para o seu guerreiro quando carregadas na memória, são escritas e fazem parte da memória. Você possui um "processo" que pode ser comparado á uma agulha de leitura do disco de vinil, esta agulha(processo) vai de instrução a instrução, uma apos a outra a realiza a ação determinada pela instrução, ele lê e executa instrução após instrução. É claro que isso acontece de forma bastante rápida, você não conseguirá acompanhar instrução por instrução do seu processo, como num jogo de "turnos", o processo é tão rápido que parece que os guerreiros estão sendo executados simultaneamente.

O processo, comparado a uma agulha de leitura da memória, é o único elemento particular do seu guerreiro, mesmo as instruções que você escreveu para ele depois de carregadas na memória não são suas e sim da memória, estando sujeitas a mudanças e reescritas como qualquér outra instrução comum.

Apesar do processo mover de instrução a instrução, sua posição na memória pode ser modificada por determinadas instruções chamadas JMP ou Jumpers(pulos), aonde seu processo pode se mandado para outra posição relativa posterior ou anterior a esta instrução.

Se existe mais de um programa na memória, a execução é alternada, os processos de cada programa são executados um a um por vez(como num jogo de tabuleiro).

Para o programa morrer seu processo tem que deixar de existir, para isso ele deve executar uma instrução ilegal, como o DAT(instrução vazia), ou uma divisão por zero(DIV). O objetivo do seu programa é colocar uma instrução ilegal no caminho do processo de seu oponente e ao mesmo tempo evitar que o mesmo aconteça com você, sendo o menor possível para ser difícil de ser atingido ou mudar constantemente de lugar e outras estratégias que veremos adiante.

Entendendo o RedCode

As instruções do RedCode

O número de instruções do RedCode tem crescido com cada padrão, do original de 5 sugerido por A. K. Dewdney para atualmente 18 ou 19. Isso não inclue os "modificadores" que permitem centenas de combinações, mas não se preocupe, você não precisará aprender todas, apenas entender.

Aqui está a lista de todas os tipos instruções utilizadas no RedCode - já que cada código destes de três letras não são considerados uma instrução integral - são chamados de "CÓDIGOS OPERACIONAIS":

DAT - data (mata o processo)

MOV - move (copia informação de um endereço para outro)

ADD - add (adiciona um número de um endereço a outro endereço)

SUB - subtract (subtrai um número de um endereço de outro endereço)

MUL - multiply (multiplica um número de um endereço por outro endereço)

DIV - divide (divide um número de um endereço por outro endereço)

MOD - modulus (divide um número de um endereço por outro endereço e dá o resto)

JMP - jump (Move o processo para outro endereço)

JMZ - jump if zero (Testa um número e, se for 0, move o processo para um endereço)

JMN - jump if not zero (Testa um número e, se for diferente de 0, move o processo)

DJN - decrement and jump if not zero (Decresce um número e, se for diferente de 0, move o processo para um endereço)

SPL - split (Começa um segundo processo em outro endereço)

CMP - compare (mesmo que SEQ)

SEQ - skip if equal (Compara duas instruções e pula a próxima se forem iguais)

SNE - skip if not equal (Compara duas instruções e pula a próxima se forem diferentes)

SLT - skip if lower than (Compara duas instruções e pula a próxima instrução se A < B)

NOP - no operation (não faz nada)

Não se preocupe se você não entendeu algumas, ou nenhuma, isto é apenas um primeiro impacto com as instruções, breve você estara habituado com elas e terá habilidades para escrever guerreiros em RedCode.

É fundamental que você entenda a estrutura de um instrução RedCode, que são as unidades básicas da memória(Core) e os blocos com os quais você ira escrever os programas guerreiros. Uma isntrução integral é definida sintaticamente por:

<Cód. Operacional> <fonte(campo-A)>, <destino(campo-B)>

O <Cód. Operacional> é uma palavra de três letras(uma das 19 da lista acima) que define exatamente a ação que tal instrução realizará caso sejá executada.

A instrução contém dois números, a fonte(campo-A) e o destino(campo-B), eles guardam os endereços da instrução fonte e da instrução destino que participarão da ação definida pelo Codigo operacional. lembre-se que os endereços são relativos à posição da instrução.

Adiantando alguns conceitos(você não precisa compreender agora), o Código operacional pode ser dividido em três partes, ou melhor, quatro: O código operacional em si(uma das 19 palavras de três letras da lista acima), um sufixo modificador(que atribuirá uma característica especial a função da instrução, veremos adiante) e para cada campo, A e B, existe um "modo de endereçamento", basicamente esse pode ser direto, indireto ou imediato; você pode apontar na fonte(campo-A) e no destino(campo-B) para outra instrução na memória, para isso você usa um modo de endereçamento específico, os estudaremos a seguir, porém lembre-se que de não existe modo de endereçamento absoluto! já que é impossível conhecer a posição absoluta(ou inicio e fim do Core) de uma instrução na memória.

O Imp

Acredito que uma boa forma de aprender algo é atraves de exemplos, o mais simples programa funcional em RedCode e provavelmente o primeiro é o chamado "Imp". publicado inicialmente por A. K. Dewdney na Scientific American de 1984 no artigo que primariamente intruduziu CoreWar ao público.

MOV.i 0, 1

Ou também

MOV.i $0, $1

;O $ pode ser ignorado, significa endereço DIRETO.

Sim, apenas uma instrução MOV, mas o que ela faz? este ".i" é um modificador que significa "instrução"(veremos os modificadores mais adiante), lembre-se que na memória os endereços são relativos, sendo que ao se referir à instrução 0 ele esta se referindo a ele próprio. Então este MOV copia ele próprio(fonte=0) para a posição logo a sua frente(destino=1). depois de executada fica assim:

MOV.i 0, 1 ;esta instrução acabou de ser executada

MOV.i 0, 1 ;esta instrução será executada a seguir

Agora o processo ira executar a instrução que ele acabou de escrever! e como a instrução é exatamente igual a anterior ele irá fazer a mesma coisa: se copiar para frente, e assim continuara, e como a memória é circunavegável, o IMP alcançara sua posição inicial novamente e continuara se movendo infinitamente pela memória deixando um rastro de MOVs.

Então o IMP cria seu próprio código enquanto segue? Sim, em CoreWar isso é mais uma regra do que uma exceção, para ser bem sucedido é quase que obrigatório modificar e escrever seu próprio código.

O Dwarf

O Imp tem um grande problema: ele nunca ganha! no máximo empata, caso ele atinjá um processo inimigo e reescreva em sua atual posição, este também começara a executar MOV.i 0, 1 se tornando um IMP ele próprio, resultando em um empate. Para matar um programa você deve(na grande maioria das estratégias) copiar um DAT(instrução vazia) sobre seu código.

Este é o que outro clássico programa escrito por Dewdney, o Dwarf, faz. ele "bombardeia" a memória com DATs em intervalos regulares, evitando é claro que não atinjá a si mesmo.

ADD.ab #4, 3 ;o processo inicia aqui

MOV.i 2, @2

JMP.b -2, 0

DAT.f #0, #0

Na verdade, isto não é precisamente o que Dewdney escreveu, a primeia instrução desta vez é um ADD, ele adiciona um número a outro, seu modificador(.ab) diz que ele ira adicionar o campo A da instrução #4 ao campo B da instrução 3,(lembre-se que os endereços são relativos), porem, um simbolo colocado na frente do 4(#), é chamado de "modo de endereçamento"(você já conhece o $), o # significa endereçamento IMEDIATO, ele sempre se refere a instrução relativa zero(ele mesmo!), então o DAT ira adicionar o seu proprio campo-A(4) ao campo-B da instrução que está três posições adiante, o resultado será:

ADD.ab #4, 3

MOV.i 2, @2 ;próxima instrução(processo)

JMP.b -2, 0

DAT.f #0, #4 ;0 + 4 = 4

Se você colocasse o simbolo de endereçamento imediato(#) na frente do 3, o ADD iria adicionar seu campo-A ao seu proprio campo-B, o resultado seria ADD.ab #4, #7.

Até agora você conhece o endereçamento DIRETO($) e o IMEDIATO(#), o direto se refere a instrução relativa a atual, se o número for positivo, esta instrução esta adiante, se for negativo, se refere a uma instrução posterior; e se for zero, se refere a si próprio.

O modo de endereçamento IMEDIATO(#), é como se fosse um modo DIRETO=0, ele se refere sempre a si próprio independente do número que o segue! Prosseguindo com o Dwarf...

O MOV outra vez nos presenteia com outro modo de endereçamento: o @ ou "INDIRETO-B", significa que o DAT não vai ser copiado sobre si mesmo como parece(qual a vantagem disso?) mas

sobre a instrução que se campo B aponta, vejámos:

ADD.ab #4, 3

MOV.i 2, @2 ; ---

JMP.b -2 0 ; | +2

DAT.f #0, #4 ;<------ Aqui que o campo B de MOV.i aponta

... ; |

... ; | +4

... ; |

DAT.f #0, #4 ; <------ Aqui que o campo B de DAT aponta.

Como você ve, o DAT é copiado 4 instruções a frente dele, seu campo B é usado com "ponteiro" pelo MOV, a instrução a seguir é um JMP(ignore seu modificador), o JMP ignora qualquer modificador e também seu campo-B. ele simplesmente pula para duas instruções antes de si, reiniciando o ciclo no MOV. Se colocassemos um endereçamento imediato(#) no campo A do JMP, ele fazeria o processo pular para a posição relativa 0(ignorando o número), ele mesmo,o processo então não saira do lugar.. uma espécie de armadilha de processos.

Adiantando conceitos, existe também o endereçamento INDIRETO-A(*), funciona do mesmo modo que o INDIRETO-B(@), porém, utiliza o campo-A da instrução relativa como ponteiro.

O MARS não traça cadeias maiores de endereçamento, o indireto é o máximo, já que o campo-B de MOV aponta para o campo-B de DAT, a instrução será copiada 4 instruções a frente de DAT INDEPENDENTE do modo de endereçamento escrito no ponteiro, interessando apenas o número.

Agora o ADD e MOV serão executados novamente, e quando o processo alcança o JMP, o programa estará desta forma:

ADD.ab #4, 3

MOV.i 2, @2

JMP.b -2 0 ; proxima instrução

DAT.f #0, #8

...

...

...

DAT.f #0, #4

...

...

...

DAT.f #0, #8

O Dwarf continuará colocando DAT a cada quatro instruções(na esperança de atingir algum inimigo) ate dar a volta completa na memória e chegar em si mesmo.

...

DAT.f #0, #-8

...

...

...

DAT.f #0, #-4

ADD.ab #4, 3 ;processo

MOV.i 2, @2

JMP.b -2 0

DAT.f #0, #-4

...

...

...

DAT.f #0, #4

...

agora o ADD ira tornar o DAT novamente #0, #0, o MOV ira fazer um exercicio de futilidade copiando o DAT sobre si mesmo, e todo o processo inicia novamente do começo.

Isso é claro não acontecera a menos que o tamanho da memória sejá divisível por 4, pois se o Dwarf copiar um DAT sobre si mesmo de 1 a 3 posições antes do DAT ele ira se matar. Por sorte o tamanho mais famoso de memória é 8000, seguido por 8192, 55400, 800, todos divisíveis por 4, então seu Dwarf estara seguro.

Nota: eu poderia ter iniciado o bombardeio de DAT.f #0, #0, a memória limpa inicialmente é toda preenchida por DAT, aonde eu escrevi três pontos(...) é na verdade DAT.f 0, 0. Eu vou continuar usando três pontos para representar a memória vazia a fim de tornar a leitura mais facil.

Os Modos de Endereçamento

Nas primeiras versões do CoreWar, os modos de endereçamento eram apenas o IMEDIATO(#), o DIRETO($ ou nada) e o INDIRETO-B(@). Depois adicionou-se o endereçamento PREDECRESSOR-B(<). Funciona do mesmo modo que INDIRETO-B só que o ponteiro será decrecido em um antes que o destino sejá calculado, exemplo:

DAT.f #0, #5

MOV.i 0, <-1 ;processo

Quando o MOV é executado, o resultado é:

DAT.f #0, #4 ; ---.

MOV.i 0, <-1 ; |

... ; | +4

... ; |

MOV.i 0, <-1 ; <---'

O padrão ICWS '94(Internationnal Core War Standard) adicionou quatro modos de endereçamento, a maioria para utilizar o campo-A como ponteiro também, resultando em um total de 8:

# - immediato

$ - direto (o $ pode ser omitido)

* - indireto-A

@ - indireto-B

{ - predecressor-A

< - predecressor-B

} - postincressor-A

> - postincressor-B

Os endereçamentos postincressores funcionam do mesmo modo que o predecressor, só que o ponteiro será incrementado de um após o calculo do destindo:

DAT.f #5, #-10

MOV.i -1, }-1 ;processo

depois da execução, o resultado é :

DAT.f #6, #-10 ; -----

MOV.i -1, }-1 ; |

... ; |

... ; | +5

... ; |

DAT.f #5, #-10 ; <----

Um coisa importante para lembra sobre os Predec/Posincressores é que o ponteiro será decrescido ou acrescido mesmo que o campo não sejá usado para nada. Então JMP.f -1, <100 ira decrescer o campo B da instrução 100(PosDecressor-B) apos a atual. mesmo que o valor que este aponta não sejá usado para nada. Mesmo DAT.f <50, <60 ira descrescer os ponteiro antes de matar o processo.

O Imp-Gate

Agora que conhecemos os modos de endereçamentos, principalmente os Prede/Posincressores(que são ferramentas super úteis), podemos contruir um programa para matar o IMP!, apresento o IMP-GATE:

JMP.b $0, <-3 ;processo inicia aqui

Note que o JMP(ignore seu modificador) faz o processo "pular" para o endereço que esta em seu campo-A, ou sejá, ele próprio(0), já que o endereçamento é relativo(direto,$). Então o processo não sai do lugar!, mas lembre-se que os endereçamentos Predec/Posincressores não são ignorados mesmo que se ponteiro não sejá usado para nada, portanto, à cada execução o campo-B da instrução -3(3 instrução antes de JMP) será decrescida em 1, após 5 ciclos vejá o resultado:

DAT.f $0, -5

...

...

JMP.b $0, <-3 ;processo

O IMP atingirá a parte superior deste programa... vejámos o que acontece, ai vem o IMP!

MOV.i $0, $1

MOV.i $0, $1 ;processo IMP

DAT.f $0, $-5

...

...

JMP.b $0, <-3 ;processo IMP-GATE

O IMP atingiu a instrução acima do "DAT.f 0, -5", agora o IMP sobrescreve a instrução no próximo ciclo:

MOV.i $0, $1

MOV.i $0, $1

MOV.i $0, $0 ;processo IMP(o IMP-GATE decresse o campo-B(1 - 1 = 0))

...

...

JMP.b $0, <-3 ;processo IMP-GATE

O IMP se copiou para frente, em seguida o IMP-GATE executou e decresceu o campo-B da isntrução do IMP, agora o ficou MOV.i 0, 0. o que acontece? o IMP irá copiar a instrução 0 para a instrução 0, realizando uma futilidade copiando o MOV sobre si mesmo, no próximo ciclo:

MOV.i $0, $1

MOV.i $0, $1

MOV.i $0, $-1 ;O IMP-GATE decresse mais um do campo-B(0 - 1 = -1))

... ;Processo IMP

...

JMP.b $0, <-3 ;processo IMP-GATE

Agora o IMP-GATE descresce mais um de MOV, mas o dano já está feito!, o processo do IMP alcança o "..." que significa DAT, deste modo, o IMP executando DAT é eliminado.

A fila de processos

Se você observou a lista de instruções um pouco acima, deve ter se intrigado com uma instrução chamada SPL(split), não há nada parecido em nenhuma linguagem de programação comum que eu conheça...

Bem cedo na história do CoreWar, foi sugerido por A. K. Dewdney que se adicionasse "multitarefas" ao sistema, significa que um programa poderia trabalhar com mais de um processo! isso tornaria o jogo muito mais interessante do que um processo para cada programa.

A instrução utilizada para criar processos adicionais é o SPL(um processo adicional por vez), ele é como o JMP, utiliza o endereço em seu campo A para iniciar o novo processo, a diferença do JMP e SPL, é que no SPL além de iniciar um processo no endereço do campo A, ele também continua a execução normalmente depois de si..

Os dois - ou mais - processos criados irão repartir o seu tempo de execução igualmente

Ao invés de um unico processo que está endereçado à instrução atual, o MARS mantém um lista(fila) de processos ordenados pela ordem em que foram criados, e executados nessa ordem, quando um processo executa um DAT, ele é eliminado da fila. Se todos os processos morrem, o guerreiro perderá a batalha.

É importante lembrar que cada programa possui a sua própria fila de processos é cada processo será executado alternadamente independente do tamanho da fila. Por isso o tempo dos processos será dividio igualmente. Se o programa 1 tem 3 processos, e o programa 2 apenas 1, então a ordem de execução será:

1. programa 1, processo 1

2. programa 2, processo 1

3. programa 1, processo 2

4. programa 2, processo 1

5. programa 1, processo 3

6. programa 2, processo 1

7. programa 1, processo 1

8. programa 2, processo 1

E finalmente um exemplo de programa utilizando o SPL:

SPL.b $0, $0 ; execução inicia aqui

MOV.i $0, $1

Os modificadores não fazem efeito em SPL(.b), assim como no JMP. vejá que o SPL aponta para si mesmo em seu campo A($0)(o campo B é ignorado), sendo assim o novo processo será iniciado nele mesmo:

SPL.b $0 $0 ; processo 2

MOV.i $0, $1 ; processo 1(inicial)

Note que a segunda linha é um IMP, aonde está o processo 1, o novo processo(2) foi criado no endereço do campo A de SPL(0), sobre ele próprio.. depois dos dois processos serem executados, o programa ficará assim:

SPL.b $0 $0 ; processo 3

MOV.i $0, $1 ; processo 2

MOV.i $0, $1 ; processo 1

Esse programa, irá lançar uma série de IMPs, um após o outro, ele continuará a faze-lo até que o primeiro IMP tenha circundado a memória e sobreescrito o SPL.

Existe um limite para o tamanho da fila de processos, esse limite pode ser bem grande, como o tamanho da própria memória, quando esse limite é atingido o SPL para de criar processos e funciona como a função NOP(que não faz nada).

Os modificadores de instrução

A coisa mais importante trazida pelo padrão ICWS '94 não foram as novas instruções nem os modos de endereçãmento, mas foram os modificadores. no velho padrão de '88 os endereçamentos sozinhos decidiam que parte das instruções seriam afetadas por uma operação. Por exemplo: MOV 1, 2 sempre copia uma instrução inteira, enquanto MOV #1, 2 copia um unico número(1). (e sempre para o camp B!)

Naturalmente isso causava algumas dificuldades. E se você quisesse mover somente o campo A e B de uma instrução, mas não seu código operacional? (você precisaria usar ADD) E se voc~e quisesse mover um valor do campo B para o campo A? (possivel mas muito dificil) Para clarear essas situações, os modificadores foram inventados.

Os modificadores são sufixos que determinam que parte da fonte(campo A) e do destino(campo B) serão afetadas. Por exemplo: MOV.AB 4, 5 Irá copiar o campo A da instrução 4 para o campo B da instrução 5(lembre-se que são instruções relativas!!). Existem 7 modificadores diferentes disponíveis:

MOV.A - copia o campo-A da fonte para o campo-A do destino.

MOV.B - copia o campo-B da fonte para o campo-B do destino.

MOV.AB - copia o campo-A da fonte para o campo-B do destino.

MOV.BA - copia o campo-B da fonte para o campo-B do destino.

MOV.F - copia ambos A e B da fonte para A e B do destino respectivamente.

MOV.X - copia ambos A e B da fonte para B e A do destino(trocados)

MOV.I - copia a instrução inteira da fonte para o destino

Naturalmente os modificadores podem ser usados em todas as instruçõs. Algumas instruções, entretanto, como JMP e SPL não ligam para modificadores.

Já que nem todos modificadores fazem sentido para todas as instruções, eles irão se comportar com o mais parecido que faz sentido. os casos mais comuns envolvem o modificador .I; Para tornar a linguagem mais simples e abstrata, não foram definidos números equivalentes para os códigos operacionais, Portanto usar operações matemáticas com eles não fará sentido nenhum. Isso significa que para todas as isntruções exceto MOV, SEQ e SNE (e CMP que é um apelido para SEQ) o modificador .I significará o mesmo que .F.

Outra coisa para lembrar sobre .I e .F é que os modos de endereçamento são também partes do código operacional, e não serão copiados por MOV.F(somente os números)

O Vampiro-Imp

Uma estratégia bem legal adotada por alguns guerreiros é a técnica do vampiro, o processo inimigo é capturado e forçado a trabalhar para você. Aproveitando o Imp já descrito acima, quero apresentar o Vampiro de Imps, ele se parece muito com o Imp-Gate, mas ao invés de aniquiliar o pobre Imp, que tal escraviza-lo para fazer um servicinho para depois mata-lo? parece muito mais interessante do que simplesmente descarta-lo. Vejámos a estrutura do Vampiro-Imp:

MOV.i $3, $4

JMP.b $-1, >-1

JMP.b $0, <-3 ;processo inicia aqui.

Ignore as duas primeiras linhas, e note que a instrução aonde o processo inicia é exatamente igual ao Imp-gate, o processo ficara "pulando" nesse mesmo local enquanto decresce a instrução relativa -3. Depois de 5 ciclos, vejá como fica:

DAT.f $0, $-5 ;o campo-B dessa instrução é descrescido 5 vezes

MOV.i $3, $4

JMP.b $-1, >-1

JMP.b $0, <-3 ;processo do vampiro-Imp

Otimo, até aqui funciona como um IMP-gate, agora vamos colocar o dito cujo na história, ai vem o IMP!

MOV.i $0, $1 ;O imp escreve sobre o DAT que havia aqui

MOV.i $3, $4

JMP.b $-1, >-1

JMP.b $0, <-3 ;processo do vampiro-Imp

Em seguida o Vampiro-Imp novamente decresce o campo-B, desta vez do MOV, tornando-o:

MOV.i $0, $0 ;O imp se escreve sobre si mesmo!

Depois dessa futilidade, o IMP avança para a nossa armadilha!!

MOV.i $0, $-1 ;instrução MOV do IMP abandonada... segue para seguinte

MOV.i $3, $4 ;processo do IMP

JMP.b $-1, >-1

JMP.b $0, <-3 ;processo do vampiro-Imp

Esse MOV copia a instrução 3 sobre a instrução 4:

MOV.i $3, $4 ;processo do IMP

JMP.b $-1, >-1 ;

JMP.b $0, <-3 ;processo do vampiro-Imp

DAT.f $0, $0 ;<----copia esta instrução

DAT.f $0, $0 ;<----para esta

Em seguida ele segue para o JMP, que apenas o retorna para o MOV, mas note o Posincressor-B!

ele incrementa o campo-B do MOV.. ao chegar no MOV novamente, nosso IMP capturado fara o seguinte:

MOV.i $3, $5 ;processo do IMP(campo-B incrementado)

JMP.b $-1, >-1 ;

JMP.b $0, <-3 ;processo do vampiro-Imp

DAT.f $0, $0 ;<----copia esta instrução

... ;

DAT.f $0, $0 ;<----para esta

Note que o IMP ficou preso em um "loop", ele vai e volta e a cada JMP, o campo-B do MOV é incrementado fazendo-o copiar um DAT cada vez uma instrução a frente... depois de mais cinco ciclos, vejá como fica:

MOV.i $3, $10 ;processo do IMP(campo-B incrementado)

JMP.b $-1, >-1

JMP.b $0, <-3 ;processo do vampiro-Imp

DAT.f $0, $0 ;<----copia esta instrução

... ;

... ;

... ;

... ;nesse intervalo já foram

... ;escritos DATs

... ;

... ;

DAT.f $0, $0 ;<----para esta

Vejá o que o IMP está fazendo! ele está bombardeando o Core com DATs, na esperança de matar oponentes... e desta vez não ha intervalos - como o Dwarf - essa tecnica é chamada de "Core clear".

E quando todo o Core for preenchido com DATs o campo-B do MOV apontar para si mesmo?? bem.. o MOV se sobreescrevera com um DAT, mas e dai?? o Imp morrera, mas o seu processo está bem seguro no JMP.b $0, $0 lembra?, então depois que o IMP limpar todo core ele acabará por se suicidar... afinal, ele deve morrer alguma hora, senão seria um empate.

Indo mais a fundo no padrão ICWS '94

O # é mais do que parece..

O comportamento definido pelo endereçamento imediato (#) no padrão de '94 é bastante inusual. enquanto o padrão é 100% compatível com a velha sintaxe, o modo imediato de endereçamento tem uma característica peculiar que que pode ser usada com bastante lógica com todas os códigos operacionais e modificadores, e faz dele uma ferramenta poderosa.

olhando para o modificador e os endereçamentos, imagine o que MOV.f #7, 10 fará. .f

deverá copiar ambos os campos A e B, Mas há somente um número na fonte??(já que # significa endereço imediato 0) Ele irá mover 7 para ambos os campos da instrução 10?

Não! definitivamente não, ele irá mover 7 para o campo-A e 10 para o campo-B da isntrução 10. Porque??

A razão para isso é que na sintaxe de '94, a fonte (e o destino) é sempre uma instrução inteira. No caso do endereçamento imediato(#) se refere sempre a instrução corrente, (ex) Não importando o valor da fonte(ou destino), se ele tem um endereçamento imediato(#), a fonte ou destino é a própria instrução. Então MOV.f #7, 10 copia ambos os campos A e B do destino(0) para o destino (10). Surpreendente?

Por exemplo JMP.b #456 irá pular para a instrução 0, já que o endereçamento imediato sempre se refere a instrução atual não importando o seu valor.

Isso oferece grandes vantagens, não só você poderá guardar informação no campo-A de "graça", mas seu código sobreviverá mesmo se alguem alterar o valor do campo-A, já que ele não faz diferença.

Agora podemos reescrever o IMP-MAKER para se tornar mais robusto:

SPL.b #0, }1

MOV.i #1234, $1

Ele funciona do mesmo modo, já os campos-A apontam para o endereço 0, funcionando como $0. Apenas por diversão coloquei um decressor no campo-B do SPL, deste modo a cada execução ele irá decrementar o campo-A do IMP, assim cada IMP ficará com um número diferente.

Esse código do IMP pode faze-lo muito mais resistente à ataques inimigos, o velho IMP que utilizava o endereçamento direto($), se sofresse um ataque no campo-A estaria copiando outro endereço e não o atual para a instrução a frente, isso poderia ser fatal, mas com o endereçamento imediato, não importa o valor que estejá em A, já que ele sempre se refere ao endereço relativo(0):

MOV.i #0, $1

Se o IMP sofrer um ataque e ficar assim:

MOV.i #1, $1

Ele funcionará do mesmo modo, mas se o velho IMP(MOV.i 0, 1) sofrer o mesmo ataque e ficar assim:

MOV.i $1, $1 ;processo

Ele irá copiar a instrução a frente(1) para o endereço(1), irá copiar um DAT para o mesmo lugar, seria fatal:

MOV.i $1, $1 ;o IMP copiou o DAT sobre ele proprio

DAT.f $0, $0 ;agora o processo esta aqui... IMP eliminado.

Vejá que neste caso, o endereçamento imediato(#) torna o IMP muito mais resistente.

módulo algébrico

Você já deve saber que a memória - vamos chamar de CORE - é circunavegável, e que um endereço relativo n sempre se refere n instruções adiante, e -n sempre se refere a n instruções atrás, 0 se refere a instrução corrente.

Mas esse conceito vai muito mais além, todos os número em CoreWar também funcionam deste modo, eles estão na faixa de valores que compreende de 0 a (tamanho do Core -1).

Não existem valores negativos! quando você escreve -7 o compilador entende como (tamanho do Core -7).. se o Core mede 8000 então -7 = 7993; -1 = 7999, e assim por diante. Podemos dizer que os números em CoreWars pertencem ao grupo dos números naturais de 0 a (tamanho do Core-1).

Quando você escreve um valor maior que o limite, digamos 8122, o compilador normaliza para 122, todos os números devem estar nesta faixa de valores. Isso não faz diferença já que o Core é circunavegável, então quando você chega na instrução 7999 e adiciona +1, supresa! agora você deu uma volta completa e está na instrução 0. E também não faz diferença para operações como ADD ou SUB, sendo que com um Core de tamanho=8000, 6+7998 da o mesmo resultado que é 4 (ou 8004) como na operação 6 - 2.

Então qual é o problema?? Acontece que para algumas instruções, isso faz diferença, é o caso do DIV, MOD e SLT que tratam os números como naturais maiores que 0 e menores que (tam. do Core-1).

isso significa que -2/2 naõ é -1, mas (tam. do Core-2)/2 = (tam. do core/2)-1. (Para tam. do Core=8000, 7998/2=3999, e não 7999) Similarmente, SLT considera -2 (ou 7998) se maior do que 0! Na realidade, 0 é o menor número possível no CoreWar, deste modo, todos os outros números são considerados maiores que ele.

O padrão '94 instrução por instrução

Se você chegou até aqui, a sua paciência merece ser recompensada! Aqui descreverei os códigos operacionais um por um detalhadamente.

É claro que eu poderia te-los listados no começo, quando eu os apresentei numa lista resumidamente, e provavelmente teria salvo você de muita adivinhação. Mas eu tinha -

pelo menos na minha opinião - uma boa razão para esperar. Não somente eu queria lhe mostrar alguma coisa prática antes da chatice teórica, Mas também eu queria passar alguns conceitos básicos sobre modificadores e modos de endereçamento. Se eu descrevesse todas as instruções antes dos modificares, Eu teria antes que lhe explicar as velhas regras do padrão de '88 E depóis ensina-lo novamente com os modificadores incluídos. Não é um modo ruim de aprender RedCode, mas tornaria tudo mais complicado desnecessariamente.

DAT

Originalmente, como o próprio nome diz, DAT foi criado para armazenar dados(data) como nas linguagens mais comuns. O principal sobre DAT é que executado ele remove o processo da lista e processos(mata o processo), DAT também é usado para armazenar ponteiros(utilizados por endereçamentos indiretos) e dados, se bem que com o novo padrão de '94, o P-Space tomou lugar do DAT em quesito de armazenamento de dados(veremos adiante). Na velha sintaxe de '88 o DAT só admitia o seu campo-B, no novo padrão ambos campos são admitidos, os modificadores não fazem nenhum efeito em DAT, mas lembre-se que executado se existirem endereçamentos Predec/Posincressores, eles irão Dec/Incrementar seus ponteiros antes do processo ser eliminado!

Lembrando(você já deve ter adivinhado): todo o Core é preenchido por DAT.f $0, $0 inicialmente, portanto.. é um ambiente bem hostil não é?

MOV

MOV copia informação do endereço de sua fonte(campo-A) para o endereço de seu destino(campo- B), sobreescrevendo este. Essa informação depende do modificador, pode seu um valor do campo-A ou B, ambos os campo A e B ou até a instrução inteira(.I). O MOV é um dos poucos códigos operacionais que suporta o modificador .I.

ADD

ADD adiciona o valor de sua fonte(campo-A) para seu destino(campo-B), escrevendo o valor da soma no destino, ADD suporta todos modificadores exceto .I mas este se comporta como .F. (O que seria MOV.AB + DJN.F ????) Lembre-se que toda matemática em CoreWars está limitada ao módulo algébrico já descrito.

SUB

Esta operação funciona exatamente como ADD, exceto pela óbvia diferença, ao invés de somar este subtrai.

MUL

Multiplicação, multiplica A por B deixando o resultado em B

DIV

DIV funciona quase como MUL, porém ele divide A por B. Mas existem algumas coisas para manter em mente: primeiro, lembre-se do módulo algébrico! algumas divisões pode dar resultados inesperados. Segundo: Divisão por zero mata o processo, igual ao DAT, e deixa não muda o destino(campo-B). Se você usar DIV.f or DIV.x para realizar duas divisões ao mesmo tempo, se uma das divisões é por zero, o processo é eliminado, mas a outra divisão ocorre normalmente.

MOD

Tudo o que foi dito sobre DIV se aplica aqui também, inclusive divisão por 0, o MOD divide A por B e coloca o resto em B. Lembre-se que o resultado do cálculo(por exemplo) MOD.ab #10, #-1 Depende do tamanho do Core. para o mais comum de 8000-instruções, o resultado vai ser 9. (7999 mod 10)

JMP

JMP move o processo para o endereço que seu campo-A aponta, ele ignora seu campo-B bem como seu modificador. Porém você pode colocar um Predec/Posincressor no campo-B.

JMZ

Esta instrução funciona exatamente como o JMP, mas ao invés de ignorar seu campo-B, ele o testa e só move o processo se o valor apontado pelo campo-B for igual a zero, se não ele irá continuar a execução normalmente para o próximo endereço. Devido a essa peculiaridade, o JMZ é sensivel a alguns restritos modificadores: .ab é o mesmo que .b, .ba é o mesmo que .a, e .x e .i o mesmo que .f. Se você testar dois valores com JMZ.f, ele irá mover o processo somente se ambos forem iguais a zero.

JMN

JMN funciona como JMZ, mas move o processo se o valor testado é diferente de zero. (surpresa,

surpresa..) JMN.f pula se ambos valores não são zero.

DJN

DJN funciona como JMN, mas o valor(es) é decrescido de um antes de ser testado. essa instrução é muito útil para criar contadores de laço, mas também é útil para danificar o código de seu oponente.

SPL

Este é o grande BAM-BAM, talvez o mais significante mudança no CoreWar antes do novo padrão de '94. Ele cria um novo processo no endereço do seu campo-A, e continua o processo original normalmente na próxima instrução. Detalhe importante: Após a execução do SPL, o processo original é executado antes do que o novo processo criado. Assim como o JMP, o SPL ignora seu campo-B e qualquér modificador. Quando o limite de processos é atingido o SPL funciona como o NOP, não faz nada(exceto pelos Predec/Posincressores que são executados sempre).

SEQ

SEQ compara seus campos A e B e pula a próxima instrução se aqueles forem iguais(ele sempre pula 2 instruções a frente, já que não há espaço para um endereço como o JMP). Já que as instruções são apenas comparadas logicamente, o modificador .i é suportado. Naturalmente usando .f, .x ou .i, o SEQ irá pular a próxima instrução somente se todos os campos forem iguais.

SNE

Funciona como o SEQ, mas pula somente se os campos não forem iguais

CMP

CMP é um apelido para SEQ. Esse era o único nome antes de SEQ e SNE serem introduzidos.

Hoje em dia não faz difereça que nome você usa, já que os MARS mais populares reconhecem SEQ mesmo no modo '88.

SLT

Como a instrução anterior, o SLT pula a próxima instrução se o campo-A é menor que o campo-B. Como a comparação é aritimética, não faz sentido usar o modificador .i. Você deve imaginar se não existe um código operacional SGT(skip if greater than) mas na maioria dos casos pode-se ter o mesmo efeito trocando os campos de SLT. lembre-se que todos valores são números naturais, então 0 é o menor valor possível e -1 é o maior.

NOP

Essa instrução não faz nada(e faz muito bem), quase nenhum guerreiro usa esta instrução. Lembre-se que qualquer Predec/Posincressores são sempre avaliados.

Compilação

Nesta sessão apresento alguns padrões de codificação do RedCode e mais algumas dicas para facilitar sua vida.

Comentários

Você já deve ter notado que utilizo o caracter ";" para fazer comentarios nas linhas de código, exatamente. qualquer coisa depois de ";" é ignorada pelo compilador.

Existem três tipos de comentários(que reconheço em meu simulador), que apesar de ignorados como códigos validos, são utilizados para identificar seu guerreiro, são os seguintes:

;Name <nome do guerreiro>

;Author <autor>

;Strat <resumo da estratégia>

Exemplo:

;Name Dwarf

;Author A. K. Dewdney

;Strat bombardeia o core com DATs.

Nada de muito importante(nem obrigatorio), apenas que os MARS utilizam essas informações para identificar seu programa.

Coloquei os indentificadores em inglês(Name, Author e Strat) apenas por uma questão de compatibilidade com arquivos já existentes de outras versões de MARS americanas.

Mais um lembrete sobre o pseudo-comentário ";Strat", forma reduzida de "Strategy", caso você precise de mais linha para escrever o seu resumo de estratégia não tem problema, apenas utilize o ";Strat" no inicio de cada linha, por exemplo:

;Strat Bombardeia o core com DATs

;Strat a cada 4 instruções, evitando

;Strat auto-destruição.

As linhas acima são válidas e podem seu utilizadas perfeitamente já que as linhas são mostradas no simulador do modo como foram escritas, ignorando é claro, o ";Strat".

O mesmo truque não vale para ";Name" e ";Author", só será válido a última aparição da pseudo-intrução, por exemplo:

;Name Este nome é ignorado

;Name Este nome também

;Name O nome seguinte será válido

;Name Dwarf

Os três primeiros ";Name" não serão utilizados pelo compilador e serão tratados como comentários comuns, só será oficial o último nome, no caso, "Dwarf". O mesmo vale para ";Author".

Pseudo-instruções ORG e END

Nem sempre quando você escreve um programa você quer que seu processo inicie na primeira linha, alguns utilizam a técnica do JMP, que consiste em iniciar o programa e pular para a linha desejáda, apesar desta técnica funcionar 100% ela causa pequenos inconveniêntes: primeiro, seu guerreiro terá uma linha de código a mais; segundo, seu guerreiro será obrigado a "gastar" um ciclo antes de começar a executar seu código de verdade.

Seria mais cômodo se você pudesse simplesmente começar de fato em um ponto de seu código logo no primeiro ciclo, sem perda de tempo e sem uma linha de código adicional. Por isso existe o ORG(ou END), após ORG(ou END) escreva o número da linha que você desejá que inicie o processo. exemplo:

ORG 2

o mesmo que:

END 2

Existem as duas formas(ORG e END) devido a compatibilidade de versões, quase todos simuladores reconhecem as duas.

Chamo de pseudo-instrução porque o ORG(ou END) não é compilado como uma instrução verdadeira, entretando tem um papel importante na funcionalidade de seu programa. Essa pseudo-instrução não é obrigatória e pode ser ignorada, caso você deseje iniciar logo na primeira linha de código de seu programa, caso não sejá explicito o valor de ORG(ou END) é igual a 0, portanto para iniciar na primeira linha do seu programa não é "ORG 1" e sim "ORG 0"(é claro que você não precisa escrever isso), para iniciar na segunda linha "ORG 1"(ou END 1), na terceira "ORG 2" e assim por diante...

Podemos montar uma sintaxe para o ORG(ou END) com a seguinte construição:

ORG(ou END) <linha - 1>

Pontuação

No final de cada campeonato, ou mesmo de um round, é exibida uma tabela(em ambos simuladores) com os resultados finais, essa tabela contém o nome dos guerreiros - Vitórias, Derrotas, Empates e Pontuação.

Nenhum desses itens necessita de explicação, exceto "Pontuação" que é um valor calculado com base no seguinte:

Antes do primeiro Round, a pontuação de um programa é 0, depois de cada round, verifica-se se o programa saiu Vitorioso ou pelo menos Empatou, nessa cndição a pontuação é somada ao seguinte cálculo:

Pontuação = Pontuação + (W * W -1)/S

Aonde:

W = número de guerreiros carregados.

S = numero de sobreviventes ao final.

Deste modo quanto menos programas terminarem a partida juntos, melhor recompensado em pontos serão os que sobrarem... se num final de partida sobrarem todos os programas, então cada um receberá apenas um ponto adicional, por exemplo, numa partida com 7 programas:

Pontuação = Pontuação + (7 * 7 -1 / 7)

Pontuação = Pontuação + 6.85

Agora se apenas um saiu vitorioso, esse recebera 48 pontos sozinho, pois:

Pontuação = Pontuação + (7 * 7 -1 / 1)

Pontuação = Pontuação + 48

A cada partida os pontos são somados e no final o valor é mostrado na tabela junto com o restante dos dados.

Labels

Quando seu guerreiro começa a ficar realmente grande, é comum a confusão com os números das instruções apontando umas para as outras, algumas vezes por um erro de contagem tudo pode sair errado.

Para resolver este inconvêniente existem os labels, observe o código do Dwarf novamente(eu sei que você já esta cansado de ve-lo):

ORG 1

ADD.ab #4, 3

MOV.i 2, @2

JMP.b -2, 0

DAT.f #0, #0

Agora observe o mesmíssimo Dwarf utilizando Labels:

ORG inicio

adder ADD.ab #4, bomba

inicio MOV.i bomba, @bomba

JMP.b -adder, 0

bomba DAT.f #0, #0

Veja que os número que foram substituídos por nomes se tornaram mais claros porque logo na primeira vista entendemos seu significado. Os labels são obviamente substituídos pelos números corretamente na compilação, eles são calculados como endereços relativos entre a referência ao label e a instrução labeada.

Download

MemoryArmagedom

(Memory Array RedCode Simulator, para Windows 32 , documentação e recode-warriors exemplos)

Borland RunTime Libraries

(caso o executável não rode, peça por algum arquivo, descompacte-os no mesmo diretório ou na pasta SYSTEM do Windows)

Links

Documentos

Programas

Campeonatos

<<<Voltar

Copyright 2006 - Rodrigo Setti