afterlife: the game of life, after hours, ported to the web - #3
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.pyandlife_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 toaethera/static/afterlife/.src/engine/holdsInfiniteLife(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 fromlife.pyrather than retyped.src/music/holdsLifeMusicEngine, 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.aethera/api/afterlife.pyandtemplates/afterlife/viewer.htmladd the route and the page.urls.txtnow list/afterlife.SITESentry setstune: True.screens/afterlife.tsruns afterlife's own engine and renderer on a 64×24 terminal. If this browser has a saved universe, the screen adopts it.Changes for the web
?) that runs every key, for phones.qgoes back to the previous page. Inside oikos it stays put, and the ticker says so.ddownloadssnapshots.jsonl, andlife_stats.csvdownloads from the keys panel.Verification
afterlife/scripts/parity.{py,ts}replayslife.pyfrom 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_musicmatches 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 newtests/test_afterlife.py(the page, the committed bundle, the sitemap entry, the framing headers)./afterlife(splash, running, stats, haunted, keys, resume, phone) and of oikos (the room,?contact).Worth a look
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