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