Purpose:
Ecology sims are software toys enjoyed by hobbyists and others. The idea is to simulate biological evolution of organisms that are independent agents. Besides being fun to play with, they are also good technical exercises for programmers for a few reasons. First is that it has to be fast: evolution takes a long time, and the simulation is more interesting the bigger it is, so the faster and more memory efficient, the better. The second is the issue of representing code as data and coming up with a way for an AI to become generated on the fly.
Research:
There are a lot of ecology sims out there. Many evolve constants or simple inputs for a complex hard-coded AI or a neural network. Others actually create a virtual machine and instructions (or a sort of domain-specific programming language) for the genetic algorithm to generate. My implementation is the later. My biggest inspiration is probably Evolve 4.0.
The language:
I designed a language based on lambda expressions and continuations.
Lambda calculus
The lambda calculus is a model of computation often contrasted with Turing Machines. Specifically, the untyped lambda calculus is a language for describing functions that take a single function and return a single function. Any computation can be expressed this way — both data structures and algorithms can be built from this simple idea. I choose it for two reasons. First, I wanted the AIs to be able to utilize dynamically allocated memory without having to manually specify registers, deal with a stack, or manage anything. I also wanted it to be able to create complex behaviors, create relationships between concepts, and avoid having to repeat itself in the code. Lambda accomplishes all of these elegantly.
Lambda requires some kind of automatic memory manager. Since the language I’m writing is purely functional (there are no side effects, no way to change pointers, and the continuations are undelimited) the language has no possibility of cycles, so I could use a very efficient reference-counting system.
At first I thought that lambda didn’t lend itself well to fuzzy logic. I wanted the creatures to be able to act organically and unpredictably, and this seemed to be a problem with lambda because it’s a deterministic computational model. Then I had a wacky idea… lambda calculus is based on binary application (which I was representing in my written language as a prefix backquote `) so there’s lots of `<expression><expression>. I just made it so that every application expression has a chance to reverse its two arguments. In C syntax this means that f(g) could sometimes be g(f), depending on the “fuzziness,” which is decided by a gene. (Everything is a function, so this doesn’t cause problems.)
Continuations
I used continuations in the interpretter because it was a natural way to write it. It’s very similar to the actionlist design pattern, which I was familiar with from using it extensively in Nightfall the previous semester. It’s a convenient way to make the interpreter re-entrant. The “eval” function of this language takes and returns a continuation, and has to be called repeatedly like an update function. Each call does one continuation, and each continuation can be thought of as a virtual machine instruction plus relevant data. This is convenient for performance because it allows me to easily avoid “hiccups” (at least when it comes to updating the AIs) since the smallest, most atomic operation is very fast and fairly consistent.
This doesn’t solve the performance problem, but it at least basically guarantees a smooth presentation.
Since I was implementing the interpreter in continuation-passing-style, it was easy to expose this to the code with call-with-current-continuation. This makes it so that doing things like looping is very easy and doesn’t require complex anonymous recursion. More complex control structures are also possible.
Design:
a stable population
In order to get something that looks like biological evolution, I had to get them to play a somewhat carefully designed “game.” I had to prevent “exploits” because the algorithm would always find them. For instance, I had to make them lose more and more energy as they age, or else they’d eat forever and never reproduce. I also had to make reproduction cost as least as much energy as the newborn starts with, or else they’d reproduce and never eat (and fill the screen of course.)
What could it do:
Actuators
They can move, lay an egg, or fertilize. Move is a function that returns a 3×3 structure of functions that actually do the moving. All the actuators have side effects on the world, but they also return something. They return themselves if they succeeded, and their argument if they failed. This makes it easy to “do X until fail” or something like that.
Sensors
There are two “internal sensors” that check either age or energy level. The threshold that they check by is set at birth by a gene. They act as a function that takes (in a curried fashion) 3 arguments and returns one of those arguments depending on if the current value is greater than, less than, or equal to the value set by the gene. In this way they can be used to “remember” what the value was if you simply don’t provide it all the arguments.
The first sensor I added was a function that would path to the nearest food. This seemed like a cheat but I wanted to give them a really useful tool because I was having trouble getting them to bootstrap. It returns a “vector” function that when supplied to the move function above, returns a move actuator in the appropriate direction.
Later, I made a less cheaty sensor for the world. Called “sense my tile,” it returns a “tile structure” that includes information about what is on the tile (wall, creature, egg, food), functions for returning adjacent tiles (with the same interface as the move function) and functions for “subtracting” with another tile to produce a “vector” between them. So it describes a graph structure with edges, nodes, and some helper functions.
Bootstrapping:
a recently stable population
It’s easy to get a stable population going if there’s lots of food and no walls. (The key was to remove the actuator that eats and just have them auto-eat all the time.) The creatures actually reproduce sexually, which is a bit unexpected. They live by traveling mostly diagonally (which with a wrapped grid makes it so they can see every tile) and lay and fertilize on the move. They move in short little bursts. The population fluctuates as it eats all the food, starves, and rebuilds itself after the slight population bottleneck. When left over night, they eventually will seek out food using the built-in “find nearest food” sensor and you can see them “fighting over” the food when it is scarce.
The addition of detailed tile info sensors was not that useful to them. Most information they could achieve just by moving to the square. They can’t tell the difference between a wall, creature, and egg though. They just bump into “something.” To see if they’d use it more, I tried making walls hurt them if they bump into them. The result was no stable population after leaving it over night. I also tried doing a regular no-walls map with no find-nearest-food sensor. The result looked similar, so I added some walls. And then more walls. Then I did an experiment where I put some food in a little nook and saw that nothing was eating it. A few minutes later it was gone, I put it back, and it was eaten immediately. To me, this means they’ve figured out something kind of path finding (or perhaps that they’re marking locations where they’ve found food like a bee does? No idea!) using the more complicated but complete interface. There were many other interesting patterns to their movement that I won’t go into. It was pleasing, to say the least, and a blast (for me anyway) to play with.
Conclusions: It’s fun to play with! With some more interactivity, it could be a decent toy, but even without it, it could make a really nifty screen-saver. What application could this have outside of little fishtank toys? Well, the thing this does that other AI techniques don’t is adapt. It wouldn’t be good for coming up with solutions to single-player problems with well understood parameters, like a slide puzzle. Such things are better done with other techniques. Similarly, 2 player games are better done with techniques like alpha-beta pruning etc. But games with hundreds of players are another issue. These game also often have systems that have rules that AREN’T well understood or search spaces that are complex to the point of not being easily represented, much less traversed. An example is an ecology of hundreds of other AIs: no heuristic could make that search space manageable to deal with without some pretty magical hardware. Another example might be Magic: The Gathering deck-building (as opposed to playing.) The way humans find the best decks is not through a conscious search algorithm, usually. It’s through experimentation and trial and error. The Magic ‘metagame’ is a search space that is too huge (considering how many agents are involved) to calculate the relevant decks without failing and learning. For gameplay, there’s only one opponent, and your interactions are pretty straightforward. But the metagame is more like an ecology. So if you wanted to find the best magic decks with a computer program, you’d probably want to simulate that ecology.
Technical conclusions:
Part of what I wanted from this project was to get a better understanding of functional programming. To a programming brought up on C, doing things without side-effects seems kind of impossible. But it’s not, and it’s pretty cool. I wouldn’t write software in this language, for a variety of reasons, but it helped me see some of the advantages of the functional style. When state is tracked with this much care, It’s easy to do things like bring the program back to an earlier state or some aspect of it, or to have trivial “undo” functionality. I was already a big fan of lexical closures, but it was neat and a good exercise to see how complex data structures could be built from simple single-argument lambdas. In this implementation, there were side effects, but they were all visible and on the world. If that part were also functional… well, it would be horribly inefficient so never mind!
Download:
http://dl.dropbox.com/u/32018938/ecosim.zip