import { useEffect, useMemo, useReducer, useRef, useState } from 'react'; import { ready, ink, fit } from '../../lib/mrly.js'; import { mount, Page, Group, Pick, Slider, Text, Check, Btn, Stats, Stat, Note } from '../../lib/app.jsx'; import { Grid, Sketch } from '../../lib/draw.jsx'; import { Picker, useSeeds } from '../../lib/select.jsx'; import { useQuery } from '../../lib/query.js'; const m = await ready(); const FLAT = 96; const LINE = 320; const ROWS = 200; const LIMIT = 512; const TRACE = 320; const FIRST = { dim: 2, code: '7', side: 3, level: 1, birth: '3', survive: '23', wrap: true, seed: 'soup', density: 0.3, bseq: '', sseq: '' }; const NAMES = [...m.life_sequences()]; const SEEDS = [['soup', 'soup'], ['single', 'one cell'], ['blank', 'blank']]; const PRESETS = [ ['Conway', { dim: 2, code: '7', side: 3, level: 1, birth: '3', survive: '23', bseq: '', sseq: '' }], ['Cantor-Life', { dim: 1, code: '1', side: 3, level: 3, birth: '3', survive: '23', bseq: '', sseq: '' }], ['Menger row', { dim: 1, code: '3', side: 3, level: 2, birth: '3', survive: '23', bseq: '', sseq: '' }], ['nine-cell XOR', { dim: 2, code: '15', side: 3, level: 1, birth: '1357', survive: '02468', bseq: '', sseq: '' }], ['rule 150', { dim: 1, code: '1', side: 3, level: 1, birth: '1', survive: '02', bseq: '', sseq: '' }], ]; const attempt = (fn) => { try { return { read: fn(), error: null }; } catch (error) { return { read: null, error }; } }; function parse(text) { const clean = String(text).trim(); const parts = /[^0-9]/.test(clean) ? clean.split(/[^0-9]+/) : [...clean]; const seen = new Set(); for (const part of parts) if (part !== '') seen.add(+part); return Uint32Array.from([...seen].sort((a, b) => a - b)); } function counts(list, sequence) { return sequence || `[${[...list].join(' ')}]`; } const alive = (types) => types.reduce((a, b) => a + b, 0); function ruleName(birth, survive, bseq, sseq, wrap) { return `rule birth ${counts(birth, bseq)}, survive ${counts(survive, sseq)}${wrap ? ', wrap' : ''}`; } // THE WORLD function sow(dim, kind, density, tap) { const width = dim === 2 ? FLAT : LINE; const height = dim === 2 ? FLAT : 1; if (kind === 'soup') return m.life_noise(width, height, density, tap || 1); const types = new Uint8Array(width * height); if (kind === 'single') types[dim === 2 ? (height >> 1) * width + (width >> 1) : width >> 1] = 1; return types; } function born(dim, kind, density, tap) { const types = sow(dim, kind, density, tap); const sheet = dim === 2 ? null : new Uint8Array(LINE * ROWS); if (sheet) sheet.set(types, 0); return { types, sheet, used: 1, pops: [alive(types)] }; } function advance(world, dim, next) { world.types = next; if (dim === 1) { if (world.used < ROWS) { world.sheet.set(next, world.used * LINE); world.used += 1; } else { world.sheet.copyWithin(0, LINE); world.sheet.set(next, (ROWS - 1) * LINE); } } world.pops.push(alive(next)); if (world.pops.length > TRACE) world.pops.shift(); } // THE TRACE const trace = (pops) => (canvas) => { const [ctx, w, h] = fit(canvas, 120); ctx.clearRect(0, 0, w, h); const top = Math.max(1, ...pops); ctx.strokeStyle = ink.blue; ctx.lineWidth = 1.5; ctx.beginPath(); pops.forEach((p, i) => { const x = 8 + (pops.length < 2 ? 0 : (i / (pops.length - 1)) * (w - 16)); const y = h - 8 - (p / top) * (h - 20); if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.stroke(); ctx.fillStyle = ink.dim; ctx.font = '11px ui-monospace, monospace'; ctx.fillText(String(top), 8, 12); }; function App() { const taps = useSeeds(); const [pick, set] = useQuery(FIRST); const [playing, setPlaying] = useState(false); const [age, setAge] = useState(0); const [verdict, setVerdict] = useState(null); const [error, setError] = useState(null); const [, force] = useReducer((x) => x + 1, 0); const dim = pick.dim === 1 ? 1 : 2; const cap = Math.min(3, m.level_cap(pick.side, 1, dim === 2 ? 27 : 81)); const level = Math.max(1, Math.min(pick.level, cap)); const mask = useMemo(() => attempt(() => m.life_mask(dim, pick.code.trim(), pick.side, level)), [dim, pick.code, pick.side, level]); const index = useMemo(() => (mask.read ? attempt(() => m.life_mask_index(mask.read.types, mask.read.width, mask.read.height)) : { read: null, error: null }), [mask.read]); const budget = mask.read ? alive(mask.read.types) : 8; const world = useRef(null); world.current ??= born(dim, pick.seed, pick.density, taps.get()); const birth = parse(pick.birth); const survive = parse(pick.survive); const name = ruleName(birth, survive, pick.bseq, pick.sseq, pick.wrap); const maskName = attempt(() => m.name_of(pick.code.trim(), dim, 2)).read; const reseed = (patch = {}) => { const next = { ...pick, ...patch }; set(patch); setPlaying(false); setAge(0); setVerdict(null); setError(null); world.current = born(next.dim === 1 ? 1 : 2, next.seed, next.density, taps.get()); force(); }; const step = () => { if (!mask.read) return; try { const width = dim === 2 ? FLAT : LINE; const height = dim === 2 ? FLAT : 1; const next = m.life_next_masked(world.current.types, width, height, birth, survive, mask.read.types, mask.read.width, mask.read.height, pick.wrap); advance(world.current, dim, next); setAge((g) => g + 1); setError(null); force(); } catch (thrown) { setPlaying(false); setError(thrown); } }; useEffect(() => { if (!playing) return; const timer = setInterval(step, 60); return () => clearInterval(timer); }, [playing, dim, pick.code, pick.side, level, pick.birth, pick.survive, pick.wrap, mask.read]); const fate = () => { if (!mask.read) return; try { const width = dim === 2 ? FLAT : LINE; const height = dim === 2 ? FLAT : 1; const run = JSON.parse(m.life_run_masked(world.current.types, width, height, birth, survive, mask.read.types, mask.read.width, mask.read.height, pick.wrap, LIMIT)); setVerdict(run); setError(null); } catch (thrown) { setError(thrown); } }; const toggle = (event) => { const box = event.target.getBoundingClientRect(); const width = dim === 2 ? FLAT : LINE; const x = Math.floor((event.clientX - box.left) / box.width * width); if (dim === 2) { const y = Math.floor((event.clientY - box.top) / box.height * FLAT); world.current.types[y * FLAT + x] ^= 1; } else { world.current.types[x] ^= 1; world.current.sheet.set(world.current.types, (world.current.used - 1) * LINE); } setVerdict(null); force(); }; const sequenced = (which, key, sequence) => { const patch = { [key]: sequence }; if (sequence) patch[which] = [...m.life_sequence(sequence, budget)].join(' '); set(patch); }; const preset = (values) => { const patch = { ...values, level: values.level ?? 1 }; reseed(patch); }; const wears = (values) => Object.entries(values).every(([key, value]) => (key === 'dim' ? dim : key === 'level' ? level : pick[key]) === value); const controls = ( <> setPlaying(!playing)}>{playing ? 'Pause' : 'Play'} Step Run to fate reseed()}>Reset set({ wrap: v })} /> reseed({ dim: +v, code: +v === 1 ? '1' : '7' })} /> set(patch)} /> set({ side: +v, level: Math.min(level, Math.min(3, m.level_cap(+v, 1, dim === 2 ? 27 : 81))) })} /> [i + 1, i + 1])} onChange={(v) => set({ level: +v })} /> sequenced('birth', 'bseq', v)} /> set({ birth: v, bseq: '' })} /> sequenced('survive', 'sseq', v)} /> set({ survive: v, sseq: '' })} /> reseed({ seed: v })} /> reseed({ seed: 'soup', density: v })} /> { taps.next(); reseed({ seed: 'soup' }); }}>Randomize {PRESETS.map(([label, values]) => preset(values)}>{label})} ); const sheet = dim === 2 ? { width: FLAT, height: FLAT, types: world.current.types } : { width: LINE, height: world.current.used, types: world.current.sheet.subarray(0, world.current.used * LINE) }; const reading = index.read === null ? 'unread' : index.read === 0 ? 'index 0: the offsets span no lattice' : index.read === 1 ? 'index 1: one lattice, nothing splits' : `index ${index.read}: ${index.read} interleaved copies`; return ( The kind is LIFE, outer-totalistic: a dead cell is born when its live neighbour count is in the birth list, a live cell stays when its count is in the survival list, and the mask says which cells are neighbours. Conway is the level-1 carpet `bang dim 2, code 7` with its centre popped, drawn plain on the Life page; the one-dimensional two-state radius-one masks are the elementary rules on the Wolfram page. Menger-Life proper lives at dim 3 on the 20 offsets of `bang dim 3, code 23` and is not drawn here, so the Menger chip reads that tile one dimension down. The masks, the indices and every generation come out of the crates through wasm. The research page is automata.} controls={controls}>

