index.jsx

5.3 kB · jsx · 133 lines

1import { useEffect, useReducer, useRef, useState } from 'react';2import { ready, ink } from '../../lib/mrly.js';3import { mount, Page, Group, Pick, Slider, Check, Btn, Stats, Stat, Note } from '../../lib/app.jsx';4import { Grid } from '../../lib/draw.jsx';5import { useSeeds } from '../../lib/select.jsx';6import { useQuery } from '../../lib/query.js';78const m = await ready();910const W = 96;11const H = 96;12const LIMIT = 512;13const BIRTH = Uint32Array.from([3]);14const SURVIVE = Uint32Array.from([2, 3]);1516const FIRST = { seed: 'soup', density: 0.3, wrap: true };1718const SEEDS = [['soup', 'soup'], ['glider', 'glider'], ['pentomino', 'R-pentomino'], ['blinker', 'blinker'], ['block', 'block'], ['blank', 'blank']];1920const SHAPES = {21  glider: [[1, 0], [2, 1], [0, 2], [1, 2], [2, 2]],22  pentomino: [[1, 0], [2, 0], [0, 1], [1, 1], [1, 2]],23  blinker: [[0, 0], [1, 0], [2, 0]],24  block: [[0, 0], [1, 0], [0, 1], [1, 1]],25};2627function sow(kind, density, tap) {28  if (kind === 'soup') return m.life_noise(W, H, density, tap || 1);29  const types = new Uint8Array(W * H);30  const cells = SHAPES[kind];31  if (!cells) return types;32  const ox = (W >> 1) - 1;33  const oy = (H >> 1) - 1;34  for (const [x, y] of cells) types[(oy + y) * W + ox + x] = 1;35  return types;36}3738const alive = (types) => types.reduce((a, b) => a + b, 0);3940function App() {41  const taps = useSeeds();42  const [pick, set] = useQuery(FIRST);43  const [playing, setPlaying] = useState(false);44  const [age, setAge] = useState(0);45  const [verdict, setVerdict] = useState(null);46  const [error, setError] = useState(null);47  const [, force] = useReducer((x) => x + 1, 0);48  const board = useRef(null);49  board.current ??= sow(pick.seed, pick.density, taps.get());5051  const reseed = (patch = {}) => {52    const next = { ...pick, ...patch };53    set(patch);54    setPlaying(false);55    setAge(0);56    setVerdict(null);57    setError(null);58    board.current = sow(next.seed, next.density, taps.get());59    force();60  };6162  const step = () => {63    try {64      board.current = m.life_next(board.current, W, H, BIRTH, SURVIVE, pick.wrap);65      setAge((g) => g + 1);66      setError(null);67    } catch (thrown) {68      setPlaying(false);69      setError(thrown);70    }71  };7273  useEffect(() => {74    if (!playing) return;75    const timer = setInterval(step, 40);76    return () => clearInterval(timer);77  }, [playing, pick.wrap]);7879  const fate = () => {80    try {81      const run = JSON.parse(m.life_run(board.current, W, H, BIRTH, SURVIVE, pick.wrap, LIMIT));82      setVerdict(run);83      setError(null);84    } catch (thrown) {85      setError(thrown);86    }87  };8889  const toggle = (event) => {90    const box = event.target.getBoundingClientRect();91    const x = Math.floor((event.clientX - box.left) / box.width * W);92    const y = Math.floor((event.clientY - box.top) / box.height * H);93    board.current[y * W + x] ^= 1;94    setVerdict(null);95    force();96  };9798  const controls = (99    <>100      <Group name="Run">101        <Btn primary onClick={() => setPlaying(!playing)}>{playing ? 'Pause' : 'Play'}</Btn>102        <Btn onClick={step}>Step</Btn>103        <Btn onClick={fate}>Run to fate</Btn>104        <Btn onClick={() => reseed()}>Reset</Btn>105        <Check label="wrap" checked={pick.wrap} onChange={(v) => set({ wrap: v })} />106      </Group>107      <Group name="The seed">108        <Pick label="seed" value={pick.seed} options={SEEDS} onChange={(v) => reseed({ seed: v })} />109        <Slider label="density" value={pick.density} min={0.05} max={0.95} step={0.01} onChange={(v) => reseed({ seed: 'soup', density: v })} />110        <Btn onClick={() => { taps.next(); reseed({ seed: 'soup' }); }}>Randomize</Btn>111      </Group>112    </>113  );114115  return (116    <Page crumb="life" title="Conway's Life"117      sub="One rule on the eight cells around: a dead cell with exactly three live neighbours is born, a live cell with two or three stays, everything else dies. Drop a soup or a glider, then play it, step it, or run it to its fate."118      foot={<>The neighbourhood is not a hand-drawn ring: it is the side-3 carpet tile `bang dim 2, code 7` with its centre popped, the same eight offsets a design writes at level one. Every generation and every fate below is stepped in Rust through wasm and the page only draws. Life is one point of a much larger family - any mask, any birth and survival list, one or two dimensions - and that family is <a href="../mrlylife">mrlylife</a>.</>}119      controls={controls}>120      <Grid grid={{ width: W, height: H, types: board.current }} on={ink.green} style={{ maxWidth: 640 }} onClick={toggle} aria-label="The grid, click a cell to turn it on or off" />121      <Stats>122        <Stat label="rule">{`rule birth [3], survive [2 3]${pick.wrap ? ', wrap' : ''}`}</Stat>123        <Stat label="generation">{age}</Stat>124        <Stat label="population">{alive(board.current)}</Stat>125        {verdict && <Stat label="fate">{verdict.fate} after {verdict.count}{verdict.loop ? `, loop ${verdict.loop}` : ''}</Stat>}126      </Stats>127      <p className="sub">Click any cell to turn it on or off, then play from there. Run to fate replays the board from here for at most {LIMIT} generations and reports whether it dies, freezes, loops, or is still moving when the count runs out. The board is {W} by {H}; with wrap on it is a torus, with wrap off the outside is dead ground.</p>128      <Note error={error} />129    </Page>130  );131}132133mount(<App />);