Each animal in EvolveJs is written in a specialised assembly code. This assembly language was designed with some inspiration from Tierra. Each thread is essentially a stack machine and each operation operates on that stack.
An assembly operation is a pair of [operation, operand].
One of the key concepts is in markers used for jumping. To make the animal’s code more robust to modification, all jump targets (such as for loops and branches) are marked as a series of nops, and the jump command specifies the jump target as the number of nops to search for.
So as a concrete example I will show a simple process that can reproduce itself.
This is the 5 label. The nop function does not require an argument, so is by convention set to 0.
["nop",0],["nop",0],["nop",0],["nop",0],["nop",0], //label 5
Now we initialise the read pointer to point to the start of the parent, and the write pointer to point to the end of the parent. The jmp*PtrB functions jump the pointer to the start of the nop template, and the jmp*PtrF functions jump to the end of the nop template. In this code all templates are unique, but it is not necessarily so. The jmp*PtrF functions search forward and wrap around, and the jmp*PtrB functions search backward and wrap around.
["jmpReadPtrB",5], //jmp to start of template 5
["jmpWritePtrF",2], //jmp to end of template 2
Get the parents memory size, allocate memory at the end of the parent, and move the write pointer to the start of the new memory.
["pushMemSize",0],
["alloc",0],
["incWritePtr",0], //inc write pointer to start of new animal
The copy loop. Copy from the read pointer to the write pointer, and increment both pointers.
["nop",0], //start copy loop
["copy",0],
["incReadPtr",0],
["incWritePtr",0],
Push the value for the write pointer and the animals total memory size (including the new child memory) to the stack and compare them using the lt operator.
["pushWritePtr",0],
["pushMemSize",0], //if writeptr < memSize
["lt",0],
Then, if write pointer is less than memory size, jump back to start of template 1.
If write pointer is not less than memory size, then the execution pointer gets incremented by 1 + operand (in this case 1), which means that the copying process has finished.
["ifdo",1],
["jmpB",1], //if not finished copying
This is the division loop. When a parent divides, the parent attempts to put the child in the square immediately in front of them. If this position is already occupied, then division fails, and the parent gets to try again.
["nop",0],["nop",0],["nop",0],["nop",0],["nop",0],["nop",0], //template 6
Divide attempts division and pushes a boolean result onto the stack indicating success. If division succeeds, then jump all the way back to template 5, and start making a new baby.
["divide",0],
["ifdo",1],
["jmpB",5], //if division successful
Here, we are using one of those nop templates to store data.
The memory pointer jumps to the first memory address in the 6template. The data that is stored in the operand is pushed onto the stack, incremented, stored back in the memory slot and compared to 4 (number of cardinal directions)
["jmpMemPtrB",6],// jump the mem pointer to a location to store some data
["pushM",0],
["add",1],
["popM",0],
["pushM",0],
["push",4],
["gte",0],
If it has already tried this 4 times, then this animal is surrounded and can't reproduce at the moment. So it sleeps, and we reset the counter. Otherwise, the ifdo,3 increments the execution pointer by an extra 3.
["ifdo",3],
["sleep",450],
["push",0],
["popM",0],
The divide didn't work, try again facing in a new direction.
["turnR",0], //turn around and try again
["jmpB",6],
The end tag
["nop",0],["nop",0]