the mask a design, centre popped

{mask.read && } {`${maskName ?? `code ${pick.code.trim()}`}, side ${pick.side}, level ${level}`} {budget} {reading}

The index is the decoupling index: the sublattice the mask offsets and the centre generate inside the whole lattice. An index above one means the board never mixes - it is that many interleaved copies of the same automaton, each blind to the others.

the rule kind LIFE on this mask

{name} {[...birth].join(', ') || 'none'} {[...survive].join(', ') || 'none'}

Counts run from 0 to {budget}, the cell count of the mask. Type them as digits, as `1 3 5 7`, or draw them from a named sequence cut at the budget; a sequence side spells itself in the name, so `rule birth primes, survive [2 3]` is a rule and not a description of one.

{dim === 2 ? 'the board' : 'the space-time diagram'} {dim === 2 ? `${FLAT} by ${FLAT}` : `${LINE} cells, newest row at the bottom`}

{age} {world.current.pops[world.current.pops.length - 1]} {verdict && {verdict.fate} after {verdict.count}{verdict.loop ? `, loop ${verdict.loop}` : ''}}

the population the last {TRACE} generations

Run to fate replays the board for at most {LIMIT} generations and reports death, a frozen board, a loop with its period, or a timeout. Every value on this page is a link: the dimension, the code, the side, the level, the two count lists, the wrap and the seed all live in the address bar.

); } mount();