Olá, a todos, tudo bom?
Bom, anos atrás eu publiquei um tópico aqui onde eu mostrava informação surgindo onde não existia através de algorítmos genéticos (um Hello World Genético, por assim dizer).
Hoje eu quero partir para algo mais avançado, eu vou fazer algo "divino": Vou criar vida, ela vai se multiplicar, vai evoluir e vai morrer.
Magia? Não! Tecnologia!
Vou fazer isso através de um emulador de Seleção Natural + mutações rodando programas virtuais.
Essa técnica foi inventada por um biólogo chamado Tom Ray, mais detalhes nesses links:
http://infidels.org/library/modern/meta/getalife/coretierra.htmlhttp://life.ou.edu/tierra/http://www.talkorigins.org/faqs/tierra.htmlE aprimorada com o Avida:
http://en.wikipedia.org/wiki/Avidahttp://myxo.css.msu.edu/papers/nature2003/Nature03_Complex.pdfEu implementei uma visão própria dos dois modelos, com diferenças: adicionei mais tipos de instruções, inclusive instruções com parâmetros, sendo algo bem mais próximo de uma arquitetura real de computador.
No modelo do Tierra a memória é compartilhada, há um pool único de memória onde os programas residem e são executados (cada um só pode gravar no seu pedaço de memória mas pode executar qualquer pedaço de memória), uma representação visual é essa:

Cada organismo é uma sequência de bytes na memória, dessa forma é fácil ver o que está sendo escrito mas é difícil distinguir um programa do outro.
Já no Modelo do Avida a memória é unica para cada organismo (com exceções)
Cada organismo ocupa então um quadrado em uma grid 2d:

Apesar do modelo do Tierra ser interessante, o modelo do Avida tem várias vantagens, como mais memória para cada organismo (até 4k atualmente), facilitar acompanhar os organismos surgindo e sumindo (filhos nascem perto do pai) e, principalmente, há a disputa por "território": uma nova célula só pode nascer caso haja um espaço vazio próximo do pai (o que acaba gerando alguns desenhos bem orgânicos já que os programas que ficam presos acabam dando erro ao tentar replicar e morrem). O que também abre espaço para melhorias evolutivas, como verificar se há espaço vazio e só realizar divisão quando tiver.
Alguns detalhes sobre o funcionamento:
* Eu inicializo com um programa ancestral, feito manualmente, capaz de realizar reprodução e mais nada.
* Não estou simulando a reprodução: cada programa tem que ir lá, alocar memória e depois copiar byte a byte de instrução para o seu filho e depois dividí-lo, separando-o (o pai perde acesso de escrita nesse momento).
* Cada cópia de instrução tem uma chance de mutar.
* A cada divisão bem-sucedida há uma chance do organismo criador se "deteriorar", mutando uma instrução aleatória, isso tanto pode melhorar o organismo como pode matá-lo.
* Erros podem ser fatais, (por exemplo, tentar alocar memória para um filho já tendo feito isso), sendo então punidos com a morte do individuo (outras punições menos fatais podem acontecer também)
* Indivíduos são recompensados ao executarem certas ações (como reprodução).
* Apesar de visualmente parecer com um autômato celular (
http://en.wikipedia.org/wiki/Cellular_automaton), não é esse o caso, pelo menos não de forma tão simples: o conjunto de instruções é "Turing Complete".
O programa ancestral é esse abaixo, com 111 bytes:
#jAvida Size: 111
0=NOP_1
1=NOP_1
2=NOP_1
3=NOP_1
4=ZERO C
5=OR C
6=SHL C
7=SHL C
8=ADRB A
9=NOP_0
10=NOP_0
11=NOP_0
12=NOP_0
13=SUB A,C,A
14=MOV A,B
15=ADRF A
16=NOP_0
17=NOP_0
18=NOP_0
19=NOP_1
20=INC A
21=SUB A,B,C
22=NOP_1
23=NOP_1
24=NOP_0
25=NOP_1
26=MAL C,A
27=CALL
28=NOP_0
29=NOP_0
30=NOP_1
31=NOP_1
32=DIVIDE
33=JMP
34=NOP_0
35=NOP_0
36=NOP_1
37=NOP_0
38=IFZ C
39=NOP_1
40=NOP_1
41=NOP_0
42=NOP_0
43=PUSH A
44=PUSH B
45=PUSH C
46=NOP_1
47=NOP_0
48=NOP_1
49=NOP_0
50=MOVI B,A
51=DEC C
52=IFZ C
53=JMP
54=NOP_0
55=NOP_1
56=NOP_0
57=NOP_0
58=INC A
59=INC B
60=JMPB
61=NOP_0
62=NOP_1
63=NOP_0
64=NOP_1
65=IFZ C
66=NOP_1
67=NOP_0
68=NOP_1
69=NOP_1
70=POP C
71=POP B
72=POP A
73=RET
74=NOP_1
75=NOP_1
76=NOP_1
77=NOP_0
78=IFZ C
Não vou entrar em detalhes, a ideia simples é: ele se copia, byte a byte, sem parar.
Após algumas horas rodando a situação é essa:

Caso não fique claro, os quadrados pretos são vazios, cinza-escuro são programas alocados (filhos não-ativos) e os coloridos são os programas ativos.
Nesse ponto notei que havia vários programas pequenos, como esse abaixo:
#jAvida Id:4823806 Gen:671 Hash:089_2FD31 Age:0 Size: 102
#REGS: = AX:4199[09] BX:103[03] CX:0[00] DX:13[03] EX:0[00] FX:22[02] GX:1[01] HX:0[00] IX:0[00] JX:1[01] SP:13[03] IP:13[03] E:-1 ID: : NOP_1
14: NOP_1
15: NOP_1
16: NOP_1
17: LOADM C,33 // C <- (33)
20: SCAN I,E // I<- checkNeighbour at Index E: 1=found, 0=not found
23: SHL C // C <- shiftLeft(C)
25: ADRB A // A <- templatePosition
27: NOP_0
28: NOP_0
29: CHG_ST 0 // changeStack 0
31: SUB A,C,A // A <- A-C
35: MOV A,B // B <- A
38: ADRF A // A <- templatePosition
40: NOP_0
41: SHL A // A <- shiftLeft(A)
43: ZERO G // G <- 0
45: NOP
46: SUB A,B,C // C <- A-B
50: NOP_1
51: NOP_1
52: SHR D // D <- shiftRight(D)
54: MAL C,A // Allocate C Bytes; store position in A
57: CALL // push(ip) ; ip <- templatePosition
58: NOP_0
59: NOP_0
60: NOP_1
61: NOP_1
62: DIVIDE // divide and start new program
63: JMP // ip <- template position
64: NOP_0
65: NOP_0
66: NOP_0
67: ZERO I // I <- 0
69: CALL // push(ip) ; ip <- templatePosition
70: NOP_1
71: NOP_1
72: NOP_0
73: NOP_0
74: OR H // H <- or(H)
76: MOVM 12,A // (A)<- (12)
79: SCAN I,D // I<- checkNeighbour at Index D: 1=found, 0=not found
82: NOP_0
83: NOP_1
84: MOVI B,A // (A) <- (B)
87: DEC C // C <- C - 1
89: IFZ C // if C == 0 then:
91: RET // ip <- pop()
92: ADD A,G,G // G <- A+G
96: INC A // A <- A + 1
98: INC B // B <- B + 1
100: JMP // ip <- template position
101: NOP_0
Que é um código menor (9 bytes a menos) e com maior tolerância à erros do que o programa original, isso fica muito evidente, como mostrado aqui, em uma "competição":

À esquerda é o programa novo e à direita o programa original, após pouco tempo nota-se facilmente que o novo programa tem uma taxa de reprodução/sobrevivência muito melhor, com poucos espaços vazios e aproveitando espaço em volta.
Eu imaginei que as últimas 4 linhas fossem opcionais, lixo, já que o "RET" iria retornar o ponteiro para o CALL anterior, experimentei remover as linhas para deixar um código menor.. não funcionou.
O que torna o programa "irredutivelmente complexo", a questão é: eu não coloquei esse código ali, surgiu "naturalmente" através de seleção natural + mutações, em um tempo finito.
Bom, no mais é isso, queria compartilhar esse experimento meu com vocês.
Caso tenha alguém que queira trocar ideias sobre o processo envolvido, estou à disposição.
Vou continuar a fazer mais experimentos, quero verificar a inter-conectividade entre as células para ver se consigo replicar comportamentos de parasitas e/ou troca de informações.
abs,
Cristiano