17

3D Game of Life: Bays 5766

drag to orbit ยท click to reseed

Conway's Game of Life lifted into three dimensions on a Moore-neighborhood lattice (each cell has 26 neighbors, not 8). The rule shown is Carter Bays's **5766** โ€” discovered by Bays in 1987 โ€” written as : a dead cell becomes alive iff it has exactly 6 alive neighbors, and an alive cell survives iff it has 5, 6, or 7. Bays catalogued this rule by exhaustively searching for 3D Life-like dynamics, where 'Life-like' means the rule supports both gliders and finite oscillators while resisting both immediate extinction and explosive filling. Most random 3D rules fail one of those tests within a few generations. Cells are rendered as an `InstancedMesh` of cubes, colored by position; the CA steps on a 150 ms wall-clock cadence (decoupled from the render loop) and auto-reseeds a small dense soup whenever the population dies, gridlocks, or runs away.

idle
177 lines ยท three
view source
// 3D Game of Life โ€” Bays's 5766 rule (Carter Bays, 1987).
//
// Rule notation B/S = 6/5,6,7 on a 26-cell Moore neighborhood:
//   - a dead cell with exactly 6 alive neighbors becomes alive
//   - an alive cell with 5, 6, or 7 alive neighbors survives
// This is one of the very few 3D Life rules that admits long-lived
// dynamics โ€” most random rules either die instantly or explode to a
// stable hash. Bays found it by exhaustive search.
//
// Rendering: a single InstancedMesh of cubes, one instance per grid cell.
// Dead cells get scaled to zero so they disappear from rasterization.
// Cells inherit color from their world position so the cube has a soft
// gradient texture that makes oscillators legible.

const N = 22;                         // grid side length (22ยณ โ‰ˆ 10.6k cells)
const STEP_MS = 150;                  // CA update period
const BIRTH = new Set([6]);
const SURVIVE = new Set([5, 6, 7]);
const CELL = 1.0;                     // world units per cell
const GAP = 0.12;                     // visual gap between cubes

let grid, next;                       // Uint8Array of size N*N*N
let mesh;                             // THREE.InstancedMesh
let dummy;                            // reusable Object3D for setMatrixAt
let colorAttr;                        // for per-instance colors
let stepAcc = 0;                      // ms accumulator for CA steps
let popCount = 0;
let stableTicks = 0;                  // consecutive steps with no change
let lastPop = -1;
let ambient, dirLight;

let userYaw = 0.6, userPitch = 0.35;
let isDragging = false;
let lastMouseX = 0, lastMouseY = 0;
let idleTime = 0;

function idx(x, y, z) { return x + N * (y + N * z); }

function countNeighbors(g, x, y, z) {
  let c = 0;
  for (let dz = -1; dz <= 1; dz++) {
    const zz = z + dz; if (zz < 0 || zz >= N) continue;
    for (let dy = -1; dy <= 1; dy++) {
      const yy = y + dy; if (yy < 0 || yy >= N) continue;
      for (let dx = -1; dx <= 1; dx++) {
        if (dx === 0 && dy === 0 && dz === 0) continue;
        const xx = x + dx; if (xx < 0 || xx >= N) continue;
        c += g[idx(xx, yy, zz)];
      }
    }
  }
  return c;
}

function stepCA() {
  let alive = 0;
  for (let z = 0; z < N; z++) {
    for (let y = 0; y < N; y++) {
      for (let x = 0; x < N; x++) {
        const n = countNeighbors(grid, x, y, z);
        const i = idx(x, y, z);
        const was = grid[i];
        let now;
        if (was) now = SURVIVE.has(n) ? 1 : 0;
        else     now = BIRTH.has(n)   ? 1 : 0;
        next[i] = now;
        if (now) alive++;
      }
    }
  }
  const tmp = grid; grid = next; next = tmp;
  if (alive === lastPop) stableTicks++; else stableTicks = 0;
  lastPop = alive;
  popCount = alive;
}

function seedRandomSoup(densityFrac, region) {
  // region: optional {x0,y0,z0,x1,y1,z1}; if null, full grid
  const r = region || { x0: 0, y0: 0, z0: 0, x1: N, y1: N, z1: N };
  for (let z = r.z0; z < r.z1; z++) {
    for (let y = r.y0; y < r.y1; y++) {
      for (let x = r.x0; x < r.x1; x++) {
        grid[idx(x, y, z)] = Math.random() < densityFrac ? 1 : 0;
      }
    }
  }
}

