Skip to content

afterlife: the game of life, after hours, ported to the web - #3

Merged
LuxiaSL merged 7 commits into
mainfrom
ccr-2dc1ba05-8h35ex
Sep 29, 2026
Merged

LuxiaSL merged 7 commits into
mainfrom
ccr-2dc1ba05-8h35ex

Conversation

@LuxiaSL

@LuxiaSL LuxiaSL commented Sep 29, 2026

Copy link
Copy Markdown
Owner

Ports afterlife, the terminal screensaver, to /afterlife, and gives it a tape on channel 10 in oikos.

It's the same program, not an imitation. The engine and the music are method-for-method TypeScript ports of life.py and life_music.py. The page is drawn as a terminal: half-block cells in the same xterm-256 colours curses used, and a status line where every glyph has its own column.

What's here

  • afterlife/ is a new Vite + TS project, like syrinx and apeiron. The built bundle is committed to aethera/static/afterlife/.
    • src/engine/ holds InfiniteLife (the infinity engine, the dramaturge, camera, auto-zoom, auto-focus, time dilation, haunted mode), the census (scipy's dilation/label/find_objects, written out), the ticker, and every musing, generated from life.py rather than retyped.
    • src/music/ holds LifeMusicEngine, running in an AudioWorklet that renders the same 2048-frame buffers PyAudio asked for. esbuild bundles the worklet and it's inlined into the page, so there's no second file.
    • src/term/ is the terminal renderer. oikos shares it.
  • Server side:
    • aethera/api/afterlife.py and templates/afterlife/viewer.html add the route and the page.
    • The sitemap and urls.txt now list /afterlife.
    • The oikos SITES entry sets tune: True.
  • oikos:
    • screens/afterlife.ts runs afterlife's own engine and renderer on a 64×24 terminal. If this browser has a saved universe, the screen adopts it.
    • Also added: a placement (angle 78, channel 10), shelf neighbours, a pane note and a status line.

Changes for the web

  • Persistence: the universe is saved in localStorage when the page is hidden or closed, and every 5000 generations. The next visit resumes it with "the universe remembers generation N".
  • Opening screen: a splash with the ASCII logo, since browsers only start sound after a gesture.
  • Controls:
    • A keys panel (?) that runs every key, for phones.
    • Drag to draw life, right-drag or shift-drag to pan, scroll to zoom. On touch: tap, drag, pinch.
    • A two-line status on narrow screens, so the ticker keeps its room.
    • q goes back to the previous page. Inside oikos it stays put, and the ticker says so.
    • d downloads snapshots.jsonl, and life_stats.csv downloads from the keys panel.
  • Bug fix: two display caches are now invalidated on pan, zoom and toggle. In the terminal, changes made while paused didn't show until you unpaused.

Verification

  • afterlife/scripts/parity.{py,ts} replays life.py from a seeded genesis, with the injections taken out because their dice can't be shared. Everything matches exactly: every step's population, telemetry, camera, zoom, mood and tempo, then the final ages, the display maps at all five zooms, the census, auto-focus, a pan/zoom, and haunted mode.
  • parity_music matches the original engine sample for sample (worst |Δ| 1.8e-7), including the cadence, the style crossfades and the noise bursts.
  • uv run pytest -q: 96 passed, including the new tests/test_afterlife.py (the page, the committed bundle, the sitemap entry, the framing headers).
  • Screenshots: headless shots of /afterlife (splash, running, stats, haunted, keys, resume, phone) and of oikos (the room, ?contact).
  • Performance: about 7 ms per frame at 1920×1080 scale, 15 ms in haunted mode, against a 50 ms frame.

Worth a look

  • The music engine matches the Python samples, but no one has listened to it in a browser yet.
  • The oikos placement is beside syrinx. Move it if you'd rather it stood elsewhere.
  • oikos bundles afterlife/src/{engine,term}, so a change there means rebuilding both. The README says so.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VFkU2eSQ39nbuUTQsiVvFK


Generated by Claude Code

claude and others added 7 commits September 29, 2026 20:49
afterlife (LuxiaSL/afterlife), the terminal screensaver, now runs at
/afterlife. It is the same program, not an imitation. The engine and the
music are method-for-method ports of life.py and life_music.py. The page is
drawn as a terminal: half-block cells in the same xterm-256 colours curses
used, and a status line where every glyph has its own column.

- src/engine: InfiniteLife (step, the infinity engine, the dramaturge,
  camera, auto-zoom, auto-focus, time dilation, haunted mode), the census
  (the scipy dilation/label/find_objects it needed, written out), the
  ticker, and every musing, generated from life.py rather than retyped.
- src/music: LifeMusicEngine in an AudioWorklet, rendering the same
  2048-frame buffers PyAudio asked for. The worklet is bundled by esbuild
  and inlined, so there's no second file.
- scripts/parity{.py,.ts} replay life.py from a seeded genesis. Every step's
  population, telemetry, camera, zoom, mood and tempo match exactly, and so
  do the final ages, the display maps at all five zooms, the census,
  auto-focus and haunted mode. parity_music matches the original engine
  sample for sample (worst |Δ| 1.8e-7), including the cadence, the style
  crossfade and the noise bursts.
- The universe persists in localStorage. It is saved on the way out and
  every 5000 generations, and resumed with "the universe remembers
  generation N".
- Added for the web: a splash to start the sound, a keys panel that runs
  every key (for phones), drag to draw life, pan and zoom with the pointer
  and touch, and a two-line status on narrow screens so the ticker keeps
  its room.

oikos gets a tape for it on channel 10. The screen runs afterlife's own
engine and renderer, imported from ../afterlife/src, on a 64×24 terminal,
and adopts your saved universe if this browser has one. It also has a
placement at the end of the floor arc, a pane note and a status line.

/afterlife is added to the sitemap and urls.txt, with tests for the page,
the committed bundle and the framing headers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VFkU2eSQ39nbuUTQsiVvFK
…can keep its size

- persist: localStorage is read inside the try, so a browser that blocks
  site data gets a fresh universe instead of a dead page
- persist: ghosts deeper than GHOST_FRAMES are not written (adopt retires
  them anyway); haunted mode no longer blows the 1.5M cap and fails silently
- persist: generation must be a safe integer
- life: the constructor takes a WorldFloor, so a resize can keep the old
  world and a small screen can run a small one

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d screens

- a resize keeps the old world as the floor (no more cropping) and carries
  the session across: pause, speed, zoom, camera, modes, ticker; a load
  keeps the saved world's size; URL-bar jitter no longer rebuilds
- an oikos screen shows the save but never writes it
- a later universe saved by another tab isn't overwritten
- the loop stops at a 12 ms budget and drops the backlog
- a failed music start closes its context and can be retried; iOS
  'interrupted' resumes on a gesture; playback audio session
- key repeat only for repeatable keys; ? and Escape don't start music;
  Enter/Space on the splash enters; touch splash copy; csv isn't a key;
  recording is capped and says when it caught nothing; drags stay on grid;
  a third finger doesn't paint; pixel-ratio changes refit; reduced motion

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nces it for good

Every 16th quantum the worklet renders a 2048-frame block, and each block
made ~45 fresh typed arrays (~474 KiB, ~10 MiB/s on the audio thread) where
numpy had made fresh arrays. The engine and each voice now own scratch
buffers sized to a block and reuse them: zero typed arrays per block, with
the element types (float64 phases, float32 signals) and so every rounding
unchanged — bit-identical to the previous port over 16M samples.

One NaN in a snapshot's population, density, spread or pop_delta used to
latch the smoothed volumes and filters to NaN for the rest of the evening.
The worklet now plays a NaN block as silence and the engine puts its
non-finite state back; it also warns if the context isn't at 44.1 kHz.

parity-music now also drives the original's PyAudio callback (volume, mute,
tanh) through a 620-buffer evening, answered by renderBlock().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The 64×23 tube stepped life.py's 400×800 floor (3.6 ms a generation) up to
30 times a second; it now keeps a world 5× its view (1.4 ms) and steps at
most 15 generations a second however fast it's drawn. A bigger save is
cropped to its centre in memory only; the screen never saves.

