An interactive browser simulation of the BFF primordial soup experiment described in Computational Life: How Well-formed, Self-replicating Programs Emerge from Simple Interaction by Blaise Agüera y Arcas, Jyrki Alakuijala, James Evans, Ben Laurie, Alexander Mordvintsev, Eyvind Niklasson, Ettore Randazzo, and Luca Versari (Google / University of Chicago, 2024).
The central question: can self-replicating programs arise spontaneously from a soup of random, non-self-replicating programs, with no fitness function, no goal, no designer? The answer, demonstrated across multiple computational substrates, is yes.
The paper draws an analogy to the Origin of Life. Biology has the RNA world hypothesis: at some point in prebiotic chemistry, self-replicating molecules arose from a soup of random organic compounds. The authors ask whether this transition from pre-life to life is a general property of any sufficiently expressive computational substrate, not just biochemistry.
They show that when random programs interact and modify each other, self-replication emerges as an attractor state. The mechanism is primarily self-modification, not random initialization or background mutation. Once a replicator arises, it rapidly colonizes the soup. This is a phase transition: a sudden, irreversible change in the dynamics.
[[{.>]-]]-]>.{[[ as a full-tape genome and immediately runs 3 forced interactions to guarantee early spread. Watch the teal expand.BFF (Brainfuck+) extends Brainfuck by replacing I/O streams with two independent heads on a unified instruction+data tape. Programs can read and write each other's code, enabling self-modification and eventually self-replication.
head0 is the read head. head1 is the write head. Both move independently. All positions wrap modulo tape length.
| OP | ACTION | NOTES |
|---|---|---|
| < | head0 -= 1 | Move read head left (wraps) |
| > | head0 += 1 | Move read head right (wraps) |
| { | head1 -= 1 | Move write head left (wraps) |
| } | head1 += 1 | Move write head right (wraps) |
| + | tape[head0] += 1 | Increment byte at read head (mod 256) |
| - | tape[head0] -= 1 | Decrement byte at read head (mod 256) |
| . | tape[head1] = tape[head0] | Copy: read position to write position |
| , | tape[head0] = tape[head1] | Copy: write position to read position |
| [ | if tape[head0] == 0: jump past ] | Loop start |
| ] | if tape[head0] != 0: jump to [ | Loop end |
| (other) | no-op | 246 other byte values do nothing |
Programs terminate when the IP reaches the end of the tape, or after 213 = 8,192 IP reads (the paper's exact limit). Unmatched brackets also terminate.
The paper's emergent replicator from Figure 4:
tape[head0] != 0. copies one byte from read to write position{), read head moves right (>)The inspector shows OP-PALINDROME: YES when a tape's BF operator subsequence reads the same forwards and backwards.
Parameters faithful to the paper (Section 2.1):
[,}<] structure: its loop breaks on zero bytes. It can copy zeros but cannot write over them. Zeros accumulate (up to ~14% of all bytes at peak). The zero fraction spike is visible in the metrics chart.[<,}] variant emerges that can overwrite zeros. It displaces the first generation. The soup becomes a competitive arena with multiple replicator variants overwriting each other.Self-modification is the primary cause of emergence, not random initialization or background mutation. Runs with zero mutation still produce replicators. Runs with only 128 epochs (too short for self-modification to accumulate) produce replicators only 0.3% of the time.
tape[head0] == 0. The zero-poisoning phase (first-generation replicator vulnerability) shows as a distinct spike. Watch the grid turn red during this phase.Single self-contained HTML file, no dependencies beyond Google Fonts. Single-threaded JS; the paper used CUDA with all 131k tapes in parallel. A WebWorker port would be the natural next step for matching paper-scale throughput.
The naive seed (bare replicator + zero padding) dies every second generation: the replicator always copies itself to B[48..63], and when that tape next runs as A, the outer [ guard sees tape[0] = 0 and skips the entire program.
The fix: fill the 64-byte tape with [ (0x5b) and place the replicator in the last 16 bytes. The [ prefix floods B[0..47] on every copy, so daughter tapes have the same structure. Verified to reach 70%+ replicator density within ~5,000 epochs at 256 tapes. Three forced interactions are run immediately after seeding so the genome does not sit idle waiting for random selection.
Extracts the BF operator subsequence of a tape (ignoring data bytes) and checks if it reads the same forwards and backwards. Necessary but not sufficient for replicator status; many palindromes are not replicators. It is a fingerprint of the replicator family the paper identified.
SLOW@ATT drops to 1 step/frame automatically when replicator density exceeds 60%, so the attractor dynamics are watchable without manual intervention. Toggle it off if you want to keep turbo running through the attractor.
The paper's 0.024% mutation rate is calibrated for 131,072 tapes. At 256 tapes it kills replicators faster than they spread: each replicator would be hit by a random mutation roughly every 7 epochs. This implementation scales the absolute mutation count to ~2 flips/epoch for small soups (under 2048 tapes), matching the biological pressure of the paper's scale rather than the raw percentage.
Paper CUDA implementation: github.com/paradigms-of-intelligence/cubff
This simulation: hed0rah.github.io/bff.html
Built with Claude. No frameworks, no build step.
Agüera y Arcas B, Alakuijala J, Evans J, Laurie B, Mordvintsev A, Niklasson E, Randazzo E, Versari L.
Computational Life: How Well-formed, Self-replicating Programs Emerge from Simple Interaction.
arXiv:2406.19108v2, 2 August 2024.
arxiv.org/abs/2406.19108 | github: cubff