A "primordial soup" simulation, as explained in the paper "Computational Life: How Well-formed, Self-replicating Programs Emerge from Simple Interaction.
import React, { useState, useEffect, useRef } from 'react';
import { createRoot } from 'react-dom/client';
// ============ Config & Types ============
const CONFIG = {
SEED: 13200, // lucky seed, non random for determinstic outcome
NUM_PROGRAMS: 32768,
MAX_HISTORY: 200,
EPOCHS_PER_UI_UPDATE: 1,
VM_MAX_STEPS: 8192,
SAMPLE_GRID_SIZE_MOBILE: 8,
SAMPLE_GRID_SIZE_MAX: 16,
PROGRAM_LENGTH: 64
} as const;
type Tape = Uint8Array;
interface SoupConfig {
numPrograms: number;
mutationRate: number;
}
interface TopProgram {
programIndex: number;
fraction: number;
}
interface EpochResult {
epoch: number;
dominantFraction: number;
distinctPrograms: number;
programEntropy: number;
topPrograms?: TopProgram[];
}
interface MetricsHistory {
epochs: number[];
dominantFraction: number[];
programEntropy: number[];
}
// Single source of truth for BFF instruction visualization
const INSTRUCTIONS = [
{ byte: 60, label: '<', name: 'Move head0 left', cls: 'move-left' },
{ byte: 62, label: '>', name: 'Move head0 right', cls: 'move-right' },
{ byte: 123, label: '{', name: 'Move head1 left', cls: 'move-left-alt' },
{ byte: 125, label: '}', name: 'Move head1 right', cls: 'move-right-alt' },
{ byte: 45, label: '-', name: 'Decrement (head0)', cls: 'decrement' },
{ byte: 43, label: '+', name: 'Increment (head0)', cls: 'increment' },
{ byte: 46, label: '.', name: 'Copy head0 → head1', cls: 'copy-to' },
{ byte: 44, label: ',', name: 'Copy head1 → head0', cls: 'copy-from' },
{ byte: 91, label: '[', name: 'While head0 ≠ 0', cls: 'loop-start' },
{ byte: 93, label: ']', name: 'Loop end', cls: 'loop-end' },
{ byte: 0, label: '0', name: 'Zero data / loop exit', cls: 'zero' },
{ byte: 255, label: '∅', name: 'Nonzero data / no-op', cls: 'noop' },
] as const;
const BYTE_TO_CLASS: Record<number, string> = Object.fromEntries(
INSTRUCTIONS.map(i => [i.byte, i.cls])
);
// Shared grid dimension calculation
const getGridLayout = (programLength: number) => {
const rows = Math.floor(Math.sqrt(programLength));
return { rows, cols: Math.ceil(programLength / rows) };
};
const DescriptionToggle = ({ label, isOpen, onToggle, children }: { label: string; isOpen: boolean; onToggle: () => void; children: React.ReactNode }) => (
<>
<button className="description-toggle" onClick={onToggle}>{isOpen ? '▼' : '▶'} {label}</button>
{isOpen && <div className="description-text">{children}</div>}
</>
);
const InstructionKey = ({ colorMode }: { colorMode: 'instructions' | 'raw' }) => (
<div className="instruction-key">
{INSTRUCTIONS.filter(instr => colorMode === 'instructions' || instr.byte !== 255).map((instr, i) => (
<div key={i} className="instruction-key-item">
<div
className={`instruction-key-color ${colorMode === 'instructions' ? `program-byte ${instr.cls}` : 'raw-byte'}`}
style={colorMode === 'raw' ? { backgroundColor: byteToColor(instr.byte) } : undefined}
/>
<span className="instruction-key-label">{instr.label}</span>
<span className="instruction-key-name">{instr.name}</span>
</div>
))}
</div>
);
const addToHistory = (prev: MetricsHistory, { epoch, dominantFraction, programEntropy }: EpochResult): MetricsHistory => {
const append = <T,>(arr: T[], val: T) => [...arr, val].slice(-CONFIG.MAX_HISTORY);
return { epochs: append(prev.epochs, epoch), dominantFraction: append(prev.dominantFraction, dominantFraction), programEntropy: append(prev.programEntropy, programEntropy) };
};
const samplePrograms = (view: Uint8Array, programLength: number, count: number): Tape[] => {
const n = Math.min(count, view.length / programLength);
return Array.from({ length: n }, (_, i) => view.slice(i * programLength, (i + 1) * programLength));
};
// ============ Worker Creation ============
const createSimulationWorker = () => {
const workerCode = `
// ============ RNG ============
function hash32(x) {
x |= 0;
x = Math.imul(x ^ (x >>> 16), 0x21f0aaad);
x = Math.imul(x ^ (x >>> 15), 0x735a2d97);
x = x ^ (x >>> 15);
return x >>> 0;
}
class MulberryRng {
constructor(seed) {
this.state = seed >>> 0;
}
nextRaw() {
this.state = (this.state + 0x6D2B79F5) | 0;
let t = Math.imul(this.state ^ (this.state >>> 15), 1 | this.state);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return (t ^ (t >>> 14)) >>> 0;
}
nextInt(max) {
return this.nextRaw() % max;
}
nextByte() {
return this.nextInt(256);
}
nextBool() {
return (this.nextRaw() & 1) === 1;
}
}
// ============ BFF VM (optimized) ============
class BffVm {
constructor(maxSteps) {
this.maxSteps = maxSteps || ${CONFIG.VM_MAX_STEPS};
}
findMatchingBracket(tape, start, forward) {
const openChar = forward ? 91 : 93;
const closeChar = forward ? 93 : 91;
let depth = 1;
let pos = start;
const len = tape.length;
while (depth > 0) {
pos = forward ? pos + 1 : pos - 1;
if (pos < 0 || pos >= len) return -1;
if (tape[pos] === openChar) depth++;
else if (tape[pos] === closeChar) depth--;
}
return pos;
}
runOnTape(tape) {
let ip = 0;
let head0 = 0;
let head1 = 0;
let steps = 0;
const len = tape.length;
const maxSteps = this.maxSteps;
while (steps < maxSteps) {
const instruction = tape[ip];
switch (instruction) {
case 60: head0 = (head0 - 1 + len) % len; break;
case 62: head0 = (head0 + 1) % len; break;
case 123: head1 = (head1 - 1 + len) % len; break;
case 125: head1 = (head1 + 1) % len; break;
case 45: tape[head0] = (tape[head0] - 1) & 0xff; break;
case 43: tape[head0] = (tape[head0] + 1) & 0xff; break;
case 46: tape[head1] = tape[head0]; break;
case 44: tape[head0] = tape[head1]; break;
case 91:
if (tape[head0] === 0) {
const m = this.findMatchingBracket(tape, ip, true);
if (m === -1) return tape;
ip = m;
}
break;
case 93:
if (tape[head0] !== 0) {
const m = this.findMatchingBracket(tape, ip, false);
if (m === -1) return tape;
ip = m;
}
break;
}
ip = (ip + 1) % len;
steps++;
}
return tape;
}
}
// ============ Worker State ============
let vm = null;
let workerId = null;
let programLength = 0;
let scratchTape = null;
let syncArray = null;
// ============ Message Handler ============
self.onmessage = function(e) {
const { type, payload } = e.data;
switch (type) {
case 'init': {
const { seed, id, config, sabSync } = payload;
workerId = id;
programLength = config.programLength;
vm = new BffVm();
scratchTape = new Uint8Array(2 * programLength);
syncArray = new Int32Array(sabSync);
self.postMessage({ type: 'ready', workerId });
break;
}
case 'processPairs': {
const {
sabPrograms,
sabPairs,
sabPairOrder,
prevOffset,
nextOffset,
startPairIdx,
endPairIdx
} = payload;
const programs = new Uint8Array(sabPrograms);
const pairs = new Uint32Array(sabPairs);
const pairOrder = new Uint8Array(sabPairOrder);
const tape = scratchTape;
const L = programLength;
for (let k = startPairIdx; k < endPairIdx; k++) {
const i = pairs[2 * k];
const j = pairs[2 * k + 1];
const prevBaseI = prevOffset + i * L;
const prevBaseJ = prevOffset + j * L;
const aFirst = pairOrder[k] === 1;
if (aFirst) {
for (let b = 0; b < L; b++) {
tape[b] = programs[prevBaseI + b];
tape[L + b] = programs[prevBaseJ + b];
}
} else {
for (let b = 0; b < L; b++) {
tape[b] = programs[prevBaseJ + b];
tape[L + b] = programs[prevBaseI + b];
}
}
vm.runOnTape(tape);
const nextBaseI = nextOffset + i * L;
const nextBaseJ = nextOffset + j * L;
if (aFirst) {
for (let b = 0; b < L; b++) {
programs[nextBaseI + b] = tape[b];
programs[nextBaseJ + b] = tape[L + b];
}
} else {
for (let b = 0; b < L; b++) {
programs[nextBaseJ + b] = tape[b];
programs[nextBaseI + b] = tape[L + b];
}
}
}
Atomics.store(syncArray, workerId, 1);
self.postMessage({ type: 'pairsComplete', workerId });
break;
}
case 'mutate': {
const {
sabPrograms,
bufferOffset,
startIdx,
endIdx,
mutationRate,
epochIndex,
mutationSeedBase,
} = payload;
const programs = new Uint8Array(sabPrograms);
const threshold = mutationRate * 0xFFFFFFFF;
for (let idx = startIdx; idx < endIdx; idx++) {
const byteIdx = bufferOffset + idx;
const h = hash32(mutationSeedBase ^ epochIndex ^ idx);
if (h < threshold) {
const old = programs[byteIdx];
let newByte = (h >>> 24) & 0xff;
if (newByte === old) newByte = (newByte + 1) & 0xff;
programs[byteIdx] = newByte;
}
}
Atomics.store(syncArray, workerId, 2);
self.postMessage({ type: 'mutateComplete', workerId });
break;
}
case 'stop': {
self.close();
break;
}
}
};
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
return new Worker(URL.createObjectURL(blob));
};
// ============ Main Thread Helpers ============
class MulberryRng {
private state: number;
constructor(seed: number) { this.state = seed >>> 0; }
nextRaw(): number {
this.state = (this.state + 0x6D2B79F5) | 0;
let t = Math.imul(this.state ^ (this.state >>> 15), 1 | this.state);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return (t ^ (t >>> 14)) >>> 0;
}
nextInt(max: number) { return this.nextRaw() % max; }
nextBool() { return (this.nextRaw() & 1) === 1; }
fork() { return (this.nextInt(0x7FFFFFFF) + 1) >>> 0; }
}
// Fast hash function for program (FNV-1a variant)
function hashProgram(view: Uint8Array, programIndex: number, programLength: number): number {
const offset = programIndex * programLength;
let hash = 2166136261; // FNV offset basis (32-bit)
for (let i = 0; i < programLength; i++) {
hash ^= view[offset + i];
hash = Math.imul(hash, 16777619); // FNV prime
}
return hash >>> 0; // Convert to unsigned 32-bit
}
// Hash a single byte to distribute colors evenly across spectrum
const hashByte = (b: number) => {
let h = b | 0;
h = Math.imul(h ^ (h >>> 16), 0x21f0aaad);
h = Math.imul(h ^ (h >>> 15), 0x735a2d97);
h = h ^ (h >>> 15);
return h >>> 0;
};
// Map byte value (0-255) to a color for raw byte visualization
const byteToColor = (b: number) => {
const hashed = hashByte(b);
const hue = hashed % 360;
return `hsl(${hue}, 90%, 50%)`;
};
// Compute program-level statistics to detect life
function computeProgramStats(
view: Uint8Array,
numPrograms: number,
programLength: number,
epoch: number
): EpochResult {
const counts = new Map<number, { count: number; idx: number }>();
for (let i = 0; i < numPrograms; i++) {
const hash = hashProgram(view, i, programLength);
const entry = counts.get(hash);
if (entry) entry.count++;
else counts.set(hash, { count: 1, idx: i });
}
const entries = [...counts.entries()].map(([hash, { count, idx }]) => ({ hash, count, idx }));
let maxCount = 0;
for (const { count } of entries) if (count > maxCount) maxCount = count;
const programEntropy = entries.reduce((sum, { count }) => {
const p = count / numPrograms;
return sum - p * Math.log2(p);
}, 0);
entries.sort((a, b) => b.count - a.count);
const topPrograms: TopProgram[] = entries.slice(0, 8).map(e => ({
programIndex: e.idx,
fraction: e.count / numPrograms,
}));
return {
epoch,
dominantFraction: maxCount / numPrograms,
distinctPrograms: counts.size,
programEntropy,
topPrograms,
};
}
function fillPairs(pairs: Uint32Array, rng: MulberryRng) {
const N = pairs.length;
for (let i = 0; i < N; i++) pairs[i] = i;
for (let i = N - 1; i > 0; i--) {
const j = rng.nextInt(i + 1);
const tmp = pairs[i];
pairs[i] = pairs[j];
pairs[j] = tmp;
}
}
const awaitWorkerMessage = (worker: Worker, workerId: number, messageType: string) =>
new Promise<void>(resolve => {
const handler = (e: MessageEvent) => {
if (e.data.type === messageType && e.data.workerId === workerId) {
worker.removeEventListener('message', handler);
resolve();
}
};
worker.addEventListener('message', handler);
});
// ============ Worker Pool with Double-Buffer ============
class WorkerPool {
private workers: Worker[] = [];
private numWorkers;
private numPrograms;
private programLength;
private programsSize;
private sabPrograms;
private sabPairs;
private sabPairOrder;
private sabSync;
private pairs;
private pairOrder;
private syncArray;
private currentBuffer: 0 | 1 = 0;
private mutationSeedBase: number;
constructor(numWorkers: number, numPrograms: number, programLength: number, seed: number) {
this.numWorkers = numWorkers;
this.numPrograms = numPrograms;
this.programLength = programLength;
this.programsSize = numPrograms * programLength;
this.mutationSeedBase = seed >>> 0;
this.sabPrograms = new SharedArrayBuffer(this.programsSize * 2);
this.sabPairs = new SharedArrayBuffer(numPrograms * 4);
this.sabPairOrder = new SharedArrayBuffer(Math.ceil(numPrograms / 2));
this.sabSync = new SharedArrayBuffer(numWorkers * 4);
this.pairs = new Uint32Array(this.sabPairs);
this.pairOrder = new Uint8Array(this.sabPairOrder);
this.syncArray = new Int32Array(this.sabSync);
const rng = new MulberryRng(seed);
const programs = new Uint8Array(this.sabPrograms, 0, this.programsSize);
for (let i = 0; i < this.programsSize; i++) programs[i] = rng.nextInt(256);
for (let i = 0; i < numWorkers; i++) {
const worker = createSimulationWorker();
this.workers.push(worker);
worker.postMessage({ type: 'init', payload: { seed: seed + i * 100000, id: i, config: { programLength, numPrograms }, sabSync: this.sabSync }});
}
}
get prevOffset() { return this.currentBuffer * this.programsSize; }
get nextOffset() { return (1 - this.currentBuffer) * this.programsSize; }
getProgramsView() {
this.memoryBarrier();
return new Uint8Array(this.sabPrograms, this.prevOffset, this.programsSize);
}
swapBuffers() { this.currentBuffer = (1 - this.currentBuffer) as 0 | 1; }
private memoryBarrier() {
for (let i = 0; i < this.numWorkers; i++) {
Atomics.load(this.syncArray, i);
}
}
private async distributeWork(start: (w: number) => void, total: number, completeType: string) {
const perWorker = Math.ceil(total / this.numWorkers);
await Promise.all(this.workers.map((worker, w) => {
const startIdx = w * perWorker;
if (startIdx >= total) return Promise.resolve();
start(w);
return awaitWorkerMessage(worker, w, completeType);
}));
}
async waitForReady() {
await this.distributeWork(() => {}, this.numWorkers, 'ready');
}
async runEpoch(rng: MulberryRng, mutationRate: number, epochIndex: number) {
const numPairs = this.numPrograms / 2;
fillPairs(this.pairs, rng);
for (let k = 0; k < numPairs; k++) this.pairOrder[k] = rng.nextBool() ? 1 : 0;
const pairsPerWorker = Math.ceil(numPairs / this.numWorkers);
await this.distributeWork(
w => {
const startPairIdx = w * pairsPerWorker;
const endPairIdx = Math.min(startPairIdx + pairsPerWorker, numPairs);
this.workers[w].postMessage({
type: 'processPairs',
payload: {
sabPrograms: this.sabPrograms,
sabPairs: this.sabPairs,
sabPairOrder: this.sabPairOrder,
startPairIdx,
endPairIdx,
prevOffset: this.prevOffset,
nextOffset: this.nextOffset
}
});
},
numPairs,
'pairsComplete'
);
this.memoryBarrier();
const bytesPerWorker = Math.ceil(this.programsSize / this.numWorkers);
await this.distributeWork(
w => {
const startIdx = w * bytesPerWorker;
const endIdx = Math.min(startIdx + bytesPerWorker, this.programsSize);
this.workers[w].postMessage({
type: 'mutate',
payload: {
sabPrograms: this.sabPrograms,
bufferOffset: this.nextOffset,
startIdx,
endIdx,
mutationRate,
epochIndex,
mutationSeedBase: this.mutationSeedBase,
}
});
},
this.programsSize,
'mutateComplete'
);
this.memoryBarrier();
this.swapBuffers();
}
terminate() { this.workers.forEach(w => w.terminate()); this.workers = []; }
getWorkerCount() { return this.numWorkers; }
}
// ============ Simulation Hook ============
function useSimulation(config: SoupConfig, seed: number) {
const [isRunning, setIsRunning] = useState(false);
const [currentEpoch, setCurrentEpoch] = useState(0);
const [currentMetrics, setCurrentMetrics] = useState<EpochResult | null>(null);
const [history, setHistory] = useState<MetricsHistory>({ epochs: [], dominantFraction: [], programEntropy: [] });
const [programsSnapshot, setProgramsSnapshot] = useState<{ programs: Tape[]; epoch: number } | null>(null);
const [visualUpdate, setVisualUpdate] = useState(0);
const [numWorkers] = useState(navigator.hardwareConcurrency || 4);
const [topPrograms, setTopPrograms] = useState<Uint8Array[] | null>(null);
const [topProgramFractions, setTopProgramFractions] = useState<number[] | null>(null);
const workerPoolRef = useRef<WorkerPool | null>(null);
const rngRef = useRef<MulberryRng | null>(null);
const epochRef = useRef(0);
const isRunningRef = useRef(false);
const shouldStopRef = useRef(false);
const updateFromStats = (view: Uint8Array, stats: EpochResult) => {
setCurrentEpoch(stats.epoch);
setCurrentMetrics(stats);
setHistory(prev => addToHistory(prev, stats));
// Update top programs
if (!stats.topPrograms?.length) {
setTopPrograms(null);
setTopProgramFractions(null);
} else {
setTopPrograms(stats.topPrograms.map(({ programIndex }) =>
view.slice(programIndex * CONFIG.PROGRAM_LENGTH, (programIndex + 1) * CONFIG.PROGRAM_LENGTH)
));
setTopProgramFractions(stats.topPrograms.map(g => g.fraction));
}
// Update snapshot
setProgramsSnapshot({
programs: samplePrograms(view, CONFIG.PROGRAM_LENGTH, CONFIG.SAMPLE_GRID_SIZE_MAX ** 2),
epoch: stats.epoch
});
setVisualUpdate(v => v + 1);
};
const initialize = async () => {
workerPoolRef.current?.terminate();
const rng = new MulberryRng(seed);
rngRef.current = rng;
const pool = new WorkerPool(numWorkers, config.numPrograms, CONFIG.PROGRAM_LENGTH, rng.fork());
workerPoolRef.current = pool;
await pool.waitForReady();
setTopPrograms(null);
setTopProgramFractions(null);
epochRef.current = 0;
setProgramsSnapshot({ programs: samplePrograms(pool.getProgramsView(), CONFIG.PROGRAM_LENGTH, CONFIG.SAMPLE_GRID_SIZE_MAX ** 2), epoch: 0 });
setCurrentEpoch(0);
setCurrentMetrics(null);
setHistory({ epochs: [], dominantFraction: [], programEntropy: [] });
};
const step = async () => {
const pool = workerPoolRef.current;
if (!pool || !rngRef.current) return;
const nextEpochIndex = epochRef.current + 1;
await pool.runEpoch(rngRef.current, config.mutationRate, nextEpochIndex);
epochRef.current = nextEpochIndex;
const stats = computeProgramStats(pool.getProgramsView(), config.numPrograms, CONFIG.PROGRAM_LENGTH, epochRef.current);
updateFromStats(pool.getProgramsView(), stats);
};
useEffect(() => {
return () => { isRunningRef.current = false; workerPoolRef.current?.terminate(); };
}, []);
useEffect(() => {
if (!workerPoolRef.current) return;
if (isRunning) {
isRunningRef.current = true;
shouldStopRef.current = false;
const runLoop = async () => {
while (isRunningRef.current) {
const pool = workerPoolRef.current!;
const rng = rngRef.current!;
for (let i = 0; i < CONFIG.EPOCHS_PER_UI_UPDATE; i++) {
const nextEpochIndex = epochRef.current + i + 1;
await pool.runEpoch(rng, config.mutationRate, nextEpochIndex);
if (shouldStopRef.current) { isRunningRef.current = false; return; }
}
epochRef.current += CONFIG.EPOCHS_PER_UI_UPDATE;
const view = pool.getProgramsView();
updateFromStats(view, computeProgramStats(view, config.numPrograms, CONFIG.PROGRAM_LENGTH, epochRef.current));
await new Promise(resolve => setTimeout(resolve, 0));
}
};
runLoop();
} else {
shouldStopRef.current = true;
}
}, [isRunning]);
return { isRunning, setIsRunning, currentEpoch, currentMetrics, history, programsSnapshot, visualUpdate, initialize, step, numWorkers, topPrograms, topProgramFractions };
}
const TopProgramsView = ({
programs,
fractions,
viewMode,
programLength,
colorMode,
}: {
programs: Uint8Array[];
fractions: number[];
viewMode: '1d' | '2d';
programLength: number;
colorMode: 'instructions' | 'raw';
}) => {
if (!programs || programs.length === 0) return null;
const { cols } = getGridLayout(programLength);
const is1d = viewMode === '1d';
return (
<div className={`top-programs mode-${viewMode}`}>
{programs.map((prog, idx) => (
<div key={idx} className={`top-program-item-${viewMode}`}>
<div
className={is1d ? 'program-line' : 'program-cell'}
style={{ gridTemplateColumns: `repeat(${is1d ? programLength : cols}, 1fr)` }}
>
{Array.from(prog).map((byte, i) => (
<ByteCell key={i} byte={byte} colorMode={colorMode} />
))}
</div>
<div className="top-program-fraction">{(fractions[idx] * 100).toFixed(2)}%</div>
</div>
))}
</div>
);
};
function LineChart({ data, width, height, color, label }: { data: number[]; width: number; height: number; color: string; label: string }) {
if (data.length < 2) return null;
const min = Math.min(...data);
const max = Math.max(...data);
const padding = (max - min || 1) * 0.15;
const yMin = min - padding;
const yRange = (max + padding) - yMin;
const pad = 3;
const points = data.map((value, i) => {
const x = pad + (i / (data.length - 1)) * (width - 2 * pad);
const y = pad + (height - 2 * pad) * (1 - (value - yMin) / yRange);
return `${x},${y}`;
}).join(' ');
return (
<div className="chart">
<div className="chart-label">{label}</div>
<svg viewBox={`0 0 ${width} ${height}`} className="chart-svg" preserveAspectRatio="none">
<polyline points={points} fill="none" stroke={color} strokeWidth="2" />
</svg>
</div>
);
}
const ByteCell = ({ byte, colorMode }: { byte: number; colorMode: 'instructions' | 'raw' }) => (
<div
className={`program-byte ${colorMode === 'instructions' ? (BYTE_TO_CLASS[byte] || 'noop') : 'raw-byte'}`}
style={colorMode === 'raw' ? { backgroundColor: byteToColor(byte) } : undefined}
/>
);
const ProgramGrid = React.memo(({ programs, updateKey, mode, programLength, gridSize, colorMode }: { programs: Tape[]; updateKey: number; mode: '2d' | '1d'; programLength: number; gridSize: number; colorMode: 'instructions' | 'raw' }) => {
const sampledPrograms = programs.slice(0, gridSize * gridSize);
const { cols } = getGridLayout(programLength);
const is1d = mode === '1d';
const outerCols = is1d ? Math.floor((gridSize * cols) / programLength) : gridSize;
return (
<div
className={is1d ? 'program-grid-1d' : 'program-grid'}
style={{ gridTemplateColumns: `repeat(${outerCols}, 1fr)` }}
>
{sampledPrograms.map((tape, idx) => (
<div
key={idx}
className={is1d ? 'program-line' : 'program-cell'}
style={{ gridTemplateColumns: `repeat(${is1d ? programLength : cols}, 1fr)` }}
>
{Array.from(tape).map((byte, i) => <ByteCell key={i} byte={byte} colorMode={colorMode} />)}
</div>
))}
</div>
);
}, (prevProps, nextProps) => prevProps.updateKey === nextProps.updateKey);
const Controls = ({ isRunning, onToggleRun, onStep, onReset, numWorkers, currentEpoch }: { isRunning: boolean; onToggleRun: () => void; onStep: () => Promise<void>; onReset: () => void; numWorkers: number; currentEpoch: number }) => (
<div className="controls">
<button onClick={onToggleRun} className="btn-icon">{isRunning ? '⏸' : '▶'}</button>
<button onClick={onStep} disabled={isRunning} className="btn-icon">⏭</button>
<button onClick={onReset} className="btn-icon">↻</button>
<div className="info-stack">
<span className="info-text">Cores: <strong>{numWorkers}</strong></span>
<span className="info-text">Epoch: <strong>{currentEpoch}</strong></span>
</div>
</div>
);
const ToggleButton = ({ onClick, title, className, children }: { onClick: () => void; title: string; className?: string; children: React.ReactNode }) => (
<button className={className ? `view-mode-toggle ${className}` : 'view-mode-toggle'} onClick={onClick} title={title}>{children}</button>
);
const cycleViewMode = (current: '2d' | '1d') => current === '2d' ? '1d' : '2d';
const cycleColorMode = (current: 'instructions' | 'raw') => current === 'instructions' ? 'raw' : 'instructions';
const ConfigSlider = ({ label, value, min, max, step, format, onChange, toSliderValue = (v) => v, fromSliderValue = (v) => v }: {
label: string;
value: number;
min: string;
max: string;
step: string;
format: (v: number) => string;
onChange: (v: number) => void;
toSliderValue?: (v: number) => number;
fromSliderValue?: (v: number) => number;
}) => (
<div className="config-group">
<label>{label}</label>
<input type="range" min={min} max={max} step={step} value={toSliderValue(value)} onChange={e => onChange(fromSliderValue(Number(e.target.value)))} />
<span className="config-value">{format(value)}</span>
</div>
);
const CONFIG_SLIDERS: { label: string; key: keyof SoupConfig; min: string; max: string; step: string; format: (v: number) => string; toSliderValue?: (v: number) => number; fromSliderValue?: (v: number) => number; }[] = [
{ label: 'Programs', key: 'numPrograms', min: '10', max: '17', step: '1', format: String, toSliderValue: (v) => Math.log2(v), fromSliderValue: (v) => 2 ** v },
{ label: 'Mutation Rate', key: 'mutationRate', min: '0', max: '0.001', step: '0.00001', format: v => v.toFixed(5) },
];
const ConfigPanel = ({ config, onChange }: { config: SoupConfig; onChange: (config: SoupConfig) => void }) => (
<div className="config-panel">
{CONFIG_SLIDERS.map(({ label, key, ...rest }) => (
<ConfigSlider key={key} label={label} value={config[key]} {...rest} onChange={v => onChange({ ...config, [key]: v })} />
))}
</div>
);
function App() {
const [config, setConfig] = useState<SoupConfig>(() => {
return {
numPrograms: CONFIG.NUM_PROGRAMS,
mutationRate: 0.00024
};
});
const [isInitialized, setIsInitialized] = useState(false);
const [showDescription, setShowDescription] = useState(false);
const [showVisualizationDescription, setShowVisualizationDescription] = useState(false);
const [showIntroduction, setShowIntroduction] = useState(false);
const [viewMode, setViewMode] = useState<'2d' | '1d'>('2d');
const [viewModeUpdateKey, setViewModeUpdateKey] = useState(0);
const [colorMode, setColorMode] = useState<'instructions' | 'raw'>('instructions');
const [desktopGridSize, setDesktopGridSize] = useState<8 | 16>(16);
const [gridSize, setGridSize] = useState<number>(CONFIG.SAMPLE_GRID_SIZE_MOBILE);
const prevConfigRef = useRef(config);
const shouldRestartRef = useRef(false);
const bumpViewKey = () => setViewModeUpdateKey(k => k + 1);
const simulation = useSimulation(config, CONFIG.SEED);
useEffect(() => {
const onResize = () => {
setGridSize(window.matchMedia('(min-width: 1024px)').matches ? desktopGridSize : CONFIG.SAMPLE_GRID_SIZE_MOBILE);
bumpViewKey();
};
onResize();
window.addEventListener('resize', onResize);
return () => window.removeEventListener('resize', onResize);
}, [desktopGridSize]);
const gridDims = getGridLayout(CONFIG.PROGRAM_LENGTH);
useEffect(() => {
const prev = prevConfigRef.current;
if (prev.numPrograms !== config.numPrograms || prev.mutationRate !== config.mutationRate) {
if (simulation.isRunning) shouldRestartRef.current = true;
simulation.setIsRunning(false);
handleReset();
}
prevConfigRef.current = config;
}, [config]);
useEffect(() => {
if (isInitialized && shouldRestartRef.current) {
shouldRestartRef.current = false;
simulation.setIsRunning(true);
}
}, [isInitialized]);
useEffect(() => {
simulation.initialize().then(() => setIsInitialized(true));
}, []);
const handleReset = async () => {
simulation.setIsRunning(false);
await new Promise(resolve => setTimeout(resolve, 0));
setIsInitialized(false);
await simulation.initialize();
setIsInitialized(true);
};
return (
<div className="app">
<header className="header">
<h1>🧬 Primordial Soup Sim</h1>
<div className="intro-toggle">
<DescriptionToggle
label="What is this?"
isOpen={showIntroduction}
onToggle={() => setShowIntroduction(!showIntroduction)}
>
<p className="intro-text">
It's a "primordial soup" simulation, as explained in
<a href="https://arxiv.org/abs/2406.19108" target="_blank" rel="noopener noreferrer"> this paper</a>.
<br/><br/>We start with a population of random BFF programs (a Brainfuck variant where code and data share
the same tape). In each epoch, random pairs are run together on a two-head VM, overwritten,
split back apart, and lightly mutated. Occasionally this produces self-copying programs that
overwrite their neighbors, colonizing the soup.
</p>
</DescriptionToggle>
</div>
</header>
<div className="control-bar">
<div className="control-row">
<Controls
isRunning={simulation.isRunning}
onToggleRun={() => isInitialized && simulation.setIsRunning(!simulation.isRunning)}
onStep={simulation.step}
onReset={handleReset}
numWorkers={simulation.numWorkers}
currentEpoch={simulation.currentEpoch}
/>
</div>
<div className="config-row"><ConfigPanel config={config} onChange={setConfig} /></div>
</div>
<main className="content">
<section className="visualization-section">
<div className="viz-header">
<h2>Soup Sample</h2>
<ToggleButton onClick={() => {setViewMode(cycleViewMode); bumpViewKey(); }}title={viewMode === '2d' ? '2D grid view' : '1D line view'}>{viewMode.toUpperCase()}</ToggleButton>
<ToggleButton onClick={() => { setDesktopGridSize(s => s === 8 ? 16 : 8); bumpViewKey(); }} title={`Sample of ${desktopGridSize ** 2} programs`} className="grid-size-toggle">{desktopGridSize ** 2}</ToggleButton>
<ToggleButton onClick={() => { setColorMode(cycleColorMode); bumpViewKey(); }} title={colorMode === 'raw' ? 'Bytes – Color by raw byte value (code + data)' : 'Codes – Color by instruction type (junk folded into gray)'}>{colorMode === 'instructions' ? 'Codes' : 'Bytes'}</ToggleButton>
</div>
{simulation.programsSnapshot && (
<ProgramGrid
programs={simulation.programsSnapshot.programs}
updateKey={simulation.visualUpdate + viewModeUpdateKey}
mode={viewMode}
programLength={CONFIG.PROGRAM_LENGTH}
gridSize={gridSize}
colorMode={colorMode}
/>
)}
<InstructionKey colorMode={colorMode} />
<DescriptionToggle
label="What am I looking at?"
isOpen={showVisualizationDescription}
onToggle={() => setShowVisualizationDescription(!showVisualizationDescription)}
>
<p>
Each cell can have 1 of 256 values: 10 are instructions, the rest are data/junk. These cells form a
program of length {CONFIG.PROGRAM_LENGTH}, shown here as a {viewMode === '2d' ? `${gridDims.rows}×${gridDims.cols} grid` : 'row of cells'}.
The {config.numPrograms.toLocaleString()} programs make up the soup; here we show a {gridSize}×{gridSize} sample of them
as a mosaic. When life takes hold, the mosaic reflects the colors and patterns of the dominant programs.
</p>
</DescriptionToggle>
</section>
{simulation.history.epochs.length > 0 && (
<section className="metrics-section">
<h2>Signs of Life</h2>
<h3 className="chart-label">Dominant Programs</h3>
{simulation.topPrograms && simulation.topProgramFractions && (
<TopProgramsView
programs={simulation.topPrograms}
fractions={simulation.topProgramFractions}
viewMode={viewMode}
programLength={CONFIG.PROGRAM_LENGTH}
colorMode={colorMode}
/>
)}
<div className="chart-container">
<LineChart
data={simulation.history.programEntropy}
width={400}
height={100}
color="var(--color-entropy)"
label="Program Entropy"
/>
{simulation.currentMetrics && (
<div className="metric-value">
{simulation.currentMetrics.programEntropy.toFixed(3)}
<span className="metric-supplemental"> (w/ {simulation.currentMetrics.distinctPrograms} distinct programs)</span>
</div>
)}
</div>
<DescriptionToggle
label="How do we measure life?"
isOpen={showDescription}
onToggle={() => setShowDescription(!showDescription)}
>
<p>
At the start, we have a dead soup of random programs: there are almost as many distinct programs as total programs,
so 2^entropy is close to the population size. When a replicating program emerges, it and its variants quickly
take over the soup, dramatically reducing the program entropy.
<br/><br/>
With 131072 programs, life emerges after 16,000 epochs in 40% of simulations. For this demo we chose starting conditions
such that with the default parameters you will see life emerge after 7600 epochs.
</p>
</DescriptionToggle>
</section>
)}
</main>
</div>
);
}
const UnsupportedBrowser = () => (
<div className="app">
<div style={{ margin: 'auto', maxWidth: 480, padding: '4rem 2rem', textAlign: 'center' }}>
<h2 style={{ marginBottom: '1rem' }}>🧬 Primordial Soup Sim</h2>
<p style={{ color: 'var(--color-text-secondary)', lineHeight: 1.8 }}>
This simulation requires shared memory to parallelize the workload.
You may be using an in-app browser that prohibits this, or an older browser that doesn't support this.
</p>
</div>
</div>
);
const container = document.getElementById('root');
const root = createRoot(container!);
try {
new SharedArrayBuffer(1);
root.render(<App />);
} catch {
root.render(<UnsupportedBrowser />);
}