function fullReseed() {
  grid.fill(0);
  // Seed a chunky cluster in the middle โ€” Bays 5766 likes ~30-40%
  // density in a ~10ยณ pocket; uniform random over the full grid
  // tends to either die or run away.
  const half = (N / 2) | 0;
  const r = 5;
  seedRandomSoup(0.35, {
    x0: half - r, y0: half - r, z0: half - r,
    x1: half + r, y1: half + r, z1: half + r,
  });
  stableTicks = 0;
  lastPop = -1;
}

function rebuildInstances() {
  // Place every cell once; visibility is encoded by scale.
  const offset = -(N - 1) * CELL / 2;
  const color = new THREE.Color();
  for (let z = 0; z < N; z++) {
    for (let y = 0; y < N; y++) {
      for (let x = 0; x < N; x++) {
        const i = idx(x, y, z);
        dummy.position.set(
          offset + x * CELL,
          offset + y * CELL,
          offset + z * CELL,
        );
        const alive = grid[i];
        const s = alive ? (CELL - GAP) : 0.0001;
        dummy.scale.set(s, s, s);
        dummy.updateMatrix();
        mesh.setMatrixAt(i, dummy.matrix);
        // Hue blends through position so the lattice reads as 3D.
        const h = ((x + y + z) / (3 * N)) % 1;
        color.setHSL(h, 0.7, 0.55);
        mesh.setColorAt(i, color);
      }
    }
  }
  mesh.instanceMatrix.needsUpdate = true;
  if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
}

function init({ scene, camera, renderer, width, height }) {
  renderer.setClearColor(0x06080d, 1);

  const total = N * N * N;
  grid = new Uint8Array(total);
  next = new Uint8Array(total);
  fullReseed();

  ambient = new THREE.AmbientLight(0xffffff, 0.45);
  scene.add(ambient);
  dirLight = new THREE.DirectionalLight(0xffffff, 0.85);
  dirLight.position.set(20, 30, 25);
  scene.add(dirLight);
  const fill = new THREE.DirectionalLight(0x88aaff, 0.35);
  fill.position.set(-25, -10, -20);
  scene.add(fill);

  const geo = new THREE.BoxGeometry(1, 1, 1);
  const mat = new THREE.MeshLambertMaterial({ color: 0xffffff });
  mesh = new THREE.InstancedMesh(geo, mat, total);
  mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
  // Per-instance color buffer.
  colorAttr = new THREE.InstancedBufferAttribute(new Float32Array(total * 3), 3);
  mesh.instanceColor = colorAttr;
  dummy = new THREE.Object3D();
  scene.add(mesh);

  rebuildInstances();

  const dist = N * 1.9;
  camera.position.set(dist * 0.6, dist * 0.4, dist * 0.7);
  camera.lookAt(0, 0, 0);

  return { scene, camera };
}

function tick({ dt, scene, camera, renderer, width, height, input }) {
  // Input: drag = orbit, click = reseed.
  const clicks = input.consumeClicks();
  if (clicks > 0) fullReseed();

  if (input.mouseDown) {
    if (!isDragging) {
      isDragging = true;
      lastMouseX = input.mouseX;
      lastMouseY = input.mouseY;
      idleTime = 0;
    } else {
      const dx = input.mouseX - lastMouseX;
      const dy = input.mouseY - lastMouseY;
      userYaw -= dx * 0.006;
      userPitch -= dy * 0.006;
      userPitch = Math.max(-1.2, Math.min(1.2, userPitch));
      lastMouseX = input.mouseX;
      lastMouseY = input.mouseY;
      idleTime = 0;
    }
  } else {
    isDragging = false;
    idleTime += dt;
    if (idleTime > 0.4) userYaw += dt * 0.18;
  }

  const r = N * 2.0;
  const cy = Math.cos(userYaw), sy = Math.sin(userYaw);
  const cp = Math.cos(userPitch), sp = Math.sin(userPitch);
  camera.position.set(r * cp * sy, r * sp, r * cp * cy);
  camera.lookAt(0, 0, 0);
  camera.aspect = width / height;
  camera.updateProjectionMatrix();
  renderer.setSize(width, height, false);

  // CA step on a fixed wall-clock cadence, not per frame.
  stepAcc += dt * 1000;
  if (stepAcc >= STEP_MS) {
    stepAcc = 0;
    stepCA();
    // Auto-reseed on death, gridlock, or runaway fill.
    const total = N * N * N;
    if (popCount === 0
        || popCount > total * 0.6
        || stableTicks > 30) {
      fullReseed();
    }
    rebuildInstances();
  }

  renderer.render(scene, camera);
}

Comments (0)

Log in to comment.