I spent two weeks building an evolution simulator where my creatures learned to "walk" toward food. I ran millions of generations across 597 genetic lineages to create the optimal species. This is a journal of that process: implementing the papers, fixing bugs, counterintuitive walls I ran into, and working with Claude Code.
There was a video I saw 9 years ago on YouTube from Carkh from showing simple creatures evolving to pick up pellets. For the longest time I've wanted to create my own version but it was never a priority.
Until I nerd sniped myself. While writing my "I was a top 0.01% Cursor user. Here's why I switched to Claude Code 2.0" article I wanted to show off what Claude Code could do and thought this was one of the coolest things I could one shot, grab a gif, and move on.
281 commits later, with the computational application of Darwin's consecrated knowledge running through my cortical connections, I have a working evolution simulator.
I used Claude Code to help me build this. I'll be honest about where that helped and where it didn't.
All code is at github.com/SilenNaihin/genetic-algorithm.
Evolution was able to create the most complex collections of matter in the universe (ourselves).
Nature doesn't have access to backpropagation or even local learning rules as in the brain. It has to use population level rules that comply with the laws of physics.
Genetic algorithms simulate this Darwinian process: measure how well a organism does in an environment, murder them in cold blood if they aren't performing well, and the rest reproduce with a chance of mutation. Repeat.
Our creatures are made of nodes (spheres) connected by muscles (springs).
Nodes have friction and size:
Muscles have a rest length (natural length), stiffness (how hard it pulls), and damping (how quickly it settles):
The muscle pulls toward its rest length using Hooke's law with damping:
Where is stiffness, is damping, is rest length, is the current length, and is the direction between nodes.
Super simple right? Well unfortunately the constraints of reality aren't baked into a physics sim by default. To give you a taste:
This is how we calculate how well a creature performed. The fitness function defines what "good" means.
Alright,Claude please look at my codebase and make a list of the different components of the fitness function we ended up with. Make no mistakes. (it made mistakes and it would have been quicker for me to write this out):
Getting the fitness function right was 10x more difficult than getting Claude to understand the nuance of our current fitness function.
Progress banking bug
When a creature collected a pellet, their fitness would drop to 20 instead of keeping the 100 points. Progress was being reset to 0, and a pellet was just adding 20.
"Claude pls what was not clear we want to bank progress at 100 points when we collect a pellet" (0f7f946).
Progress baseline position
Progress was being calculated from where the creature spawned, not from where it was when it picked up the last pellet. "Claude pls reset the baseline position after each collection"
Center vs edge calculation
I was measuring distance from creature center to pellet center. But creatures have different sizes. A large creature could "reach" a pellet while its center was still far away. Had to calculate from the edge of the creature instead.
The edge calculation itself was tricky: I needed a stable radius from the genome (rest state), not the current physics state. Otherwise the radius oscillates with muscle animation and fitness swings wildly (3bde5ec).
Uncapped distance reward
I added the 20 points bonus for distance traveled to give the creature a gradient to maximize while it hasn't learned to move in a direction yet.
Claude decided to interpret this as making the reward absolute to "encourage more movement". Below is the result for the kind of creatures we evolved. Sad to think so many locomotive creatures were exterminated because their environment was so hostile.
For my first attempt to get the creatures to optimize towards this fitness function was to give the muscles evolvable oscillation parameters: amplitude (range of contraction), frequency (oscillation speed), and phase offset (timing in the cycle).
Instead of pulling toward a fixed rest length, muscles now pull toward an oscillating target:
Where is amplitude, is frequency, and is phase offset. The spring force now pulls toward instead of :
First we catapult the bottom 50% (roughly) of creatures out of the gene pool based on fitness.
Then survivors reproduce, either through direct cloning or crossover ;) with another survivor, always followed by mutation.
Finally, we simulate the new generation and measure their fitness. Repeat.
At this point our creatures are brainless oscillators.
Naturally, several problems emerged.
Sometimes the simulation would just explode. Creatures would fly off to infinity. I had to add checks to disqualify creatures with invalid or NaN fitness values. I say this plainly, but there were many things that were causing this. For example: (6715202).
Pellets were spawning too close to the creature. A creature could collect multiple pellets without moving much at all, just by being in the right spot when the next pellet appeared.
The fix: spawn pellets at least 5 units away from the creature's edge (not center), in the semicircle opposite to the creature's current direction of motion. This forces the creature to actually travel to collect each pellet.
Our best creatures with pure oscillation mechanics evolved to spaz out in a radius and occasionally bump into pellets. Which is pretty much all we could hope for without any ability to respond to the environment.
So let's upgrade the genotype. Time to IQ max.
Each creature gets a small feedforward network: sensory inputs → hidden layer → muscle outputs. The network outputs one value per muscle in , which directly controls muscle length: . Output of +1 means fully contracted, -1 means fully extended.
| Input Type | Input Count | What it tells the creature |
|---|---|---|
| Pellet direction | 3 | Where is the food? (unit vector, x, y, z) |
| Velocity direction | 3 | Which way am I moving? (x, y, z) |
| Distance to pellet | 1 | How far is the food? |
| Time encoding | 0-2 | What time is it in the simulation? (ex. oscillates between -1 and 1 every 2 sec) |
| Muscle strain | 0-15 | How stretched is each muscle? (x, y, z for each muscle) |
| Node velocities | 0-24 | How fast is each body part moving? (x, y, z for each node) |
| Ground contact | 0-8 | Which parts are touching the ground? (0 or 1 for each node) |
The basic version uses 7 inputs (pellet direction, velocity, distance). The full version can include proprioception (muscle strain, node velocities, ground contact) for up to 54 inputs total. Hidden layer size is configurable (8-32 neurons typical).
Now there is no base oscillation anymore. The network has full control over when and how each muscle contracts.
And the creatures failed to learn anything. Even their spazzing was ineffective.
I decided to take matters into my own hands. I asked Claude something like what is wrong with our creatures? make no mistakes or else a random child across the world will lose their favorite stuffed animal
The conversation that followed made me realize I can't delegate everything to Claude without understanding the codebase myself.
Basically, a lot had gotten lost in the details. Some examples:
After diving into the details and fixing things I saw improvement for the first time for more than 20 generations.
There are many reproduction strategies
| Crossover Type | What it does | Trade-off |
|---|---|---|
| Uniform | Each weight randomly from parent A or B | Maximum mixing, can destroy coordinated weights |
| Interpolation | Weighted average between parents | Smoother blending, less exploration |
| Single-point | All weights before point from A, after from B | Preserves local structure, less mixing |
and mutations strategies
| Mutation Type | What it does | When it helps |
|---|---|---|
| Weight perturbation | Add Gaussian noise to existing weights | Fine tuning an already good solution |
| Weight replacement | Replace weight with new random value | Escaping local optima, exploring new regions |
| Body mutation | Modify node and muscle parameters | Evolving morphology alongside behavior |
| Structural (NEAT), more on this later | Add/remove neurons and connections | Finding simpler or more complex architectures |
that I experimented with.
For weight mutations, magnitude matters a lot (9324dec). Weight perturbation adds Gaussian noise with standard deviation σ to each weight. But when you do this across many weights, the total displacement in weight space scales with the square root of dimensions:
Think of the neural network as a single point in high dimensional space, where each weight is one coordinate. A network with 200 weights is a point in . When you mutate, you move from one point to another. The "distance" is just the L2 norm between old and new weight vectors.
High-dimensional noise explodes in norm
High-dimensional noise explodes in norm
In a ~200 dimensional network: σ=0.3 gives . Since individual weights are typically magnitude ~1, moving 4.2 units means many weights changed by ~30%. You've left the local basin and the network's behavior is mostly destroyed. That's a random restart, not optimization. σ=0.05 gives . Small coordinated nudges across many weights. The network function is mostly preserved. You're still on the same fitness ridge and can hill-climb.
Our later neural architecture search confirmed this: aggressive body mutation with conservative weight mutation worked best. Focus evolution on morphology, let weights fine-tune.
I expected creatures to evolve walking gaits: rhythmic, coordinated movements like animals. They didn't. I built an activation analysis notebook to understand what was actually happening (with the help of Claude Code of course).
The dominant oscillation frequency was 0.17 Hz, much slower than typical locomotion gaits. Creatures evolved aperiodic, exploratory movements that happen to reach pellets. They didn't walk, they strategically flailed.
The best performing creatures had a mean output of -0.12, with most outputs hovering near zero (in the deadzone). The failing creatures had mean positive outputs and more chaotic activation patterns.
After a few successful runs, I noticed a pattern. Runs would improve for up to 50 generations, then plateau. Looking at the population, everyone had converged to the same strategy. The top 50% survive, they're all similar, they breed, offspring are even more similar. Eventually everyone is a minor variation of the same local optimum.
This is a known problem. I started reading about diversity maintenance: fitness sharing, tournament selection, and how the famous NEAT paper does it.
I experimented with three selection methods:
| Method | How it works | Trade-off |
|---|---|---|
| Truncation | Kill bottom 50%, clone survivors | Simple but aggressive. Fast convergence, loses diversity quickly. |
| Rank | Selection probability proportional to rank, not raw fitness | Gentler pressure. Creature at rank 2 isn't 10x more likely to survive than rank 10. |
| Tournament | Pick k random creatures, best one survives | Stochastic. Weaker creatures in weak groups can survive, preserving diversity. |
Tournament selection (d3e7a8c) adds randomness. Pick k=3 creatures at random, keep the best. A mediocre creature in a group of three bad ones survives. This lets "stepping stone" genomes persist, ones that aren't great now but might lead somewhere good.
In Theory.
After our neural architecture search, I realized that rank and tournament selection didn't help at all. Go figure.
If two creatures are similar, they split their fitness. This penalizes crowded regions of the search space. The intuition: imagine 10 creatures all clustered around the same local optimum. Without fitness sharing, they'd all survive and breed, making the population even more homogeneous. With fitness sharing, they divide the reward among themselves, so one novel creature exploring elsewhere might actually have higher effective fitness.
The formula (Goldberg & Richardson, 1987):
Each creature's fitness gets divided by a "niche count": how many similar creatures exist. The function determines how much two creatures "share" based on their distance:
The key parameter is , the sharing radius. It defines "how different is different enough." If two creatures have distance , they're considered similar and share fitness. If , they're far enough apart to not affect each other.
When (identical creatures), , meaning full sharing. As distance increases toward , sharing decreases linearly (when ). At the boundary and beyond, , no sharing.
For neural networks, I computed the RMS (root mean squared) Euclidean distance across all weight matrices. By flattening both networks' weights into vectors, computing the element-wise differences, squaring them, averaging, and then taking the square root. This gives a single number representing how different two brains are.
def neural_genome_distance(genome1, genome2) -> float: ng1 = genome1.get('neuralGenome') ng2 = genome2.get('neuralGenome') total_squared_diff = 0.0 total_weights = 0 # Compare all weight matrices for key in ['weights_ih', 'weights_ho', 'biases_h', 'biases_o']: w1 = _flatten(ng1.get(key, [])) w2 = _flatten(ng2.get(key, [])) min_len = min(len(w1), len(w2)) for i in range(min_len): diff = w1[i] - w2[i] total_squared_diff += diff * diff total_weights += 1 # Penalize size mismatch (topology difference) size_diff = abs(len(w1) - len(w2)) total_squared_diff += size_diff * 4.0 # max diff squared total_weights += size_diff # Root mean squared distance return math.sqrt(total_squared_diff / total_weights)
def neural_genome_distance(genome1, genome2) -> float: ng1 = genome1.get('neuralGenome') ng2 = genome2.get('neuralGenome') total_squared_diff = 0.0 total_weights = 0 # Compare all weight matrices for key in ['weights_ih', 'weights_ho', 'biases_h', 'biases_o']: w1 = _flatten(ng1.get(key, [])) w2 = _flatten(ng2.get(key, [])) min_len = min(len(w1), len(w2)) for i in range(min_len): diff = w1[i] - w2[i] total_squared_diff += diff * diff total_weights += 1 # Penalize size mismatch (topology difference) size_diff = abs(len(w1) - len(w2)) total_squared_diff += size_diff * 4.0 # max diff squared total_weights += size_diff # Root mean squared distance return math.sqrt(total_squared_diff / total_weights)
This didn't really help, but I didn't spend enough time debugging to find out why.
Instead I decided to implement a paper in which these things had already been solved and work. NEAT (NeuroEvolution of Augmenting Topologies).
NEAT asks 'are we limiting evolution by fixing the network structure?'
Every creature had the same architecture: 7 inputs, one hidden layer, N outputs. But some tasks may need more hidden neurons. And some connections could be useless.
Why am I still hand designing the topology of the network like a troglodyte instead of letting evolution figure it out? I should be evolution maxxing.
NEAT can mutate everything about the network topology:
| Mutation | What it does | Effect |
|---|---|---|
| Add connection | Creates a new connection between two unconnected nodes | Increases network connectivity |
| Add node | Splits an existing connection by inserting a node in the middle | Increases network depth/complexity |
| Mutate weight | Perturb (90%) or replace (10%) connection weight | Fine-tunes or escapes local optima |
| Enable connection | Re-enables a disabled connection | Can reactivate old genes |
| Disable connection | Disables an existing connection | Prunes connections without deleting them |
From these mutations, networks can start with 0 connections and hidden nodes and grow to be as complex as needed.
These mutations mean every creature can have a different network structure.
But that creates a problem: how do you do crossover between two networks with different topologies?
NEAT's solution: every time a new connection or node is added anywhere in the population, it gets a globally unique ID called an innovation number. This lets you align genes from two parents by their historical origin, not their position in the genome.
Innovation numbers solve crossover alignment. When two parents have genes with the same innovation number, those genes came from the same ancestral mutation. They're homologous.
Genes that don't match are either disjoint (in the middle) or excess (at the end). The offspring inherits matching genes from either parent randomly, plus all disjoint/excess genes from the fitter parent.
NEAT uses these same concepts (matching, disjoint, excess genes) to measure how different two genomes are.
Instead of following our neanderthal truncation rules where the bottom 50% of creatures are vaporized into context, we can use speciation to protect new structures.
This is useful for a mutation that adds a node that hurts fitness initially. With speciation, it competes only against similar genomes, giving it time to optimize.
NEAT introduces a compatibility distance that determines whether two creatures belong to the same species:
Think of δ as "genome distance". A single number measuring how different two creatures are. More mismatched genes (E, D) and bigger weight differences (W̄) means higher distance.
You pick a threshold δ_t. If two creatures have δ = 2.3 and your threshold is δ_t = 3.0, they're in the same species (2.3 < 3.0). If creature two δ = 4.1, they're different species (4.1 > 3.0).
To assign species I iterate through creatures in order and compare each creature to existing species representatives. If δ < δ_t, the creature joins that species. If no match, we start a new species with this creature as the representative.
Species are rebuilt from scratch each generation with the first creature assigned becoming the representative. This is simple, and we end up with however many species clusters naturally form in genome space.
If this still feels confusing, this video is what I watched to get a base-level understanding of NEAT.
To add more complexity, I had to solve the problem that standard NEAT assumes fixed input/output counts.
Our creatures can mutate their bodies by adding or removing muscles ie output nodes. So creatures can have different output counts.
I added a term: , where is the number of output neurons (one per muscle).
When a muscle is added, I create a new output neuron with sparse random connections. When removed, I delete that output neuron and its connections.
Why? Imagine two creatures with identical hidden layers, same connections, same weights. Standard NEAT would say δ = 0, they're twins. But one has 3 muscles and the other has 5. They're solving completely different control problems, so they should be in different species. The output count term ensures this.
In speciation, each species runs its own selection proportionally. With a 50% survival rate, a species of 10 keeps 5, a species of 50 keeps 25. There's no cap on species size. The compatibility threshold controls how many species form, and selection is proportional within each.
| Bug | What happened | Commit |
|---|---|---|
| Cycles forming | Network execution hangs or loops forever | e28f706 |
| Invalid crossover | Output neurons used as connection sources | 9b5ff50 |
| Wrong output removed | Deleting muscle removed wrong neuron | 9a28945 |
| Hidden nodes at wrong depth | Hidden neurons overlapping inputs in visualizer | c93b8b1 |
| Clones not mutating | 50% of population frozen (not evolving) | 849cb4e |
| Rates 10x too low | Using 5%/3% instead of NEAT standard 50%/20% | 43e02d3 |
Etc.
Most of these bugs came from letting Claude have it's way without providing specific enough instructions.
If you're curious for specifics, read the original paper which has more specifics. For example, an input bias node. Crazy.
So how do we perform? Empirically good.
NEAT created the most "creature like" behaviors I could get. The two above are clearly able to walk and have a solid sense of direction.
But objectively bad.
I couldn't get NEAT runs to pick up more than 2 pellets, and the average rarely crossed 10 points per creature.
Time to pull out the BIG GUNS.
At this point I had 20+ hyperparameters and no idea which ones mattered. Mutation rates, crossover rates, network topology settings, speciation thresholds were all being hand tuned by my god given intuition.
Neural Architecture Search (NAS) is supposed to automates my flawed intuition into raw confidence intervals by running hundreds of trials with different parameter combinations, seeing what actually works.
I used Optuna for Bayesian optimization (8807da4). I tested three hardware configurations:
If you're a compute nerd, this is for you
OMP_NUM_THREADS=1 (and similar thread limits) inside each worker before importing PyTorch.If you're a compute nerd, this is for you
OMP_NUM_THREADS=1 (and similar thread limits) inside each worker before importing PyTorch.| Hardware | Configuration | Result |
|---|---|---|
| M3 Max (local) | 12 cores, sequential | ~11 min/trial, reliable |
| T4 GPU (Azure) | CUDA, batched physics | Slower than CPU |
| Azure D128as_v7 | 128 vCPUs, parallel | Failed initially |
Final runs used a CLI I built for the search.
The local NEAT run used 3 seeds per trial for variance estimation (each configuration tested with seeds 42, 123, 456). The VM runs used 1 seed per trial to maximize trial throughput, which means we're more susceptible to lucky seeds (as the reproduction results later show).
| Mode | Best Fitness | Trials | Seeds | Time |
|---|---|---|---|---|
| Pure NN (VM) | 798.6 | 200 | 1 | ~12 hrs |
| NEAT (VM) | ~400 | 137 | 1 | ~13 hrs |
| NEAT (local) | 441.2 | 100 | 3 | ~48 hrs |
Pure neural networks nearly doubled NEAT's performance on this task. The simple fixed topology beat variable topology. I didn't expect this (more on this later).
I tried to reproduce the top results by running the best configurations again while capturing the full activations and physics frames. 13 reproduction runs (3 Pure, 10 NEAT) using the exact parameters from the top NAS trials:
# Reproduction run - load params from NAS trial, run in frontend python cli.py reproduce neat-full 68 \ --generations 200 \ --population-size 200 # This loads trial_68.json params and runs the full evolution # in the web UI, storing results to PostgreSQL for analysis
| Trial | NAS Best | NAS Avg | Repro Best | Repro Avg |
|---|---|---|---|---|
| Pure #42 (top best) | 798.6 | 81.9 | 420.7 | 58.9 |
| Pure #178 (top avg) | 587.5 | 118.3 | 129.6 | 24.2 |
| NEAT #68 (top best) | 441.2 | 27.1 | 312.9 | 32.6 |
| NEAT #96 (top avg) | 218.2 | 41.7 | — | — |
| NEAT #57 | 439.5 | 27.6 | 609.5 | 34.2 |
NEAT #57 actually exceeded its NAS result (609.5 vs 439), a lucky seed. But Pure #42 and NEAT #68 fell far short. Pure #178's reproduction was especially disappointing - from 118.3 average down to 24.2. Across all 13 reproduction runs, the best performers were:
| Metric | 1st | 2nd | 3rd |
|---|---|---|---|
| Best fitness | NEAT #57 (609.5) | Pure #165 (330.5) | NEAT #94 (313.8) |
| Best average | NEAT #106 (45.0) | Pure #165 (44.3) | Pure #43 (36.4) |
Genetic algorithms are stochastic. The same hyperparameters with different random seeds produce wildly different results. The NAS found configurations that can achieve high fitness, not configurations that reliably achieve it.
The best creatures collected 8 pellets, but the population mean hovered around 0.3 pellets. Most creatures just flailed in place or crawled in the wrong direction.
I had a SINGLE run where the average creature was able to pick up a single pellet. And it didn't reproduce.
The winners were outliers, not the norm. Best fitness varies wildly with luck, but average fitness never exceeded 100 (one pellet) across all 100 NAS trials.
More counterintuitive results:
use_crossover: False. Mutation-only won. Caveat: this could be confounded with other hyperparameters. The standard explanation is that crossover destroys coordinated weight patterns. Parent A learned one strategy, parent B learned another, and mixing them scrambles both.time_encoding=sin produced better population learning (19% ratio) but lower peak (213 best), while time_encoding=none produced extreme elite dominance (4-7% ratio) but higher peak (441 best). If I wanted whole population learning, I'd use sin encoding and accept lower peak performance.initial_connectivity: full. Mean 331.0 vs 272-296 for others.More raw analysis in the NAS postmortem notebook.
For supervised learning with a differentiable loss function, gradient descent is provably more sample-efficient than evolution. Backprop solves MNIST in minutes with 99%+ accuracy. Deep GA would need 1000s of workers and hours to match. This is worth stating clearly: genetic algorithms are not SOTA for tasks where gradients exist.
So when should you use them?
| Method | When to use |
|---|---|
| Gradient descent | Differentiable loss, supervised learning, sample efficiency matters |
| GA / Evolution strategies | Non-differentiable fitness, black-box optimization, massive parallelism available |
| NEAT | Small networks where topology matters, want to see structure emerge |
Evolution Lab uses GA because the fitness function is effectively a black box. Physics simulation involves discontinuities (contacts, friction regimes), long rollouts, and chaotic dynamics where small parameter changes lead to large outcome differences. Even with simulator internals, differentiating through thousands of unstable timesteps would yield noisy, high-variance gradients. Evolution is simpler and more robust for this regime.
Uber AI's Deep Neuroevolution paper (2017) showed GAs can train networks with millions of parameters. They matched DQN and A3C on Atari in wall-clock time, despite using far more environment samples. The trick: GA is embarrassingly parallel across rollouts (each genome evaluation is independent, no replay buffers or gradient sync), so 1000 workers can compensate for low sample efficiency. Note that Atari doesn't have clean gradients either: DQN uses noisy, bootstrapped estimates, not true reward gradients. GA was competing with noisy RL, not backprop.
The real tradeoff is sample-efficient but complex (RL) vs compute-hungry but simple (GA). DQN extracts learning signal from every timestep and assigns credit to individual actions. GA only sees episode-level return and treats the policy as an indivisible blob. For most control problems, RL wins asymptotically. But for black-box, structure-evolving problems like Evolution Lab, GA trades sample efficiency for robustness and simplicity.
So many things that should work in theory don't work in practice, and I didn't have time to explore everything. Fitness sharing, speciation, NEAT, different selection strategies... the literature says these help, but I couldn't get consistent improvements. Maybe my implementations were buggy. Maybe the hyperparameters were wrong. Maybe the task is just different enough that the standard advice doesn't apply.
Claude Code is great at writing code. It's not great at telling you when you're implementing an algorithm wrong. The NEAT bugs (wrong mutation rates, wrong crossover alignment, etc) all came from not reading the paper carefully enough.
The best workflow: understand the theory first, then use Claude to implement it. Not the other way around.
One tool that helped was my /integration-stress-testcommand I built for Claude. When I would find a bug, Claude would first reproduce it via a test before attempting a fix.
This makes the entire codebase much more reliable. AI is not good at writing unit tests because it just tests the functionality it wrote with the same cognition as the code it wrote. So it'll often create tests with the same bugs it introduced.
Instead of hoping evolution learns smooth movement, make smooth movement the only option. This mirrors real biology: joints have limits, tendons only stretch so far. Evolution operates within constraints, it doesn't learn them. The fitness landscape is shaped as much by what's physically impossible as by what's rewarded.
Every time I added a physics constraint, creatures got better. Zero-length muscles led to vibration; add minimum lengths and they started walking. Per-frame output updates caused jitter; add smoothing and they moved deliberately. Each constraint removed a failure mode from the search space. The tradeoff is you might eliminate novel solutions (no catapult mechanics if muscles can't overextend), but removing degenerate solutions is usually worth it.
There's still a lot I don't understand. Why does crossover hurt? Why does proprioception hurt when it should help?
A great next goal would be to find a configuration that consistently generates populations of creatures that can pick up at least 1 pellet within 150 generations.
I have more experiments I want to try: energy systems (metabolic cost for muscle activation), multi-layer hidden networks, better NEAT crossover alignment by matching muscle innovation IDs, recurrent connections (memory), HyperNEAT (indirect encoding via CPPNs), novelty search, coevolution, interspecies mating, actually figuring out why crossover hurts, and gaining more statistical significance on the best runs.
I could keep pushing, but to be frank I need to free up the few hours a day I was spending on this to work on my other projects.
Maybe someone else will pick up where I left off and make something great (it's open source).
For now, the creatures walk. And exhibit creature like behvaiors. That's something.
Two weeks of staring at blobs. I learned more about genetic algorithms by building this than I would have just reading the papers. Though reading the papers first would have helped a lot.
Code is on GitHub. I'm @silennai on Twitter and my website is silennai.com.
Resources: