DEV Community

cosmosoneness
cosmosoneness

Posted on

I Built an Artificial Life Simulation Where Cells Carry Tiny Neural Networks — and Started Measuring Their Φ

A six-phase journey from Conway's Game of Life to Integrated Information, all in one TypeScript file tree.


There's a particular flavor of programming that feels less like writing software and more like building a small universe — set the rules, press play, and watch what happens. Conway's Game of Life is the canonical example, but the modern Cambrian explosion of compute has made it almost trivial to go much, much further: real ecosystems with energy and nutrients, evolving genomes, chemical communication, multi-cell organisms, embedded neural networks for decision-making, and — at the top of the stack — measurements borrowed from consciousness research.

I spent a few sessions building exactly that. It's a TypeScript + Vite project called Life, the first subproject of an open-source umbrella I'm calling Cosmos Research Institute. The whole thing runs deterministically from a seed, ticks at about 10 Hz in a browser tab, and ships with 75 passing tests that pin every layer in place.

This post is a tour through the six phases that turn a 128×128 grid of dead pixels into something I can no longer fully predict.

Phase 0 — The foundation

The architecture is the boring-on-purpose part: TypeScript in strict mode, Vite for the dev server, Vitest for tests, and HTML5 Canvas as the only rendering surface. No frameworks, no DOM-per-cell, no WebGL. The simulation core (src/core/) is pure: every tick takes a WorldState and returns a new WorldState. The renderer (src/render/) is the only place side effects happen.

Two foundational utilities make everything else possible:

// xoshiro256** PRNG — deterministic, fast, good statistical properties
const rng = createRNG(42);
rng.next();      // 0..1
rng.nextInt(0, 100);
rng.nextBool(0.3);

// Toroidal grid — coordinates wrap, so the world has no edges
const grid = createGrid<Cell>(128, 128, () => createEmptyCell());
grid.get(-1, 0); // same cell as grid.get(127, 0)
Enter fullscreen mode Exit fullscreen mode

Determinism matters. Same seed → identical universe, every time. That's what lets me run regression tests on a simulation whose emergent behavior I can't predict.

Phase 1 — Conway's Game of Life, the right way

Conway is famous because it's the smallest interesting cellular automaton. B3/S23: a dead cell with exactly three live neighbors becomes alive; a live cell with two or three live neighbors survives. From two rules you get gliders, oscillators, glider guns, even Turing-complete computation.

The implementation is a pure-function tick:

export function tickWorld(state: WorldState): WorldState {
  const next = grid.clone();
  for (let y = 0; y < grid.height; y++) {
    for (let x = 0; x < grid.width; x++) {
      const cell = grid.get(x, y);
      const n = countNeighbors(grid, x, y, c => c.alive);
      if (cell.alive)  next.set(x, y, (n === 2 || n === 3) ? cell : dead());
      else             next.set(x, y, (n === 3) ? born() : dead());
    }
  }
  return { ...state, grid: next, tick: state.tick + 1 };
}
Enter fullscreen mode Exit fullscreen mode

Two tests pin it down: a blinker oscillates with period 2, a block is a still life. If either ever breaks, I know an upstream change leaked into the rules.

Phase 2 — Energy, nutrients, and the end of binary life

Conway's "alive" is binary. Real cells aren't — they need fuel, they starve, they overpopulate. So Phase 2 adds two grid layers underneath the cell grid:

  • Energy — added by sunlight (with a top-down gradient and a sinusoidal day/night cycle), absorbed by living cells, returned to the environment when they die ("decay").
  • Nutrients — regenerate slowly in empty tiles, get consumed by living cells, diffuse via discrete Laplacian to spread.

Now there's a carrying capacity. Dense regions starve themselves. The population oscillates around a stable equilibrium, with boom-bust dynamics that emerge entirely from local rules — no global tuning required.

Phase 3 — Genomes, mutation, speciation

This is where it stops feeling like cellular automata and starts feeling like evolution.

Every living cell carries a 64-bit genome stored as Uint8Array(8). The bits map to traits:

Bits Trait Effect
0–3 birth threshold how many neighbors trigger birth
4–7 survive threshold how many neighbors are needed to survive
8–11 metabolic efficiency scales energy consumption
12–15 reproduction cost energy required to spawn a child
16–19 aggression probability of attacking neighbors
20–23 cooperation probability of sharing energy
24–31 color hue phenotype color (this is what you see)

Reproduction copies the parent's genome and applies per-bit mutation at a small rate. Adjacent compatible cells (Hamming distance < threshold) can also reproduce sexually via single-point crossover. Over generations, a greedy clustering algorithm groups similar genomes into species — and because color hue is part of the genome, related cells naturally cluster visually into colored bands.

Within minutes of starting from a uniform random soup, the simulation reliably evolves dozens of co-existing species. None of this is hard-coded. It's just selection acting on local rules.

Phase 4 — Organisms, signals, terrain

Cells alone get boring. Multi-cell organisms are where the interesting structure lives. Every tick, a flood-fill connected-component algorithm finds clusters of cells whose genomes are mutually similar (Hamming distance < 8). Each cluster gets an organism record with size, centroid, total energy, and a partition into boundary cells (touching at least one non-member) and interior cells. Interior cells share energy with boundary cells (amplified by the cooperation gene). Organisms larger than 20 cells split at their thinnest point.

On top of this sits a chemical signaling layer — three diffusing scalar fields:

  • Food signal — emitted by cells with surplus energy. Other cells preferentially reproduce toward the gradient (chemotaxis).
  • Danger signal — emitted by cells under attack. Other cells preferentially reproduce away.
  • Attract signal — emitted during reproduction readiness. Adjusts mating direction.

I never coded flocking, herding, or predator avoidance directly. They show up anyway, as side effects of local emission + diffusion + gradient sensing.

Finally there's terrain: multi-octave value noise generates an elevation map, water occupies low areas (cells can't live there), latitude + elevation set per-tile temperature, and a seasonal cycle modulates everything globally. Meteors, plagues, and ice ages strike at random and stress the population from outside.

Phase 5 — Genomes that think

Phase 5 doubles the genome from 64 to 128 bits and uses the new bits to encode a small feed-forward neural network: 4 inputs, 2 hidden, 3 outputs. Fourteen weights, each a 4-bit signed integer.

Inputs:  [normalized energy, neighbor count, food signal, danger signal]
Hidden:  2 nodes with tanh activation
Outputs: [action, direction bias, signal emission]
         action ∈ {STAY, MOVE, REPRODUCE} via thresholded output
Enter fullscreen mode Exit fullscreen mode

Each tick, every cell runs its own neural network and acts on the output. Mutation and crossover operate on raw genome bits, so the neural weights evolve through natural selection — cells with brains that lead to better survival out-reproduce cells with worse brains.

Click any cell in the running simulation and a live inspector pops up: phenotype values, current neural inputs, current output, and a diagram of the network with green/red connections sized by weight magnitude. It's hypnotic to watch the same brain fire across hundreds of related cells in a clade.

Phase 6 — Measuring something like consciousness

Here's where I went off the deep end, in a fun way.

If organisms are forming via natural selection, and they're integrating information across their member cells, then maybe — just maybe — they should register on the metrics consciousness researchers have proposed. Integrated Information Theory (IIT) defines a quantity called Φ (phi) that measures how much information a system generates beyond the sum of its parts. Exact Φ is computationally intractable, but there's a useful approximation called Φ*:

  1. For each organism, sample 24 random bipartitions of its cells.
  2. For each bipartition, compute the normalized mutual information across the cut, using a discrete state token per cell (energy bucket + age bucket + aggression + neighborhood signature).
  3. Report the minimum NMI as Φ* — the bottleneck partition.

High Φ* means the organism's information is integrated: you can't cleanly decompose it into independent parts without losing structure. Low Φ* means it's just a pile of cells that happen to be next to each other.

The Phase 6 dashboard surfaces this alongside a few other complexity metrics:

  • Shannon entropy of the cell-state distribution (bits/cell)
  • Compression complexity — the more compressible the grid, the more ordered it is
  • Spatial mutual information between distant regions — long-range order detection
  • Lyapunov exponent estimate from population history — chaotic vs stable dynamics

There's also a hypothesis log: whenever an organism's Φ* crosses a configurable threshold, the simulation records the tick, the organism's size, its centroid, and a hex hash of its founder's genome.

In a test run on a 64×64 grid with seed 42, one organism reached Φ* = 1.0. That doesn't mean it's conscious. It means the integrated-information measure detected something — and that something arose from local rules with zero global supervision.

What I take away from this

  1. Determinism is non-negotiable for emergence research. Without seed reproducibility, every weird thing you observe is unfalsifiable. The xoshiro256** + immutable-state-per-tick discipline pays off constantly.

  2. You can't reason your way to good rule sets — you have to run them. I spent more time on the thresholds (mutation rate, reproduction cost, signal decay) than on the algorithms. Good rules are a search problem.

  3. The neural network layer changes the regime. Before Phase 5, behaviors were essentially hand-coded. After Phase 5, behaviors are selected for. You can almost feel the moment when chemotaxis starts emerging from random networks under selection pressure.

  4. Φ* is suggestive, not conclusive. I make no claim that any organism here is conscious. But the metric does flag exactly the structures that look coherent — and that's already useful as an instrument.

Try it

git clone https://github.com/cosmosoneness/Cosmos.git
cd Cosmos/Emulation/Life
npm install
npm run dev
# open http://localhost:5173
Enter fullscreen mode Exit fullscreen mode
Key Action
Space pause / resume
. single-step
V cycle view (genome / life / energy / nutrients / signals / terrain / phi)
D toggle emergence dashboard
Click inspect a cell (phenotype + neural network)

The whole project is MIT-licensed. It's also part of the Cosmos Research Institute — an umbrella for "computational emulations" that try to run scientific systems end-to-end on a laptop. Alongside Life are two siblings: HumanBody (a 13-organ multi-scale physiology simulator) and WholeBrainEmulation (connectome-level neural simulation with consciousness metrics). The plan is to keep building until the institute earns its name.

If you build something on top of it, or just want to argue about Φ*, I'd love to hear from you.


Built with the help of Claude Code. All bugs are mine.

Top comments (0)