Primordial Soup

A "primordial soup" simulation, as explained in the paper "Computational Life: How Well-formed, Self-replicating Programs Emerge from Simple Interaction.

code.tsx

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 />);
}

Markdown source · More bulbs by samples · Typebulb home