readUniverse no longer JSON.parses the whole save (up to 1.5 MB) for three
numbers: it reads them from the head before the cells blob, skips a save it
has already seen, and hears new saves through the storage event, with a
60 s poll as the fallback.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
life.ts
- Python's -v // k is (-v) // k: genesis, edge and garden ranges now use
  fdiv(-v, k) for their low bound, as life.py does
- genesis oscillators draw y, x, then the pattern, in life.py's order
- percentileInt interpolates from the nearer end, as numpy's _lerp does
- auto-focus's hotspot reproduces scipy's uniform_filter to the bit
  (separable running sums), so exact ties break where scipy's rounding
  breaks them (previously ~9% of tie-heavy worlds focused elsewhere)
- note on findQuietSpot: argpartition's tie order is numpy's detail

parity
- 22 cases: 16 still (nine seeds, tiny/odd/1080p sizes, torus seams,
  haunted, every key, resizes via adopt), 2 auto-focus probe sets, and
  4 live cases whose dice come from one mulberry32 stream shared with
  life.py (genesis, injections, a gen-5000 garden, on-cue provokes and
  collisions, draw count checked every step)
- per-step age checksums, smoothed/activity sums, display maps
- parity.py refuses to run without scipy; README says uv run --with scipy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LuxiaSL
LuxiaSL merged commit 78c4c90 into main Sep 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants