From 20f657e45e6b3e3244adc257c882ccf20d65a767 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 22 Sep 2026 05:40:00 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20split=20into=20engine,=20ap?= =?UTF-8?q?ps=20and=20CLI=20packages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automate-Automata was one SvelteKit app with the simulation living inside a 612-line Svelte component. This splits it into a Bun workspace: an engine anyone can depend on, a UI layer, and three ways to run it — web, Android and the terminal. packages/complexity-automata (npm: complexity-automata) The engine, with no DOM, no timers and no dependencies. Rules parse from B3/S23 notation, rule numbers or names; the 18-bit rule number makes all 262,144 universes addressable. Adds toroidal wrapping, a seeded RNG so a universe is reproducible, typed-array cells and colours, stagnation detection, twelve demos, a pattern catalogue with fractal seeds, and universe save/load as Life RLE — JSON, a URL-safe share string, or a UniverseLibrary over any key/value store. packages/complexity-automata-cli (npm: complexity-automata-cli) Terminal animation in truecolor half-blocks, two grid rows per line. Demos, patterns, rule tours, keyboard control, saved universes in ~/.complexity-automata, and a --print mode that writes one frame to stdout for pipes and CI. packages/complexity-automata-svelte (npm: complexity-automata-svelte) The canvas view, the rule as eighteen switches, the demo picker, the intro screen and the saved-universe shelf. Painting goes through an offscreen buffer at one pixel per cell, so a redraw costs cells rather than screen pixels. apps/web SvelteKit shell around the component. Static, client-rendered, with ?demo=, ?intro=0, ?cell= and ?u= (shared universe) URL parameters. apps/android The same build wrapped by Capacitor: webDir points at ../web/build, so the app ships exactly what Cloudflare serves. Renamed to Complexity Automata, version 3.0.0. 86 tests across the three packages, plus a CI workflow that typechecks, tests, builds all three and verifies the Capacitor copy step. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DPssgQtTm138yEbfJm6Ham --- .cloudflare/public/_app/version.json | 1 - .cloudflare/public/logo.png | Bin 16838 -> 0 bytes .eslintrc.cjs | 20 - .github/workflows/ci.yml | 31 + .gitignore | 17 +- README.md | 101 +- android/capacitor.settings.gradle | 3 - apps/android/README.md | 50 + {android => apps/android/android}/.gitignore | 0 .../android/android}/.idea/.gitignore | 0 .../android/android}/.idea/compiler.xml | 0 .../.idea/deploymentTargetSelector.xml | 0 .../android/android}/.idea/migrations.xml | 0 .../android/android}/.idea/misc.xml | 0 .../android/android}/.idea/other.xml | 0 .../android/android}/app/.gitignore | 0 .../android/android}/app/build.gradle | 4 +- .../android}/app/capacitor.build.gradle | 0 .../android/android}/app/debug.keystore | Bin .../android}/app/debug/output-metadata.json | 0 .../android/android}/app/proguard-rules.pro | 0 .../myapp/ExampleInstrumentedTest.java | 0 .../android}/app/src/main/AndroidManifest.xml | 0 .../com/vtempest/automata/MainActivity.java | 0 .../main/res/drawable-land-hdpi/splash.png | Bin .../main/res/drawable-land-mdpi/splash.png | Bin .../main/res/drawable-land-xhdpi/splash.png | Bin .../main/res/drawable-land-xxhdpi/splash.png | Bin .../main/res/drawable-land-xxxhdpi/splash.png | Bin .../main/res/drawable-port-hdpi/splash.png | Bin .../main/res/drawable-port-mdpi/splash.png | Bin .../main/res/drawable-port-xhdpi/splash.png | Bin .../main/res/drawable-port-xxhdpi/splash.png | Bin .../main/res/drawable-port-xxxhdpi/splash.png | Bin .../drawable-v24/ic_launcher_foreground.xml | 0 .../res/drawable/ic_launcher_background.xml | 0 .../app/src/main/res/drawable/splash.png | Bin .../app/src/main/res/layout/activity_main.xml | 0 .../res/mipmap-anydpi-v26/ic_launcher.xml | 0 .../mipmap-anydpi-v26/ic_launcher_round.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../mipmap-hdpi/ic_launcher_foreground.png | Bin .../res/mipmap-hdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../mipmap-mdpi/ic_launcher_foreground.png | Bin .../res/mipmap-mdpi/ic_launcher_round.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../mipmap-xhdpi/ic_launcher_foreground.png | Bin .../res/mipmap-xhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin .../res/values/ic_launcher_background.xml | 0 .../app/src/main/res/values/strings.xml | 4 +- .../app/src/main/res/values/styles.xml | 0 .../app/src/main/res/xml/file_paths.xml | 0 .../getcapacitor/myapp/ExampleUnitTest.java | 0 .../android/android}/build.gradle | 0 .../android/android/capacitor.settings.gradle | 3 + .../android/android}/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.jar | Bin .../gradle/wrapper/gradle-wrapper.properties | 0 {android => apps/android/android}/gradlew | 0 {android => apps/android/android}/gradlew.bat | 0 .../android/android}/settings.gradle | 0 .../android/android}/variables.gradle | 0 apps/android/capacitor.config.ts | 23 + apps/android/package.json | 22 + .../resources}/android/icon-background.png | Bin .../resources}/android/icon-foreground.png | Bin .../android/resources}/icon.png | Bin .../resources}/screenshots/logo-512.png | Bin .../android/resources}/screenshots/s1.png | Bin .../android/resources}/screenshots/s2.png | Bin .../android/resources}/screenshots/s2.webp | Bin .../android/resources}/screenshots/s3.png | Bin .../resources}/screenshots/unnamed.webp | Bin .../android/resources}/splash-dark.png | Bin .../android/resources}/splash.png | Bin apps/web/README.md | 31 + apps/web/package.json | 32 + apps/web/src/app.css | 33 + {src => apps/web/src}/app.d.ts | 0 apps/web/src/app.html | 23 + apps/web/src/routes/+layout.js | 4 + {src => apps/web/src}/routes/+layout.svelte | 4 +- apps/web/src/routes/+page.svelte | 14 + {static => apps/web/static}/logo.png | Bin apps/web/svelte.config.js | 19 + apps/web/tsconfig.json | 14 + vite.config.js => apps/web/vite.config.js | 0 apps/web/wrangler.toml | 7 + bun.lock | 867 ++++++++++++++++++ bun.lockb | Bin 258906 -> 0 bytes capacitor.config.ts | 9 - mobile.md | 6 - package.json | 69 +- packages/complexity-automata-cli/README.md | 107 +++ packages/complexity-automata-cli/package.json | 54 ++ packages/complexity-automata-cli/src/args.ts | 258 ++++++ packages/complexity-automata-cli/src/cli.ts | 320 +++++++ packages/complexity-automata-cli/src/intro.ts | 59 ++ .../complexity-automata-cli/src/library.ts | 40 + .../complexity-automata-cli/src/render.ts | 147 +++ .../complexity-automata-cli/src/session.ts | 179 ++++ .../complexity-automata-cli/test/args.test.ts | 58 ++ .../complexity-automata-cli/test/helpers.ts | 3 + .../test/render.test.ts | 81 ++ .../test/session.test.ts | 102 +++ .../complexity-automata-cli/tsconfig.json | 21 + packages/complexity-automata-svelte/README.md | 83 ++ .../complexity-automata-svelte/package.json | 47 + .../src/AutomataApp.svelte | 545 +++++++++++ .../src/AutomataCanvas.svelte | 152 +++ .../src/IntroScreen.svelte | 208 +++++ .../src/RuleControls.svelte | 183 ++++ .../src/UniverseShelf.svelte | 217 +++++ .../complexity-automata-svelte/src/canvas.js | 96 ++ .../complexity-automata-svelte/src/index.js | 22 + .../test/canvas.test.js | 36 + packages/complexity-automata/README.md | 170 ++++ packages/complexity-automata/package.json | 55 ++ packages/complexity-automata/src/automata.ts | 434 +++++++++ packages/complexity-automata/src/autopilot.ts | 102 +++ packages/complexity-automata/src/color.ts | 91 ++ packages/complexity-automata/src/demos.ts | 249 +++++ packages/complexity-automata/src/index.ts | 60 ++ packages/complexity-automata/src/patterns.ts | 532 +++++++++++ packages/complexity-automata/src/rules.ts | 138 +++ packages/complexity-automata/src/universe.ts | 355 +++++++ .../complexity-automata/test/automata.test.ts | 155 ++++ .../complexity-automata/test/demos.test.ts | 64 ++ .../complexity-automata/test/rules.test.ts | 56 ++ .../complexity-automata/test/universe.test.ts | 104 +++ packages/complexity-automata/tsconfig.json | 23 + postcss.config.cjs | 6 - src/app.css | 12 - src/app.html | 13 - src/lib/GameOfLife/GameOfLife.svelte | 612 ------------- src/lib/GameOfLife/patterns.js | 44 - src/routes/+layout.js | 1 - src/routes/+page.svelte | 6 - svelte.config.js | 27 - tailwind.config.cjs | 11 - tsconfig.json | 24 - wrangler.toml | 6 - 149 files changed, 6911 insertions(+), 868 deletions(-) delete mode 100644 .cloudflare/public/_app/version.json delete mode 100644 .cloudflare/public/logo.png delete mode 100644 .eslintrc.cjs create mode 100644 .github/workflows/ci.yml delete mode 100644 android/capacitor.settings.gradle create mode 100644 apps/android/README.md rename {android => apps/android/android}/.gitignore (100%) rename {android => apps/android/android}/.idea/.gitignore (100%) rename {android => apps/android/android}/.idea/compiler.xml (100%) rename {android => apps/android/android}/.idea/deploymentTargetSelector.xml (100%) rename {android => apps/android/android}/.idea/migrations.xml (100%) rename {android => apps/android/android}/.idea/misc.xml (100%) rename {android => apps/android/android}/.idea/other.xml (100%) rename {android => apps/android/android}/app/.gitignore (100%) rename {android => apps/android/android}/app/build.gradle (97%) rename {android => apps/android/android}/app/capacitor.build.gradle (100%) rename {android => apps/android/android}/app/debug.keystore (100%) rename {android => apps/android/android}/app/debug/output-metadata.json (100%) rename {android => apps/android/android}/app/proguard-rules.pro (100%) rename {android => apps/android/android}/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java (100%) rename {android => apps/android/android}/app/src/main/AndroidManifest.xml (100%) rename {android => apps/android/android}/app/src/main/java/com/vtempest/automata/MainActivity.java (100%) rename {android => apps/android/android}/app/src/main/res/drawable-land-hdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-land-mdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-land-xhdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-land-xxhdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-land-xxxhdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-port-hdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-port-mdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-port-xhdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-port-xxhdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-port-xxxhdpi/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/drawable-v24/ic_launcher_foreground.xml (100%) rename {android => apps/android/android}/app/src/main/res/drawable/ic_launcher_background.xml (100%) rename {android => apps/android/android}/app/src/main/res/drawable/splash.png (100%) rename {android => apps/android/android}/app/src/main/res/layout/activity_main.xml (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-hdpi/ic_launcher_round.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-mdpi/ic_launcher_round.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png (100%) rename {android => apps/android/android}/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png (100%) rename {android => apps/android/android}/app/src/main/res/values/ic_launcher_background.xml (100%) rename {android => apps/android/android}/app/src/main/res/values/strings.xml (60%) rename {android => apps/android/android}/app/src/main/res/values/styles.xml (100%) rename {android => apps/android/android}/app/src/main/res/xml/file_paths.xml (100%) rename {android => apps/android/android}/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java (100%) rename {android => apps/android/android}/build.gradle (100%) create mode 100644 apps/android/android/capacitor.settings.gradle rename {android => apps/android/android}/gradle.properties (100%) rename {android => apps/android/android}/gradle/wrapper/gradle-wrapper.jar (100%) rename {android => apps/android/android}/gradle/wrapper/gradle-wrapper.properties (100%) rename {android => apps/android/android}/gradlew (100%) rename {android => apps/android/android}/gradlew.bat (100%) rename {android => apps/android/android}/settings.gradle (100%) rename {android => apps/android/android}/variables.gradle (100%) create mode 100644 apps/android/capacitor.config.ts create mode 100644 apps/android/package.json rename {resources => apps/android/resources}/android/icon-background.png (100%) rename {resources => apps/android/resources}/android/icon-foreground.png (100%) rename {resources => apps/android/resources}/icon.png (100%) rename {resources => apps/android/resources}/screenshots/logo-512.png (100%) rename {resources => apps/android/resources}/screenshots/s1.png (100%) rename {resources => apps/android/resources}/screenshots/s2.png (100%) rename {resources => apps/android/resources}/screenshots/s2.webp (100%) rename {resources => apps/android/resources}/screenshots/s3.png (100%) rename {resources => apps/android/resources}/screenshots/unnamed.webp (100%) rename {resources => apps/android/resources}/splash-dark.png (100%) rename {resources => apps/android/resources}/splash.png (100%) create mode 100644 apps/web/README.md create mode 100644 apps/web/package.json create mode 100644 apps/web/src/app.css rename {src => apps/web/src}/app.d.ts (100%) create mode 100644 apps/web/src/app.html create mode 100644 apps/web/src/routes/+layout.js rename {src => apps/web/src}/routes/+layout.svelte (53%) create mode 100644 apps/web/src/routes/+page.svelte rename {static => apps/web/static}/logo.png (100%) create mode 100644 apps/web/svelte.config.js create mode 100644 apps/web/tsconfig.json rename vite.config.js => apps/web/vite.config.js (100%) create mode 100644 apps/web/wrangler.toml create mode 100644 bun.lock delete mode 100644 bun.lockb delete mode 100644 capacitor.config.ts delete mode 100644 mobile.md create mode 100644 packages/complexity-automata-cli/README.md create mode 100644 packages/complexity-automata-cli/package.json create mode 100644 packages/complexity-automata-cli/src/args.ts create mode 100644 packages/complexity-automata-cli/src/cli.ts create mode 100644 packages/complexity-automata-cli/src/intro.ts create mode 100644 packages/complexity-automata-cli/src/library.ts create mode 100644 packages/complexity-automata-cli/src/render.ts create mode 100644 packages/complexity-automata-cli/src/session.ts create mode 100644 packages/complexity-automata-cli/test/args.test.ts create mode 100644 packages/complexity-automata-cli/test/helpers.ts create mode 100644 packages/complexity-automata-cli/test/render.test.ts create mode 100644 packages/complexity-automata-cli/test/session.test.ts create mode 100644 packages/complexity-automata-cli/tsconfig.json create mode 100644 packages/complexity-automata-svelte/README.md create mode 100644 packages/complexity-automata-svelte/package.json create mode 100644 packages/complexity-automata-svelte/src/AutomataApp.svelte create mode 100644 packages/complexity-automata-svelte/src/AutomataCanvas.svelte create mode 100644 packages/complexity-automata-svelte/src/IntroScreen.svelte create mode 100644 packages/complexity-automata-svelte/src/RuleControls.svelte create mode 100644 packages/complexity-automata-svelte/src/UniverseShelf.svelte create mode 100644 packages/complexity-automata-svelte/src/canvas.js create mode 100644 packages/complexity-automata-svelte/src/index.js create mode 100644 packages/complexity-automata-svelte/test/canvas.test.js create mode 100644 packages/complexity-automata/README.md create mode 100644 packages/complexity-automata/package.json create mode 100644 packages/complexity-automata/src/automata.ts create mode 100644 packages/complexity-automata/src/autopilot.ts create mode 100644 packages/complexity-automata/src/color.ts create mode 100644 packages/complexity-automata/src/demos.ts create mode 100644 packages/complexity-automata/src/index.ts create mode 100644 packages/complexity-automata/src/patterns.ts create mode 100644 packages/complexity-automata/src/rules.ts create mode 100644 packages/complexity-automata/src/universe.ts create mode 100644 packages/complexity-automata/test/automata.test.ts create mode 100644 packages/complexity-automata/test/demos.test.ts create mode 100644 packages/complexity-automata/test/rules.test.ts create mode 100644 packages/complexity-automata/test/universe.test.ts create mode 100644 packages/complexity-automata/tsconfig.json delete mode 100644 postcss.config.cjs delete mode 100644 src/app.css delete mode 100644 src/app.html delete mode 100644 src/lib/GameOfLife/GameOfLife.svelte delete mode 100644 src/lib/GameOfLife/patterns.js delete mode 100644 src/routes/+layout.js delete mode 100644 src/routes/+page.svelte delete mode 100644 svelte.config.js delete mode 100644 tailwind.config.cjs delete mode 100644 tsconfig.json delete mode 100644 wrangler.toml diff --git a/.cloudflare/public/_app/version.json b/.cloudflare/public/_app/version.json deleted file mode 100644 index d2d0d05..0000000 --- a/.cloudflare/public/_app/version.json +++ /dev/null @@ -1 +0,0 @@ -{"version":"1722213320195"} \ No newline at end of file diff --git a/.cloudflare/public/logo.png b/.cloudflare/public/logo.png deleted file mode 100644 index 359f140a72b103bdea5c696f3c2df92c633f6004..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16838 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 FV1hqkbus_| diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index d41201c..0000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,20 +0,0 @@ -module.exports = { - root: true, - parser: '@typescript-eslint/parser', - extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], - plugins: ['svelte3', '@typescript-eslint'], - ignorePatterns: ['*.cjs'], - overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], - settings: { - 'svelte3/typescript': () => require('typescript') - }, - parserOptions: { - sourceType: 'module', - ecmaVersion: 2020 - }, - env: { - browser: true, - es2017: true, - node: true - } -}; \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8ae4dbd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.3.11 + + - run: bun install --frozen-lockfile + + # The engine builds first: the CLI and the components import it from + # `dist`, so nothing else can be checked until it exists. + - run: bun run build:engine + - run: bun run typecheck + - run: bun run test + - run: bun run build:cli + - run: bun run build:web + + # The Android app is the web build plus a Capacitor copy step. Assembling + # the APK needs the Android SDK; verifying the copy does not. + - run: bunx cap sync android + working-directory: apps/android diff --git a/.gitignore b/.gitignore index 8085912..eeb3cbe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ .DS_Store node_modules -/build -/dist -/.svelte-kit +build +dist +.svelte-kit /package .env .env.* @@ -10,4 +10,13 @@ node_modules vite.config.js.timestamp-* vite.config.ts.timestamp-* .cloudflare/ -.wappler/ \ No newline at end of file +.wappler/ +*.tsbuildinfo + +# Android / Capacitor build output +apps/android/android/app/build +apps/android/android/build +apps/android/android/.gradle +apps/android/android/local.properties +apps/android/android/app/src/main/assets/public +apps/android/android/app/src/main/assets/capacitor.config.json diff --git a/README.md b/README.md index fb59e2c..2a9e2e0 100644 --- a/README.md +++ b/README.md @@ -12,10 +12,12 @@ GitHub Discussions + + npm + - GitHub last commit

-

-

- 🚀 Demo - 🚀 Android App +

+ +

+ 🚀 Web app · + 📱 Android app · + npx complexity-automata-cli

+## Complexity Automata + +Conway's Game of Life, and the 262,143 other universes that share its shape. + +A cell is **born** when it has exactly three live neighbours and **survives** +with two or three. That is the entire rule. The gliders that walk across the +screen, the gun that fires them forever, and the fractals that copy themselves +all follow from it — and changing two numbers gives you a different universe +with its own physics. + +- **Draw** patterns into a running grid — gliders, guns, oscillators, fractal seeds +- **Colour inheritance**: cells take the average colour of the cells that made + them, so colonies drift apart and collisions fade into blends +- **Save universes** and reopen, share or download them; the same file opens in + the web app, the terminal, and other Life software +- **Automatic mode** tours random rules, moving on whenever one stops evolving +- **Twelve demos**, from Conway's to Fredkin's fractal replicator + +Runs the same on the web, on Android, and in a terminal. + +## What is in here + +| | | | +| --- | --- | --- | +| [`packages/complexity-automata`](packages/complexity-automata) | `complexity-automata` | The engine. Rules, grid, colour, patterns, demos, saving. No DOM, no dependencies. | +| [`packages/complexity-automata-cli`](packages/complexity-automata-cli) | `complexity-automata-cli` | The terminal animation, in truecolor half-blocks. | +| [`packages/complexity-automata-svelte`](packages/complexity-automata-svelte) | `complexity-automata-svelte` | Svelte components: canvas view, rule switches, demo picker, saved universes. | +| [`apps/web`](apps/web) | | The web app — SvelteKit, deployed to Cloudflare. | +| [`apps/android`](apps/android) | | The Android app — the same build, wrapped by Capacitor. | + +## Try it + +```bash +# In a terminal +npx complexity-automata-cli # Conway's, with gliders and a gun +npx complexity-automata-cli fractal # a Sierpinski triangle from one cell +npx complexity-automata-cli --auto # a tour of random universes + +# In your own code +npm i complexity-automata +``` -## Automate Automata: "Game of Life" App +```js +import { Automata } from 'complexity-automata'; -* Custom rule sets, common patterns, and color mixing to produce artistic fades +const life = new Automata({ width: 80, height: 40, rule: 'B3/S23' }); +life.stampCentered('Glider Gun'); +life.run(120); +console.log(life.toString()); +``` -* Automated mode - randomize rule set changes & patterns and watch life unfold... +## Developing -* developed in 2014 and 10+ years as Android app with Javascript, rewriten in Svelte +Bun workspaces and no build orchestration to learn: +```bash +bun install +bun run build # engine → CLI → web +bun run dev # the web app at localhost:5173 +bun run cli # the terminal animation from source +bun run test # every package's suite +bun run typecheck +bun run android # build the web app and assemble the APK +bun run deploy # wrangler deploy from apps/web +``` -### Further Research +The engine has no dependencies and is tested on its own (`cd +packages/complexity-automata && bun test`); the CLI and the components are thin +layers over it. If a change is about *what the automata do*, it belongs in the +engine, with a test. +## Further Research * Johnston, Nathaniel, and Greene, Dave (2022). "Conway's Game of Life". https://conwaylife.com/book/#rle_files @@ -50,21 +114,22 @@ * Wolfram, Stephen (2013). "Talking about the Computational Future at SXSW 2013", https://writings.stephenwolfram.com/2013/03/talking-about-the-computational-future-at-sxsw-2013/ -#### Excerpt: +#### Excerpt: > By the way, it’s bizarre how few people work on this. Because I’m sure that, just like cloning, there’s just going to be a wacky procedure that makes it possible—and once we know it, we’re just going to be able to do it quite routinely, and it’s going to be societally very important. But in the end, we want to solve the problem of keeping all the complexity that is a human running indefinitely. There are some fascinating basic science problems here. Connected to concepts like computational irreducibility, and a bit to the traditional halting problem. But I have no doubt that eventually it’ll be solved, and we’ll achieve effective human immortality. And when that happens I expect it’ll be the single biggest discontinuity in human history. -> +> > Cellular automata -> +> > You know, as one thinks about such things, one can’t help wondering about the general future of the human condition. And here’s something someone like me definitely thinks about. I’m spending my life trying to automate things. Trying to make it possible to do automatically with computation things that humans used to have to do themselves. -> +> > Now, if we look at the arc of human history, the biggest systematic change through time is the arrival of more and more technology, and the automation of more and more kinds of tasks. So here’s a question: what if we succeed in automating everything? What will happen then? What will the humans do? There’s an ultimate—almost philosophical—version of this question. And there’s also a practical next-few-decades version. -> +> > Let’s start with the ultimate version. As we go on and build more and more technology, what will the end point be? We might assume that we could somehow go on forever, achieving more and more. But the Principle of Computational Equivalence tells us that we cannot. One we have reached a certain level, everything is already in a sense possible. And even though our current engineering has not yet reached this point, the Principle of Computational Equivalence also tells us that this maximal level of computational sophistication is not particularly rare. Indeed it happens in many places in the physical world, as well as in systems like simple cellular automata. -> +> > Cellular automata -> +> > And it’s not too hard to see that as we improve our technology, getting down to the smallest scales, and removing everything that seems redundant, that we might wind up with something that looks just like a physical process that already happens in nature. So does this mean that in the ultimate future, with all that great automation and technology, all we’ll achieve is just to produce something that’s indistinguishable from zillions of things that already exist in nature? -> +> > Wolfram, Stephen (2013). "Talking about the Computational Future at SXSW 2013", https://writings.stephenwolfram.com/2013/03/talking-about-the-computational-future-at-sxsw-2013/ +MIT · developed in 2014, ten years as an Android app, rewritten in Svelte in 2024, split into engine, apps and CLI in 2025. diff --git a/android/capacitor.settings.gradle b/android/capacitor.settings.gradle deleted file mode 100644 index 9a5fa87..0000000 --- a/android/capacitor.settings.gradle +++ /dev/null @@ -1,3 +0,0 @@ -// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN -include ':capacitor-android' -project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') diff --git a/apps/android/README.md b/apps/android/README.md new file mode 100644 index 0000000..084d35e --- /dev/null +++ b/apps/android/README.md @@ -0,0 +1,50 @@ +# Android app + +The web app in a WebView, wrapped by [Capacitor](https://capacitorjs.com). +There is no Android-specific UI: `capacitor.config.ts` points `webDir` at +`../web/build`, so the app ships exactly the bundle Cloudflare serves. + +Published as [com.vtempest.automata](https://play.google.com/store/apps/details?id=com.vtempest.automata). + +## Build + +```bash +bun run sync # build the web app, then copy it into the Android project +bun run build # sync, then assemble a debug APK +bun run release # sync, then bundle a signed release AAB +bun run open # open the project in Android Studio +``` + +`bun run build` needs a JDK and the Android SDK (`ANDROID_HOME`); `bun run sync` +needs neither, so CI can verify the copy step on its own. + +Output: `android/app/build/outputs/apk/debug/app-debug.apk`. + +## What is checked in + +The Gradle project under `android/`, the launcher icons and splash screens under +`resources/`, and the Capacitor config. The synced web assets +(`android/app/src/main/assets/public`) are build output and are ignored. + +## Changing the app + +- **The UI** is [`packages/complexity-automata-svelte`](../../packages/complexity-automata-svelte) +- **The version** is `versionCode`/`versionName` in `android/app/build.gradle` +- **The name** is `app_name` in `android/app/src/main/res/values/strings.xml` +- **Icons and splash** are regenerated from `resources/` with + `bunx @capacitor/assets generate --android` + +The page is laid out for a phone: the controls sit inside the safe area, the +grid takes the rest, and `overscroll-behavior: none` keeps pull-to-refresh from +fighting with drawing on the grid. + +## First-time setup + +The project was created with: + +```bash +bun i @capacitor/core @capacitor/android +bun i -D @capacitor/cli +bunx cap init +bunx cap add android +``` diff --git a/android/.gitignore b/apps/android/android/.gitignore similarity index 100% rename from android/.gitignore rename to apps/android/android/.gitignore diff --git a/android/.idea/.gitignore b/apps/android/android/.idea/.gitignore similarity index 100% rename from android/.idea/.gitignore rename to apps/android/android/.idea/.gitignore diff --git a/android/.idea/compiler.xml b/apps/android/android/.idea/compiler.xml similarity index 100% rename from android/.idea/compiler.xml rename to apps/android/android/.idea/compiler.xml diff --git a/android/.idea/deploymentTargetSelector.xml b/apps/android/android/.idea/deploymentTargetSelector.xml similarity index 100% rename from android/.idea/deploymentTargetSelector.xml rename to apps/android/android/.idea/deploymentTargetSelector.xml diff --git a/android/.idea/migrations.xml b/apps/android/android/.idea/migrations.xml similarity index 100% rename from android/.idea/migrations.xml rename to apps/android/android/.idea/migrations.xml diff --git a/android/.idea/misc.xml b/apps/android/android/.idea/misc.xml similarity index 100% rename from android/.idea/misc.xml rename to apps/android/android/.idea/misc.xml diff --git a/android/.idea/other.xml b/apps/android/android/.idea/other.xml similarity index 100% rename from android/.idea/other.xml rename to apps/android/android/.idea/other.xml diff --git a/android/app/.gitignore b/apps/android/android/app/.gitignore similarity index 100% rename from android/app/.gitignore rename to apps/android/android/app/.gitignore diff --git a/android/app/build.gradle b/apps/android/android/app/build.gradle similarity index 97% rename from android/app/build.gradle rename to apps/android/android/app/build.gradle index be03db8..6ef95dd 100644 --- a/android/app/build.gradle +++ b/apps/android/android/app/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.vtempest.automata" minSdkVersion 26 targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 3 - versionName "2.0.1" + versionCode 4 + versionName "3.0.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. diff --git a/android/app/capacitor.build.gradle b/apps/android/android/app/capacitor.build.gradle similarity index 100% rename from android/app/capacitor.build.gradle rename to apps/android/android/app/capacitor.build.gradle diff --git a/android/app/debug.keystore b/apps/android/android/app/debug.keystore similarity index 100% rename from android/app/debug.keystore rename to apps/android/android/app/debug.keystore diff --git a/android/app/debug/output-metadata.json b/apps/android/android/app/debug/output-metadata.json similarity index 100% rename from android/app/debug/output-metadata.json rename to apps/android/android/app/debug/output-metadata.json diff --git a/android/app/proguard-rules.pro b/apps/android/android/app/proguard-rules.pro similarity index 100% rename from android/app/proguard-rules.pro rename to apps/android/android/app/proguard-rules.pro diff --git a/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/apps/android/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java similarity index 100% rename from android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java rename to apps/android/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java diff --git a/android/app/src/main/AndroidManifest.xml b/apps/android/android/app/src/main/AndroidManifest.xml similarity index 100% rename from android/app/src/main/AndroidManifest.xml rename to apps/android/android/app/src/main/AndroidManifest.xml diff --git a/android/app/src/main/java/com/vtempest/automata/MainActivity.java b/apps/android/android/app/src/main/java/com/vtempest/automata/MainActivity.java similarity index 100% rename from android/app/src/main/java/com/vtempest/automata/MainActivity.java rename to apps/android/android/app/src/main/java/com/vtempest/automata/MainActivity.java diff --git a/android/app/src/main/res/drawable-land-hdpi/splash.png b/apps/android/android/app/src/main/res/drawable-land-hdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-hdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-land-hdpi/splash.png diff --git a/android/app/src/main/res/drawable-land-mdpi/splash.png b/apps/android/android/app/src/main/res/drawable-land-mdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-mdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-land-mdpi/splash.png diff --git a/android/app/src/main/res/drawable-land-xhdpi/splash.png b/apps/android/android/app/src/main/res/drawable-land-xhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-xhdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-land-xhdpi/splash.png diff --git a/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/apps/android/android/app/src/main/res/drawable-land-xxhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-xxhdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-land-xxhdpi/splash.png diff --git a/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/apps/android/android/app/src/main/res/drawable-land-xxxhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-land-xxxhdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-land-xxxhdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-hdpi/splash.png b/apps/android/android/app/src/main/res/drawable-port-hdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-hdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-port-hdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-mdpi/splash.png b/apps/android/android/app/src/main/res/drawable-port-mdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-mdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-port-mdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-xhdpi/splash.png b/apps/android/android/app/src/main/res/drawable-port-xhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-xhdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-port-xhdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/apps/android/android/app/src/main/res/drawable-port-xxhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-xxhdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-port-xxhdpi/splash.png diff --git a/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/apps/android/android/app/src/main/res/drawable-port-xxxhdpi/splash.png similarity index 100% rename from android/app/src/main/res/drawable-port-xxxhdpi/splash.png rename to apps/android/android/app/src/main/res/drawable-port-xxxhdpi/splash.png diff --git a/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/apps/android/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to apps/android/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/apps/android/android/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from android/app/src/main/res/drawable/ic_launcher_background.xml rename to apps/android/android/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/android/app/src/main/res/drawable/splash.png b/apps/android/android/app/src/main/res/drawable/splash.png similarity index 100% rename from android/app/src/main/res/drawable/splash.png rename to apps/android/android/app/src/main/res/drawable/splash.png diff --git a/android/app/src/main/res/layout/activity_main.xml b/apps/android/android/app/src/main/res/layout/activity_main.xml similarity index 100% rename from android/app/src/main/res/layout/activity_main.xml rename to apps/android/android/app/src/main/res/layout/activity_main.xml diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to apps/android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to apps/android/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to apps/android/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/apps/android/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png rename to apps/android/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/apps/android/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to apps/android/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/apps/android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to apps/android/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/apps/android/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png rename to apps/android/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/apps/android/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to apps/android/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/apps/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to apps/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/apps/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png rename to apps/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/apps/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to apps/android/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/apps/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to apps/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/apps/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png rename to apps/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/apps/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to apps/android/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/apps/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to apps/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/apps/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png rename to apps/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/apps/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to apps/android/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/android/app/src/main/res/values/ic_launcher_background.xml b/apps/android/android/app/src/main/res/values/ic_launcher_background.xml similarity index 100% rename from android/app/src/main/res/values/ic_launcher_background.xml rename to apps/android/android/app/src/main/res/values/ic_launcher_background.xml diff --git a/android/app/src/main/res/values/strings.xml b/apps/android/android/app/src/main/res/values/strings.xml similarity index 60% rename from android/app/src/main/res/values/strings.xml rename to apps/android/android/app/src/main/res/values/strings.xml index 7de1553..c594b05 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/apps/android/android/app/src/main/res/values/strings.xml @@ -1,7 +1,7 @@ - Automated Automata - Automated Automata + Complexity Automata + Complexity Automata com.vtempest.automata com.vtempest.automata diff --git a/android/app/src/main/res/values/styles.xml b/apps/android/android/app/src/main/res/values/styles.xml similarity index 100% rename from android/app/src/main/res/values/styles.xml rename to apps/android/android/app/src/main/res/values/styles.xml diff --git a/android/app/src/main/res/xml/file_paths.xml b/apps/android/android/app/src/main/res/xml/file_paths.xml similarity index 100% rename from android/app/src/main/res/xml/file_paths.xml rename to apps/android/android/app/src/main/res/xml/file_paths.xml diff --git a/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/apps/android/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java similarity index 100% rename from android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java rename to apps/android/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java diff --git a/android/build.gradle b/apps/android/android/build.gradle similarity index 100% rename from android/build.gradle rename to apps/android/android/build.gradle diff --git a/apps/android/android/capacitor.settings.gradle b/apps/android/android/capacitor.settings.gradle new file mode 100644 index 0000000..4b38951 --- /dev/null +++ b/apps/android/android/capacitor.settings.gradle @@ -0,0 +1,3 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../../../node_modules/.bun/@capacitor+android@6.2.2+bbf93636f05d5b82/node_modules/@capacitor/android/capacitor') diff --git a/android/gradle.properties b/apps/android/android/gradle.properties similarity index 100% rename from android/gradle.properties rename to apps/android/android/gradle.properties diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/apps/android/android/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.jar rename to apps/android/android/gradle/wrapper/gradle-wrapper.jar diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/apps/android/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.properties rename to apps/android/android/gradle/wrapper/gradle-wrapper.properties diff --git a/android/gradlew b/apps/android/android/gradlew similarity index 100% rename from android/gradlew rename to apps/android/android/gradlew diff --git a/android/gradlew.bat b/apps/android/android/gradlew.bat similarity index 100% rename from android/gradlew.bat rename to apps/android/android/gradlew.bat diff --git a/android/settings.gradle b/apps/android/android/settings.gradle similarity index 100% rename from android/settings.gradle rename to apps/android/android/settings.gradle diff --git a/android/variables.gradle b/apps/android/android/variables.gradle similarity index 100% rename from android/variables.gradle rename to apps/android/android/variables.gradle diff --git a/apps/android/capacitor.config.ts b/apps/android/capacitor.config.ts new file mode 100644 index 0000000..87f8799 --- /dev/null +++ b/apps/android/capacitor.config.ts @@ -0,0 +1,23 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +/** + * The Android app is the web app in a WebView. `webDir` points at the + * SvelteKit static build one directory over, so `bun run build` here builds the + * web app and syncs the very same files Cloudflare serves. + */ +const config: CapacitorConfig = { + appId: 'com.vtempest.automata', + appName: 'Complexity Automata', + webDir: '../web/build', + android: { + // The grid is drawn on a dark canvas; matching the shell avoids a white + // flash between the splash screen and the first frame. + backgroundColor: '#080a10', + allowMixedContent: false, + }, + server: { + androidScheme: 'https', + }, +}; + +export default config; diff --git a/apps/android/package.json b/apps/android/package.json new file mode 100644 index 0000000..3d0907c --- /dev/null +++ b/apps/android/package.json @@ -0,0 +1,22 @@ +{ + "name": "@automate-automata/android", + "private": true, + "version": "3.0.0", + "description": "Capacitor wrapper that ships the web app as the Android app.", + "type": "module", + "scripts": { + "build": "bun run sync && bun run assemble", + "sync": "cd ../web && bun run build && cd ../android && cap sync android", + "assemble": "cd android && ./gradlew assembleDebug", + "release": "cd android && ./gradlew bundleRelease", + "open": "cap open android" + }, + "dependencies": { + "@capacitor/android": "^6.1.1", + "@capacitor/core": "^6.1.1" + }, + "devDependencies": { + "@capacitor/cli": "^6.1.1", + "typescript": "^5.5.4" + } +} diff --git a/resources/android/icon-background.png b/apps/android/resources/android/icon-background.png similarity index 100% rename from resources/android/icon-background.png rename to apps/android/resources/android/icon-background.png diff --git a/resources/android/icon-foreground.png b/apps/android/resources/android/icon-foreground.png similarity index 100% rename from resources/android/icon-foreground.png rename to apps/android/resources/android/icon-foreground.png diff --git a/resources/icon.png b/apps/android/resources/icon.png similarity index 100% rename from resources/icon.png rename to apps/android/resources/icon.png diff --git a/resources/screenshots/logo-512.png b/apps/android/resources/screenshots/logo-512.png similarity index 100% rename from resources/screenshots/logo-512.png rename to apps/android/resources/screenshots/logo-512.png diff --git a/resources/screenshots/s1.png b/apps/android/resources/screenshots/s1.png similarity index 100% rename from resources/screenshots/s1.png rename to apps/android/resources/screenshots/s1.png diff --git a/resources/screenshots/s2.png b/apps/android/resources/screenshots/s2.png similarity index 100% rename from resources/screenshots/s2.png rename to apps/android/resources/screenshots/s2.png diff --git a/resources/screenshots/s2.webp b/apps/android/resources/screenshots/s2.webp similarity index 100% rename from resources/screenshots/s2.webp rename to apps/android/resources/screenshots/s2.webp diff --git a/resources/screenshots/s3.png b/apps/android/resources/screenshots/s3.png similarity index 100% rename from resources/screenshots/s3.png rename to apps/android/resources/screenshots/s3.png diff --git a/resources/screenshots/unnamed.webp b/apps/android/resources/screenshots/unnamed.webp similarity index 100% rename from resources/screenshots/unnamed.webp rename to apps/android/resources/screenshots/unnamed.webp diff --git a/resources/splash-dark.png b/apps/android/resources/splash-dark.png similarity index 100% rename from resources/splash-dark.png rename to apps/android/resources/splash-dark.png diff --git a/resources/splash.png b/apps/android/resources/splash.png similarity index 100% rename from resources/splash.png rename to apps/android/resources/splash.png diff --git a/apps/web/README.md b/apps/web/README.md new file mode 100644 index 0000000..70c4056 --- /dev/null +++ b/apps/web/README.md @@ -0,0 +1,31 @@ +# Web app + +SvelteKit shell around [`complexity-automata-svelte`](../../packages/complexity-automata-svelte). +Everything on screen is the `AutomataApp` component; this app only supplies the +page, the metadata and the URL handling. + +```bash +bun run dev # localhost:5173 +bun run build # static bundle in build/ +bun run preview +bun run deploy # wrangler deploy +``` + +## URL parameters + +| | | +| --- | --- | +| `?demo=fractal` | open straight into a demo | +| `?intro=0` | skip the intro screen | +| `?cell=6` | CSS pixels per cell — bigger cells, smaller universe | +| `?u=…` | open a shared universe | + +## How it is built + +`adapter-static` with an `index.html` fallback, rendered entirely on the client +(`ssr = false` — the app is a canvas, so there is nothing useful to render on a +server). That single `build/` directory is what Cloudflare serves *and* what +Capacitor copies into the Android app, so the two can never drift apart. + +Because rendering is client-side, the page title and social metadata live in +`src/app.html` rather than in a ``. diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..95a380a --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,32 @@ +{ + "name": "@automate-automata/web", + "private": true, + "version": "3.0.0", + "description": "The Complexity Automata web app — the same UI the Android app wraps.", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "deploy": "wrangler deploy" + }, + "dependencies": { + "complexity-automata": "workspace:*", + "complexity-automata-svelte": "workspace:*" + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.2", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "tailwindcss": "^3.4.3", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^5.0.11", + "wrangler": "^3.57.0" + } +} diff --git a/apps/web/src/app.css b/apps/web/src/app.css new file mode 100644 index 0000000..505b59b --- /dev/null +++ b/apps/web/src/app.css @@ -0,0 +1,33 @@ +:root { + color-scheme: dark; + --bg: #080a10; +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + width: 100%; + height: 100%; + overflow: hidden; + background: var(--bg); + font-family: + system-ui, + -apple-system, + 'Segoe UI', + Roboto, + sans-serif; + /* Stop the pull-to-refresh and rubber-banding that fight with drawing on + the grid in a mobile browser and in the Android WebView. */ + overscroll-behavior: none; + -webkit-tap-highlight-color: transparent; +} + +body > div { + width: 100%; + height: 100%; +} diff --git a/src/app.d.ts b/apps/web/src/app.d.ts similarity index 100% rename from src/app.d.ts rename to apps/web/src/app.d.ts diff --git a/apps/web/src/app.html b/apps/web/src/app.html new file mode 100644 index 0000000..12e6d81 --- /dev/null +++ b/apps/web/src/app.html @@ -0,0 +1,23 @@ + + + + + + + + Complexity Automata — Conway's Game of Life + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/web/src/routes/+layout.js b/apps/web/src/routes/+layout.js new file mode 100644 index 0000000..1e2868f --- /dev/null +++ b/apps/web/src/routes/+layout.js @@ -0,0 +1,4 @@ +// The whole app is one client-rendered canvas, so it prerenders to a single +// static page — which is also what Capacitor ships inside the Android app. +export const prerender = true; +export const ssr = false; diff --git a/src/routes/+layout.svelte b/apps/web/src/routes/+layout.svelte similarity index 53% rename from src/routes/+layout.svelte rename to apps/web/src/routes/+layout.svelte index a04e315..4039a26 100644 --- a/src/routes/+layout.svelte +++ b/apps/web/src/routes/+layout.svelte @@ -1,7 +1,5 @@ - diff --git a/apps/web/src/routes/+page.svelte b/apps/web/src/routes/+page.svelte new file mode 100644 index 0000000..17b6acf --- /dev/null +++ b/apps/web/src/routes/+page.svelte @@ -0,0 +1,14 @@ + + +{#key demo} + +{/key} diff --git a/static/logo.png b/apps/web/static/logo.png similarity index 100% rename from static/logo.png rename to apps/web/static/logo.png diff --git a/apps/web/svelte.config.js b/apps/web/svelte.config.js new file mode 100644 index 0000000..7e6c871 --- /dev/null +++ b/apps/web/svelte.config.js @@ -0,0 +1,19 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + // A single static bundle: Cloudflare serves it, and Capacitor copies the + // same `build/` directory into the Android app. + adapter: adapter({ + pages: 'build', + assets: 'build', + fallback: 'index.html', + precompress: false, + }), + }, +}; + +export default config; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..f426771 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": false, + "moduleResolution": "bundler" + } +} diff --git a/vite.config.js b/apps/web/vite.config.js similarity index 100% rename from vite.config.js rename to apps/web/vite.config.js diff --git a/apps/web/wrangler.toml b/apps/web/wrangler.toml new file mode 100644 index 0000000..ece6cc1 --- /dev/null +++ b/apps/web/wrangler.toml @@ -0,0 +1,7 @@ +name = "automata-game-of-life" +compatibility_date = "2024-09-01" + +# The SvelteKit static build, served straight from Cloudflare's edge. +[assets] +directory = "./build" +not_found_handling = "single-page-application" diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..a43b731 --- /dev/null +++ b/bun.lock @@ -0,0 +1,867 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "automate-automata", + }, + "apps/android": { + "name": "@automate-automata/android", + "version": "3.0.0", + "dependencies": { + "@capacitor/android": "^6.1.1", + "@capacitor/core": "^6.1.1", + }, + "devDependencies": { + "@capacitor/cli": "^6.1.1", + "typescript": "^5.5.4", + }, + }, + "apps/web": { + "name": "@automate-automata/web", + "version": "3.0.0", + "dependencies": { + "complexity-automata": "workspace:*", + "complexity-automata-svelte": "workspace:*", + }, + "devDependencies": { + "@sveltejs/adapter-static": "^3.0.2", + "@sveltejs/kit": "^2.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "svelte": "^4.2.7", + "svelte-check": "^3.6.0", + "tailwindcss": "^3.4.3", + "tslib": "^2.4.1", + "typescript": "^5.0.0", + "vite": "^5.0.11", + "wrangler": "^3.57.0", + }, + }, + "packages/complexity-automata": { + "name": "complexity-automata", + "version": "1.0.0", + "devDependencies": { + "@types/bun": "^1.1.10", + "typescript": "^5.5.4", + }, + }, + "packages/complexity-automata-cli": { + "name": "complexity-automata-cli", + "version": "1.0.0", + "bin": { + "automata": "./dist/cli.js", + "complexity-automata": "./dist/cli.js", + }, + "dependencies": { + "complexity-automata": "workspace:*", + }, + "devDependencies": { + "@types/bun": "^1.1.10", + "@types/node": "^22.5.0", + "typescript": "^5.5.4", + }, + }, + "packages/complexity-automata-svelte": { + "name": "complexity-automata-svelte", + "version": "1.0.0", + "dependencies": { + "complexity-automata": "workspace:*", + }, + "devDependencies": { + "svelte": "^4.2.7", + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0", + }, + }, + }, + "packages": { + "@alloc/quick-lru": ["@alloc/quick-lru@5.3.0", "", {}, "sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@automate-automata/android": ["@automate-automata/android@workspace:apps/android"], + + "@automate-automata/web": ["@automate-automata/web@workspace:apps/web"], + + "@capacitor/android": ["@capacitor/android@6.2.2", "", { "peerDependencies": { "@capacitor/core": "^6.2.0" } }, "sha512-JjIu1ryNE16PQTp51rb4NEygxP7cSCDMmUqx4f5f12xscKdGKTjO9bkEP3TroLnTa/J6p2QopIhRbrd3E/7DrA=="], + + "@capacitor/cli": ["@capacitor/cli@6.2.2", "", { "dependencies": { "@ionic/cli-framework-output": "^2.2.5", "@ionic/utils-fs": "^3.1.6", "@ionic/utils-subprocess": "2.1.11", "@ionic/utils-terminal": "^2.3.3", "commander": "^9.3.0", "debug": "^4.3.4", "env-paths": "^2.2.0", "kleur": "^4.1.4", "native-run": "^2.0.0", "open": "^8.4.0", "plist": "^3.0.5", "prompts": "^2.4.2", "rimraf": "^4.4.1", "semver": "^7.3.7", "tar": "^6.1.11", "tslib": "^2.4.0", "xml2js": "^0.5.0" }, "bin": { "cap": "bin/capacitor", "capacitor": "bin/capacitor" } }, "sha512-5wEYSHBzRKJKZnhD2sVJAP0t0kUT7o2/vlgEGzFE3t2w+inL9qMDN7zxg6pvqveJiPNZT2k93ZvJDdRKIn/w1w=="], + + "@capacitor/core": ["@capacitor/core@6.2.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-qEI3DcwJEPzhW+hVGKH8m4jbujtkdUiGkpMLHmpR8epACU7wEsV6NF8pCHzpsMYjc80oK7Cmpcw+MIienv/YRw=="], + + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.0.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.14", "workerd": "^1.20250124.0" }, "optionalPeers": ["workerd"] }, "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250718.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250718.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250718.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250718.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250718.0", "", { "os": "win32", "cpu": "x64" }, "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="], + + "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="], + + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + + "@ionic/cli-framework-output": ["@ionic/cli-framework-output@2.2.8", "", { "dependencies": { "@ionic/utils-terminal": "2.3.5", "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g=="], + + "@ionic/utils-array": ["@ionic/utils-array@2.1.5", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-HD72a71IQVBmQckDwmA8RxNVMTbxnaLbgFOl+dO5tbvW9CkkSFCv41h6fUuNsSEVgngfkn0i98HDuZC8mk+lTA=="], + + "@ionic/utils-fs": ["@ionic/utils-fs@3.1.7", "", { "dependencies": { "@types/fs-extra": "^8.0.0", "debug": "^4.0.0", "fs-extra": "^9.0.0", "tslib": "^2.0.1" } }, "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA=="], + + "@ionic/utils-object": ["@ionic/utils-object@2.1.5", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-XnYNSwfewUqxq+yjER1hxTKggftpNjFLJH0s37jcrNDwbzmbpFTQTVAp4ikNK4rd9DOebX/jbeZb8jfD86IYxw=="], + + "@ionic/utils-process": ["@ionic/utils-process@2.1.10", "", { "dependencies": { "@ionic/utils-object": "2.1.5", "@ionic/utils-terminal": "2.3.3", "debug": "^4.0.0", "signal-exit": "^3.0.3", "tree-kill": "^1.2.2", "tslib": "^2.0.1" } }, "sha512-mZ7JEowcuGQK+SKsJXi0liYTcXd2bNMR3nE0CyTROpMECUpJeAvvaBaPGZf5ERQUPeWBVuwqAqjUmIdxhz5bxw=="], + + "@ionic/utils-stream": ["@ionic/utils-stream@3.1.5", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-hkm46uHvEC05X/8PHgdJi4l4zv9VQDELZTM+Kz69odtO9zZYfnt8DkfXHJqJ+PxmtiE5mk/ehJWLnn/XAczTUw=="], + + "@ionic/utils-subprocess": ["@ionic/utils-subprocess@2.1.11", "", { "dependencies": { "@ionic/utils-array": "2.1.5", "@ionic/utils-fs": "3.1.6", "@ionic/utils-process": "2.1.10", "@ionic/utils-stream": "3.1.5", "@ionic/utils-terminal": "2.3.3", "cross-spawn": "^7.0.3", "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-6zCDixNmZCbMCy5np8klSxOZF85kuDyzZSTTQKQP90ZtYNCcPYmuFSzaqDwApJT4r5L3MY3JrqK1gLkc6xiUPw=="], + + "@ionic/utils-terminal": ["@ionic/utils-terminal@2.3.5", "", { "dependencies": { "@types/slice-ansi": "^4.0.0", "debug": "^4.0.0", "signal-exit": "^3.0.3", "slice-ansi": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "tslib": "^2.0.1", "untildify": "^4.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.4", "", { "os": "android", "cpu": "arm" }, "sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.4", "", { "os": "android", "cpu": "arm64" }, "sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.4", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.4", "", { "os": "linux", "cpu": "arm" }, "sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.4", "", { "os": "linux", "cpu": "arm" }, "sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.4", "", { "os": "linux", "cpu": "none" }, "sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.4", "", { "os": "linux", "cpu": "none" }, "sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.4", "", { "os": "linux", "cpu": "none" }, "sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.4", "", { "os": "linux", "cpu": "none" }, "sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.4", "", { "os": "linux", "cpu": "x64" }, "sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.4", "", { "os": "linux", "cpu": "x64" }, "sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.4", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.4", "", { "os": "none", "cpu": "arm64" }, "sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.4", "", { "os": "win32", "cpu": "x64" }, "sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.4", "", { "os": "win32", "cpu": "x64" }, "sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="], + + "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.10", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.0", "sirv": "^3.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@3.1.2", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", "debug": "^4.3.4", "deepmerge": "^4.3.1", "kleur": "^4.1.5", "magic-string": "^0.30.10", "svelte-hmr": "^0.16.0", "vitefu": "^0.2.5" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@2.1.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^3.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.0" } }, "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg=="], + + "@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/fs-extra": ["@types/fs-extra@8.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ=="], + + "@types/node": ["@types/node@22.20.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-zJRE40jpHtKqE/C4fgHrAKQLJuSpzEnP9ff9Y7YtoR3Wd2pwqzlekDeEuUQXjRd+QCYnVnNwuJYmhdk9XV8gvA=="], + + "@types/pug": ["@types/pug@2.0.10", "", {}, "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA=="], + + "@types/slice-ansi": ["@types/slice-ansi@4.0.0", "", {}, "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.9.12", "", {}, "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A=="], + + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + + "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "arg": ["arg@5.0.2", "", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "autoprefixer": ["autoprefixer@10.6.1", "", { "dependencies": { "browserslist": "^4.28.9", "caniuse-lite": "^1.0.30001810", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.25", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw=="], + + "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + + "bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="], + + "brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "browserslist": ["browserslist@4.29.0", "", { "dependencies": { "baseline-browser-mapping": "^2.11.23", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.427", "node-releases": "^2.0.55", "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], + + "camelcase-css": ["camelcase-css@2.0.1", "", {}, "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], + + "chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + + "code-red": ["code-red@1.0.4", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15", "@types/estree": "^1.0.1", "acorn": "^8.10.0", "estree-walker": "^3.0.3", "periscopic": "^3.1.0" } }, "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + + "complexity-automata": ["complexity-automata@workspace:packages/complexity-automata"], + + "complexity-automata-cli": ["complexity-automata-cli@workspace:packages/complexity-automata-cli"], + + "complexity-automata-svelte": ["complexity-automata-svelte@workspace:packages/complexity-automata-svelte"], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "css-tree": ["css-tree@2.3.1", "", { "dependencies": { "mdn-data": "2.0.30", "source-map-js": "^1.0.1" } }, "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw=="], + + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "detect-indent": ["detect-indent@6.1.0", "", {}, "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devalue": ["devalue@5.9.4", "", {}, "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg=="], + + "didyoumean": ["didyoumean@1.2.2", "", {}, "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw=="], + + "dlv": ["dlv@1.1.3", "", {}, "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA=="], + + "electron-to-chromium": ["electron-to-chromium@1.5.434", "", {}, "sha512-7EeFW9OLf1NN9NKQP9xdOE/XKqjxv7JDPP6AEAkHli11+Fae1OaLoNuAf13hQv1PPNtuES0pLQSPk2+fS3s8ww=="], + + "elementtree": ["elementtree@0.1.7", "", { "dependencies": { "sax": "1.1.4" } }, "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es6-promise": ["es6-promise@3.3.1", "", {}, "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg=="], + + "esbuild": ["esbuild@0.21.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + + "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], + + "fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-source": ["get-source@2.0.12", "", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="], + + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], + + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@1.21.7", "", { "bin": { "jiti": "bin/jiti.js" } }, "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mdn-data": ["mdn-data@2.0.30", "", {}, "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + + "miniflare": ["miniflare@3.20250718.3", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="], + + "minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + + "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], + + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + + "native-run": ["native-run@2.0.3", "", { "dependencies": { "@ionic/utils-fs": "^3.1.7", "@ionic/utils-terminal": "^2.3.4", "bplist-parser": "^0.3.2", "debug": "^4.3.4", "elementtree": "^0.1.7", "ini": "^4.1.1", "plist": "^3.1.0", "split2": "^4.2.0", "through2": "^4.0.2", "tslib": "^2.6.2", "yauzl": "^2.10.0" }, "bin": { "native-run": "bin/native-run" } }, "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q=="], + + "node-releases": ["node-releases@2.0.56", "", {}, "sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-hash": ["object-hash@3.0.0", "", {}, "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw=="], + + "ohash": ["ohash@2.0.12", "", {}, "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "periscopic": ["periscopic@3.1.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^3.0.0", "is-reference": "^3.0.0" } }, "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], + + "plist": ["plist@3.1.1", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], + + "postcss-js": ["postcss-js@4.1.0", "", { "dependencies": { "camelcase-css": "^2.0.1" }, "peerDependencies": { "postcss": "^8.4.21" } }, "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw=="], + + "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], + + "postcss-nested": ["postcss-nested@6.2.0", "", { "dependencies": { "postcss-selector-parser": "^6.1.1" }, "peerDependencies": { "postcss": "^8.2.14" } }, "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ=="], + + "postcss-selector-parser": ["postcss-selector-parser@6.1.4", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ=="], + + "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], + + "printable-characters": ["printable-characters@1.0.42", "", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "read-cache": ["read-cache@1.0.2", "", {}, "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@4.4.1", "", { "dependencies": { "glob": "^9.2.0" }, "bin": { "rimraf": "dist/cjs/src/bin.js" } }, "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og=="], + + "rollup": ["rollup@4.63.4", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.4", "@rollup/rollup-android-arm64": "4.63.4", "@rollup/rollup-darwin-arm64": "4.63.4", "@rollup/rollup-darwin-x64": "4.63.4", "@rollup/rollup-freebsd-arm64": "4.63.4", "@rollup/rollup-freebsd-x64": "4.63.4", "@rollup/rollup-linux-arm-gnueabihf": "4.63.4", "@rollup/rollup-linux-arm-musleabihf": "4.63.4", "@rollup/rollup-linux-arm64-gnu": "4.63.4", "@rollup/rollup-linux-arm64-musl": "4.63.4", "@rollup/rollup-linux-loong64-gnu": "4.63.4", "@rollup/rollup-linux-loong64-musl": "4.63.4", "@rollup/rollup-linux-ppc64-gnu": "4.63.4", "@rollup/rollup-linux-ppc64-musl": "4.63.4", "@rollup/rollup-linux-riscv64-gnu": "4.63.4", "@rollup/rollup-linux-riscv64-musl": "4.63.4", "@rollup/rollup-linux-s390x-gnu": "4.63.4", "@rollup/rollup-linux-x64-gnu": "4.63.4", "@rollup/rollup-linux-x64-musl": "4.63.4", "@rollup/rollup-openbsd-x64": "4.63.4", "@rollup/rollup-openharmony-arm64": "4.63.4", "@rollup/rollup-win32-arm64-msvc": "4.63.4", "@rollup/rollup-win32-ia32-msvc": "4.63.4", "@rollup/rollup-win32-x64-gnu": "4.63.4", "@rollup/rollup-win32-x64-msvc": "4.63.4", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ=="], + + "rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="], + + "rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="], + + "rollup-pluginutils": ["rollup-pluginutils@2.8.2", "", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "sander": ["sander@0.5.1", "", { "dependencies": { "es6-promise": "^3.1.2", "graceful-fs": "^4.1.3", "mkdirp": "^0.5.1", "rimraf": "^2.5.2" } }, "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA=="], + + "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "set-cookie-parser": ["set-cookie-parser@3.1.2", "", {}, "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw=="], + + "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.33.5", "@img/sharp-darwin-x64": "0.33.5", "@img/sharp-libvips-darwin-arm64": "1.0.4", "@img/sharp-libvips-darwin-x64": "1.0.4", "@img/sharp-libvips-linux-arm": "1.0.5", "@img/sharp-libvips-linux-arm64": "1.0.4", "@img/sharp-libvips-linux-s390x": "1.0.4", "@img/sharp-libvips-linux-x64": "1.0.4", "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", "@img/sharp-libvips-linuxmusl-x64": "1.0.4", "@img/sharp-linux-arm": "0.33.5", "@img/sharp-linux-arm64": "0.33.5", "@img/sharp-linux-s390x": "0.33.5", "@img/sharp-linux-x64": "0.33.5", "@img/sharp-linuxmusl-arm64": "0.33.5", "@img/sharp-linuxmusl-x64": "0.33.5", "@img/sharp-wasm32": "0.33.5", "@img/sharp-win32-ia32": "0.33.5", "@img/sharp-win32-x64": "0.33.5" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "sorcery": ["sorcery@0.11.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.14", "buffer-crc32": "^1.0.0", "minimist": "^1.2.0", "sander": "^0.5.0" }, "bin": { "sorcery": "bin/sorcery" } }, "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "sourcemap-codec": ["sourcemap-codec@1.4.8", "", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "stacktracey": ["stacktracey@2.2.0", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg=="], + + "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + + "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "svelte": ["svelte@4.2.20", "", { "dependencies": { "@ampproject/remapping": "^2.2.1", "@jridgewell/sourcemap-codec": "^1.4.15", "@jridgewell/trace-mapping": "^0.3.18", "@types/estree": "^1.0.1", "acorn": "^8.9.0", "aria-query": "^5.3.0", "axobject-query": "^4.0.0", "code-red": "^1.0.3", "css-tree": "^2.3.1", "estree-walker": "^3.0.3", "is-reference": "^3.0.1", "locate-character": "^3.0.0", "magic-string": "^0.30.4", "periscopic": "^3.1.0" } }, "sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q=="], + + "svelte-check": ["svelte-check@3.8.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.17", "chokidar": "^3.4.1", "picocolors": "^1.0.0", "sade": "^1.7.4", "svelte-preprocess": "^5.1.3", "typescript": "^5.0.3" }, "peerDependencies": { "svelte": "^3.55.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0" }, "bin": { "svelte-check": "bin/svelte-check" } }, "sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q=="], + + "svelte-hmr": ["svelte-hmr@0.16.0", "", { "peerDependencies": { "svelte": "^3.19.0 || ^4.0.0" } }, "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA=="], + + "svelte-preprocess": ["svelte-preprocess@5.1.4", "", { "dependencies": { "@types/pug": "^2.0.6", "detect-indent": "^6.1.0", "magic-string": "^0.30.5", "sorcery": "^0.11.0", "strip-indent": "^3.0.0" }, "peerDependencies": { "@babel/core": "^7.10.2", "coffeescript": "^2.5.1", "less": "^3.11.3 || ^4.0.0", "postcss": "^7 || ^8", "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", "pug": "^3.0.0", "sass": "^1.26.8", "stylus": "^0.55.0", "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["@babel/core", "coffeescript", "less", "postcss", "postcss-load-config", "pug", "sass", "stylus", "sugarss", "typescript"] }, "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA=="], + + "tailwindcss": ["tailwindcss@3.4.19", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.7", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2 || ^5.0 || ^6.0", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ=="], + + "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unenv": ["unenv@2.0.0-rc.14", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.1", "ohash": "^2.0.10", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "untildify": ["untildify@4.0.0", "", {}, "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="], + + "update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vite": ["vite@5.4.21", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="], + + "vitefu": ["vitefu@0.2.5", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["vite"] }, "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "workerd": ["workerd@1.20250718.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250718.0", "@cloudflare/workerd-darwin-arm64": "1.20250718.0", "@cloudflare/workerd-linux-64": "1.20250718.0", "@cloudflare/workerd-linux-arm64": "1.20250718.0", "@cloudflare/workerd-windows-64": "1.20250718.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg=="], + + "wrangler": ["wrangler@3.114.17", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/unenv-preset": "2.0.2", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250718.3", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.14", "workerd": "1.20250718.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250408.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + + "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], + + "zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + + "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@ionic/utils-process/@ionic/utils-terminal": ["@ionic/utils-terminal@2.3.3", "", { "dependencies": { "@types/slice-ansi": "^4.0.0", "debug": "^4.0.0", "signal-exit": "^3.0.3", "slice-ansi": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "tslib": "^2.0.1", "untildify": "^4.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ=="], + + "@ionic/utils-subprocess/@ionic/utils-fs": ["@ionic/utils-fs@3.1.6", "", { "dependencies": { "@types/fs-extra": "^8.0.0", "debug": "^4.0.0", "fs-extra": "^9.0.0", "tslib": "^2.0.1" } }, "sha512-eikrNkK89CfGPmexjTfSWl4EYqsPSBh0Ka7by4F0PLc1hJZYtJxUZV3X4r5ecA8ikjicUmcbU7zJmAjmqutG/w=="], + + "@ionic/utils-subprocess/@ionic/utils-terminal": ["@ionic/utils-terminal@2.3.3", "", { "dependencies": { "@types/slice-ansi": "^4.0.0", "debug": "^4.0.0", "signal-exit": "^3.0.3", "slice-ansi": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "tslib": "^2.0.1", "untildify": "^4.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-RnuSfNZ5fLEyX3R5mtcMY97cGD1A0NVBbarsSQ6yMMfRJ5YHU7hHVyUfvZeClbqkBC/pAqI/rYJuXKCT9YeMCQ=="], + + "chokidar/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "elementtree/sax": ["sax@1.1.4", "", {}, "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + + "miniflare/acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + + "rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="], + + "rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], + + "sander/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + + "sander/rimraf": ["rimraf@2.7.1", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w=="], + + "sorcery/buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="], + + "sucrase/commander": ["commander@4.1.1", "", {}, "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA=="], + + "tinyglobby/picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], + + "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "youch/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "sander/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="], + + "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], + + "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="], + + "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="], + + "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="], + + "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="], + + "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="], + + "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="], + + "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="], + + "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="], + + "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="], + + "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="], + + "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="], + + "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="], + + "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="], + + "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="], + + "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="], + + "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="], + + "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="], + + "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="], + + "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="], + + "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="], + + "sander/rimraf/glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "sander/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + } +} diff --git a/bun.lockb b/bun.lockb deleted file mode 100644 index 4293a4870cad216c40ae8ea75c205d2497b962ce..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 258906 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
n&qC??@>ZswD9HgW`lzN}nbXQ0 zrcY7mQ;z%WE&gst7wYpt4UFD?PkmW0PJFTAbLBoJk(p1x*qe5{r4IVDMqtVQOS>WIy~oJRbWp29+{+ zqn9#PGEbFP{;ju(L!*FdjJtKyUSp=3Lx#N3f zdzd;!c%oXR;zQDuyktidqFgXN)`<;CQ3M&0l#0(D7ff<6 z4U4DB1qpc~O0Y!X8RCd1{r)GVlPS?U^|w>J=r_8uNv85a(CEA%)&Zj!tg8DgOM+lR z-fl7hA>9}hcp#Z+SEqILjFe399<3}X5{yX%tY~(2xgv_ZgV%#?O^q#45b3Yje1fKq znK8!Jwvgs-bd{$q>lZ_;hqhS#0a+YY?&&hc9LJmHf{_P@WNKHYz+gkdr=YHqptgX; znv|ok%3@E6ooZVQ1oET0uBy7ajIyi7#Y3H~8-!Sfhg}X^NlV+1;aR}sP_C$VZ=h^Qt-GPJ2DeGVyi6@ND9bSDf1_41FhEHjt=oS%?i#)Y9HIpkCHYEKi1{|Vx5je}bGB%`w&e?+>pDuc%Ri=r>3QAWr14ybD4)#8e zxTPS$or=SPoc=6wsHUWe3XZtBeIoaZJ;e;}aoTF4p30AL zTCP~CvU#eOXJrEOfY29GMI2D8{sSE?1Y!j0vdrkw`ow{U7OU=RHc+`yn#8`XYG-sk z3515d41;~5VjIJ~^C8i>ZBn>+EbW|jDVc+bmWJdTGrDa#-j8C|(*kl8ee=dtRvvlO zdFAM`g!F_`Y9$8dBq>}gWpcA!s(5|(cT5gptqF<78&=d^jsxpPX%=e=mknqffSISy zti`i1ffd}ta5o!>-6#_<^Ljw!Q)Pl66{pk4T7XU?*UdRJ>OK7!=K)O|rgmZOw0h&r zA7T!+AzeloGd$`H=SoX!MA7%~!OaF9H>%=hj+sodnSz4G$!p0l&I1m$N?C%*CX1AX zV+e776DTed#+IF#Hlu}xLzu;7$yDgjc*0TMsD?m0G1a0&(4r=>!QA(TgY?$W-hnI! zDNueAH|$ZzHylmJ{08|%zqZW*?xr}Ijw>{-_ZALWuNDpsf_IMg>qAMnUdlm#?V zkSA2E#>%V8a_Xe3FW2=Ntfe_uS@S!Rbh8O<4wUw$kP0S;$#|YsX7DJeBXFn}rf3B? z71v2PgrSdVE=R=9JXE`Ahmz{+g)ehbdFaOV%5YF4aC?Ihub3W3+CqA5K-zS4lW8`Y z59=$v8La$`W6zE2E=(L|?g-zyW?ZYAYZx{} zc_P@9*P;94n*hhRo+>Aqgy}H^6%M<+O>bt)+AyS@AnuFxfVBzGKH(ZBT?7h~NI?x{ zA;!AGWY2P|OKDyxT+nHk^wCc5H4QvPwu|zbiwPkA7LA9euKhypi-%^rY)grlS3}Y- zko`|{O6!M~BXZu2I>`?f7Ar5-Jz1@|UZ0O`i3gcxiQPv}RmroiBU6S6sIZ50gBY&? zGiA)EoAgr%g8on73EL_0Eea@q(S~g2L}81V>8Y-h3j|J#V#5<%Q~;n+5H(YFc>JTo zH1<+@CSW#{EXzE!N)bGGZ|;Dbpi&tc#Uj#ND)kV4fXX_~+S_8%FaaGwLh@T$)m+z* OwlIOD@kaia{`-H*LI^eh diff --git a/capacitor.config.ts b/capacitor.config.ts deleted file mode 100644 index 20d1c67..0000000 --- a/capacitor.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { CapacitorConfig } from '@capacitor/cli'; - -const config: CapacitorConfig = { - appId: 'com.vtempest.automata', - appName: 'Automated Automata', - webDir: 'build' -}; - -export default config; diff --git a/mobile.md b/mobile.md deleted file mode 100644 index f446ca8..0000000 --- a/mobile.md +++ /dev/null @@ -1,6 +0,0 @@ -bun i @capacitor/core -bun i -D @capacitor/cli -bunx cap init -bun i @capacitor/android -bun cap add android - diff --git a/package.json b/package.json index 640144f..8e1e956 100644 --- a/package.json +++ b/package.json @@ -1,44 +1,37 @@ { - "name": "automata-game-of-life", - "version": "2.0.0", + "name": "automate-automata", + "private": true, + "version": "3.0.0", + "description": "Cellular automata for the web, Android and the terminal \u2014 engine, UI components, CLI and apps.", + "type": "module", + "packageManager": "bun@1.3.11", + "workspaces": [ + "packages/*", + "apps/web", + "apps/android" + ], "scripts": { - "dev": "vite dev", - "build": "vite build ", - "preview": "vite preview", - "package": "svelte-kit sync && svelte-package && publint", - "deploy": "wrangler deploy && wrangler tail", - "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json" - }, - "peerDependencies": { - "svelte": "^4.0.0" + "build": "bun run build:engine && bun run build:cli && bun run build:web", + "build:engine": "cd packages/complexity-automata && bun run build", + "build:cli": "cd packages/complexity-automata-cli && bun run build", + "build:web": "cd apps/web && bun run build", + "dev": "cd apps/web && bun run dev", + "cli": "cd packages/complexity-automata-cli && bun run start", + "test": "bun run test:engine && bun run test:cli && bun run test:ui", + "typecheck": "cd packages/complexity-automata && bun run typecheck && cd ../complexity-automata-cli && bun run typecheck", + "android": "cd apps/android && bun run build", + "deploy": "cd apps/web && bun run deploy", + "test:engine": "cd packages/complexity-automata && bun test", + "test:cli": "cd packages/complexity-automata-cli && bun test", + "test:ui": "cd packages/complexity-automata-svelte && bun test" }, - "devDependencies": { - "@capacitor/cli": "^6.1.1", - "@sveltejs/adapter-auto": "^3.0.0", - "@sveltejs/adapter-cloudflare": "^4.6.0", - "@sveltejs/kit": "^2.0.0", - "@sveltejs/package": "^2.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0", - "publint": "^0.1.9", - "svelte": "^4.2.7", - "svelte-check": "^3.6.0", - "tslib": "^2.4.1", - "typescript": "^5.0.0", - "vite": "^5.0.11" + "engines": { + "node": ">=18", + "bun": ">=1.1" }, - "type": "module", - "dependencies": { - "@capacitor/android": "^6.1.1", - "@capacitor/core": "^6.1.1", - "@sveltejs/adapter-cloudflare-workers": "^2.4.0", - "@sveltejs/adapter-static": "^3.0.2", - "autoprefixer": "^10.4.19", - "bootstrap": "^5.3.3", - "lucide-svelte": "^0.399.0", - "postcss": "^8.4.38", - "tailwind": "^4.0.0", - "tailwindcss": "^3.4.3", - "vite-plugin-sveld": "^1.1.0", - "wrangler": "3.57" + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenSourceAGI/Automate-Automata.git" } } diff --git a/packages/complexity-automata-cli/README.md b/packages/complexity-automata-cli/README.md new file mode 100644 index 0000000..4ecf943 --- /dev/null +++ b/packages/complexity-automata-cli/README.md @@ -0,0 +1,107 @@ +# complexity-automata-cli + +Cellular automata animation in your terminal. Conway's Game of Life, fractal +replicators and the 262,143 other rules, drawn in truecolor half-blocks at two +grid rows per terminal line. + +```bash +npx complexity-automata-cli # or: npm i -g complexity-automata-cli +automata +``` + +## What you get + +``` +automata Conway's Game of Life — gliders, a gun, an acorn +automata fractal a Sierpinski triangle grown from one cell +automata gun -f 30 Gosper's glider gun, fast +automata --rule highlife --auto a tour of random universes that stay alive +automata -p "Glider Gun" stamp a pattern and watch it run +automata --load my-universe reopen something you saved +``` + +`automata demos`, `automata patterns` and `automata rules` list what there is. + +## Keys + +| key | | +| --- | --- | +| `space` | play / pause | +| `n` | jump to a random rule | +| `a` | automatic mode — new universe whenever this one stops evolving | +| `r` | random soup | +| `c` | clear the grid | +| `p` | stamp a random pattern | +| `d` | next demo | +| `s` | save this universe | +| `+` `-` | faster / slower | +| `q` | quit | + +## Options + +``` +-d, --demo start from a demo (default: conway) +-r, --rule rule to run, e.g. B3/S23, 6152, highlife +-p, --pattern stamp a pattern in the centre, e.g. "Glider Gun" +-l, --load load a saved universe: file path, saved name, or - +-s, --save write the universe to a file when the run ends + --name save to the library under this name when the run ends +-f, --fps generations per second (default 20) +-g, --generations stop after n generations +-W, --width grid width in cells (default: terminal width) +-H, --height grid height in cells (default: terminal height × 2) + --density <0..1> fraction of cells alive when seeding randomly + --seed RNG seed, so a run can be reproduced exactly + --no-wrap bound the edges instead of wrapping into a torus +-a, --auto automatic mode + --color auto, truecolor, 256 or mono + --mono, --ascii no colour / plain ASCII output +-q, --quiet hide the status bar + --print render one frame to stdout and exit + --intro show the intro screen first +``` + +## In a pipe + +Without a TTY — or with `--print` — it runs the generations you ask for and +writes a single frame to stdout, which is what makes it usable in scripts, in a +README, or as a CI check. + +```console +$ automata gliders --print -g 30 -W 68 -H 26 --ascii + ## # # + ### + ### + # # # + ### # # ## ## + # # # ## + # ## # # # + # # ## # # # + ### ### # # # +``` + +## Saving universes + +`s` while running, or `--name`, writes to `~/.complexity-automata/universes.json`. +`--save ` writes a single universe file instead. + +```bash +automata gun --generations 500 --save gun.json +automata --load gun.json +automata saved # what is on the shelf +automata forget "old thing" # take it off +``` + +Universes are stored as their rule plus their live cells in +[Life RLE](https://conwaylife.com/book/#rle_files), so the same file opens in the +web app, in other Life software, and as a shareable link. Set +`COMPLEXITY_AUTOMATA_HOME` to keep the shelf somewhere else. + +## Colour + +Truecolor by default, falling back to the 256 colour palette or to plain blocks. +`NO_COLOR` is respected, and output that is not a TTY is never coloured. + +Built on [`complexity-automata`](https://www.npmjs.com/package/complexity-automata). + +MIT diff --git a/packages/complexity-automata-cli/package.json b/packages/complexity-automata-cli/package.json new file mode 100644 index 0000000..f424e71 --- /dev/null +++ b/packages/complexity-automata-cli/package.json @@ -0,0 +1,54 @@ +{ + "name": "complexity-automata-cli", + "version": "1.0.0", + "description": "Cellular automata animation in your terminal — Conway's Game of Life, fractal replicators and 262,144 other rules, in truecolor half-blocks.", + "keywords": [ + "cli", + "terminal", + "animation", + "cellular-automata", + "game-of-life", + "conway", + "ansi", + "screensaver" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenSourceAGI/Automate-Automata.git", + "directory": "packages/complexity-automata-cli" + }, + "homepage": "https://github.com/OpenSourceAGI/Automate-Automata/tree/master/packages/complexity-automata-cli#readme", + "type": "module", + "main": "./dist/cli.js", + "types": "./dist/cli.d.ts", + "bin": { + "automata": "./dist/cli.js", + "complexity-automata": "./dist/cli.js" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc -p tsconfig.json && chmod +x dist/cli.js", + "start": "bun run src/cli.ts", + "test": "bun test", + "typecheck": "tsc -p tsconfig.json --noEmit", + "prepublishOnly": "bun run build" + }, + "dependencies": { + "complexity-automata": "workspace:*" + }, + "devDependencies": { + "@types/bun": "^1.1.10", + "@types/node": "^22.5.0", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/complexity-automata-cli/src/args.ts b/packages/complexity-automata-cli/src/args.ts new file mode 100644 index 0000000..55ffc97 --- /dev/null +++ b/packages/complexity-automata-cli/src/args.ts @@ -0,0 +1,258 @@ +/** + * Command line parsing, kept separate from the animation loop so it can be + * tested without a terminal. + */ + +export type Command = 'run' | 'help' | 'version' | 'demos' | 'patterns' | 'rules' | 'saved' | 'forget'; + +export type ColorChoice = 'auto' | 'truecolor' | '256' | 'mono'; + +export interface CliOptions { + command: Command; + /** Demo id to start from. */ + demo?: string; + /** Rule override, in any notation the engine accepts. */ + rule?: string; + /** Pattern to stamp in the centre. */ + pattern?: string; + /** A saved universe: a file path, a name in the library, or `-` for stdin. */ + load?: string; + /** Write the universe to this file when the run ends. */ + save?: string; + /** Save to the library under this name when the run ends. */ + name?: string; + /** Name to forget, for the `forget` command. */ + target?: string; + fps: number; + /** Stop after this many generations instead of running until quit. */ + generations?: number; + width?: number; + height?: number; + density?: number; + seed?: number; + wrap: boolean; + color: ColorChoice; + ascii: boolean; + /** Automatic mode: keep jumping to new rules when a universe stops evolving. */ + auto: boolean; + /** Hide the status bar. */ + quiet: boolean; + /** Render one frame to stdout and exit — for pipes, screenshots and CI. */ + print: boolean; + /** Show the intro screen before the animation starts. */ + intro: boolean; +} + +export interface ParseResult { + options: CliOptions; + errors: string[]; +} + +const DEFAULTS: CliOptions = { + command: 'run', + fps: 20, + wrap: true, + color: 'auto', + ascii: false, + auto: false, + quiet: false, + print: false, + intro: false, +}; + +const COMMANDS = new Set(['run', 'help', 'version', 'demos', 'patterns', 'rules', 'saved', 'forget']); + +/** Parse `process.argv.slice(2)`. Unknown flags are reported, not thrown. */ +export function parseArgs(argv: string[]): ParseResult { + const options: CliOptions = { ...DEFAULTS }; + const errors: string[] = []; + const positional: string[] = []; + + const number = (flag: string, raw: string | undefined, { min = -Infinity, max = Infinity } = {}): number | undefined => { + const value = Number(raw); + if (raw === undefined || Number.isNaN(value)) { + errors.push(`${flag} needs a number`); + return undefined; + } + if (value < min || value > max) { + errors.push(`${flag} must be between ${min} and ${max}`); + return undefined; + } + return value; + }; + + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]!; + const next = (): string | undefined => argv[++i]; + + switch (arg) { + case '-h': + case '--help': + options.command = 'help'; + break; + case '-v': + case '--version': + options.command = 'version'; + break; + case '-d': + case '--demo': + options.demo = next(); + if (!options.demo) errors.push('--demo needs a demo id (try `automata demos`)'); + break; + case '-r': + case '--rule': + options.rule = next(); + if (!options.rule) errors.push('--rule needs a rule like B3/S23'); + break; + case '-p': + case '--pattern': + options.pattern = next(); + if (!options.pattern) errors.push('--pattern needs a pattern name (try `automata patterns`)'); + break; + case '-l': + case '--load': + options.load = next(); + if (!options.load) errors.push('--load needs a file, a saved name, or - for stdin'); + break; + case '-s': + case '--save': + options.save = next(); + if (!options.save) errors.push('--save needs a file path'); + break; + case '--name': + options.name = next(); + if (!options.name) errors.push('--name needs a name to save under'); + break; + case '-f': + case '--fps': + options.fps = number(arg, next(), { min: 0.1, max: 240 }) ?? options.fps; + break; + case '-g': + case '--generations': + options.generations = number(arg, next(), { min: 1 }); + break; + case '-W': + case '--width': + options.width = number(arg, next(), { min: 4, max: 4096 }); + break; + case '-H': + case '--height': + options.height = number(arg, next(), { min: 4, max: 4096 }); + break; + case '--density': + options.density = number(arg, next(), { min: 0, max: 1 }); + break; + case '--seed': + options.seed = number(arg, next()); + break; + case '--wrap': + options.wrap = true; + break; + case '--no-wrap': + options.wrap = false; + break; + case '-a': + case '--auto': + options.auto = true; + break; + case '--ascii': + options.ascii = true; + break; + case '--mono': + options.color = 'mono'; + break; + case '--no-color': + case '--no-colour': + options.color = 'mono'; + break; + case '--color': + case '--colour': { + const value = next(); + if (value === 'auto' || value === 'truecolor' || value === '256' || value === 'mono') { + options.color = value; + } else if (value === 'none' || value === 'never') { + options.color = 'mono'; + } else { + errors.push('--color takes auto, truecolor, 256 or mono'); + } + break; + } + case '-q': + case '--quiet': + options.quiet = true; + break; + case '--print': + options.print = true; + break; + case '--intro': + options.intro = true; + break; + default: + if (arg.startsWith('-') && arg !== '-') errors.push(`unknown option: ${arg}`); + else positional.push(arg); + } + } + + if (positional.length > 0) { + const [first, ...rest] = positional; + if (COMMANDS.has(first as Command)) { + options.command = first as Command; + if (options.command === 'forget') { + options.target = rest.join(' '); + if (!options.target) errors.push('forget needs the name of a saved universe'); + } + } else if (!options.demo) { + // `automata fractal` is shorthand for `automata --demo fractal`. + options.demo = first; + } + } + + if (options.print) options.generations ??= 0; + + return { options, errors }; +} + +export const HELP = `complexity-automata — cellular automata in your terminal + +Usage + automata [demo] [options] + automata demos | patterns | rules | saved + automata forget + +Options + -d, --demo start from a demo (default: conway) + -r, --rule rule to run, e.g. B3/S23, 6152, highlife + -p, --pattern stamp a pattern in the centre, e.g. "Glider Gun" + -l, --load load a saved universe: file path, saved name, or - + -s, --save write the universe to a file when the run ends + --name save to the library under this name when the run ends + -f, --fps generations per second (default 20) + -g, --generations stop after n generations + -W, --width grid width in cells (default: terminal width) + -H, --height grid height in cells (default: terminal height × 2) + --density <0..1> fraction of cells alive when seeding randomly + --seed RNG seed, so a run can be reproduced exactly + --no-wrap bound the edges instead of wrapping into a torus + -a, --auto automatic mode: tour random rules that stay alive + --color auto, truecolor, 256 or mono + --mono, --ascii no colour / plain ASCII output + -q, --quiet hide the status bar + --print render one frame to stdout and exit + --intro show the intro screen first + -h, --help this help + -v, --version print the version + +Keys while running + space play / pause n next random rule + a automatic mode r random soup + c clear the grid p stamp a random pattern + d next demo s save this universe + + / - faster / slower q quit + +Examples + automata Conway's Game of Life, gliders and a gun + automata fractal a Sierpinski triangle from one cell + automata --rule highlife --auto tour random universes at HighLife speed + automata -p "Glider Gun" -f 30 the gun, fast + automata --print -g 200 > life.txt 200 generations, one frame, piped +`; diff --git a/packages/complexity-automata-cli/src/cli.ts b/packages/complexity-automata-cli/src/cli.ts new file mode 100644 index 0000000..c56c32d --- /dev/null +++ b/packages/complexity-automata-cli/src/cli.ts @@ -0,0 +1,320 @@ +#!/usr/bin/env node +/** + * `automata` — the terminal animation. + * + * Reads stdin (for `--load -`), takes over the alternate screen buffer, and + * redraws one frame per generation. Everything it knows about cellular automata + * comes from the `complexity-automata` engine; this file is the terminal. + */ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { basename } from 'node:path'; +import { Autopilot, demos, getDemo, patterns } from 'complexity-automata'; +import { HELP, parseArgs, type CliOptions } from './args.js'; +import { openLibrary } from './library.js'; +import { renderIntro } from './intro.js'; +import { + ALT_SCREEN_OFF, + ALT_SCREEN_ON, + CLEAR_SCREEN, + frameTo, + gridHeightForRows, + HIDE_CURSOR, + renderFrame, + RESET, + SHOW_CURSOR, + type ColorMode, + type RenderOptions, +} from './render.js'; +import { autoName, buildUniverse, demoList, patternList, savedList, snapshot, statusLine } from './session.js'; + +const VERSION = '1.0.0'; + +/** What the terminal can actually display, unless the user overrode it. */ +function detectColor(choice: CliOptions['color'], isTTY: boolean): ColorMode { + if (choice === 'mono') return 'mono'; + if (choice === 'truecolor') return 'truecolor'; + if (choice === '256') return 'ansi256'; + if (process.env.NO_COLOR !== undefined || !isTTY) return 'mono'; + const term = `${process.env.COLORTERM ?? ''} ${process.env.TERM ?? ''}`.toLowerCase(); + if (term.includes('truecolor') || term.includes('24bit')) return 'truecolor'; + if (term.includes('256')) return 'ansi256'; + // Most terminals in use handle 24 bit colour even without advertising it. + return term.includes('dumb') ? 'mono' : 'truecolor'; +} + +function readSource(load: string): string { + if (load === '-') return readFileSync(0, 'utf8'); + try { + return readFileSync(load, 'utf8'); + } catch { + const saved = openLibrary().get(load); + if (!saved) throw new Error(`cannot read universe: ${load}`); + return JSON.stringify(saved); + } +} + +export async function main(argv: string[] = process.argv.slice(2)): Promise { + const { options, errors } = parseArgs(argv); + if (errors.length > 0) { + for (const error of errors) process.stderr.write(`automata: ${error}\n`); + process.stderr.write('\nTry `automata --help`.\n'); + return 1; + } + + switch (options.command) { + case 'help': + process.stdout.write(HELP); + return 0; + case 'version': + process.stdout.write(`${VERSION}\n`); + return 0; + case 'demos': + process.stdout.write(`${demoList()}\n`); + return 0; + case 'patterns': + process.stdout.write(`${patternList()}\n`); + return 0; + case 'rules': + process.stdout.write( + `${demos.map((d) => ` ${d.rule.padEnd(16)} ${d.name}`).join('\n')}\n` + + `\n Any of the 262,144 rule numbers also works: automata --rule 6152\n`, + ); + return 0; + case 'saved': + process.stdout.write(`${savedList(openLibrary().list())}\n`); + return 0; + case 'forget': { + const removed = openLibrary().remove(options.target ?? ''); + process.stdout.write(removed ? `forgot ${options.target}\n` : `no saved universe called ${options.target}\n`); + return removed ? 0 : 1; + } + } + + const isTTY = Boolean(process.stdout.isTTY); + const color = detectColor(options.color, isTTY); + const render: RenderOptions = { color, ascii: options.ascii }; + + const columns = options.width ?? (process.stdout.columns || 80); + const rows = process.stdout.rows || 24; + const height = options.height ?? gridHeightForRows(rows, options.quiet ? 0 : 1, options.ascii); + + let source: string | undefined; + if (options.load) { + try { + source = readSource(options.load); + } catch (error) { + process.stderr.write(`automata: ${(error as Error).message}\n`); + return 1; + } + } + + let built; + try { + built = buildUniverse(options, { width: columns, height }, source); + } catch (error) { + process.stderr.write(`automata: ${(error as Error).message}\n`); + return 1; + } + + let { automata } = built; + const { title } = built; + + // Non-interactive: run the requested generations and print one frame. This is + // what makes the CLI useful in a pipe, a README, or a CI check. + if (options.print || !isTTY) { + automata.run(options.generations ?? 0); + process.stdout.write(`${renderFrame(automata, render)}\n`); + if (options.save) writeFileSync(options.save, JSON.stringify(snapshot(automata, options.name ?? title), null, 2)); + if (options.name && !options.save) openLibrary().save(automata, { name: options.name }); + return 0; + } + + return runInteractive(automata, title, options, render, rows); +} + +function runInteractive( + automata: import('complexity-automata').Automata, + title: string, + options: CliOptions, + render: RenderOptions, + initialRows: number, +): Promise { + return new Promise((resolve) => { + let playing = true; + let fps = options.fps; + let auto = options.auto; + let demoIndex = Math.max(0, demos.findIndex((d) => d.id === (getDemo(options.demo ?? 'conway')?.id ?? 'conway'))); + let status: string | undefined; + let statusUntil = 0; + let rows = initialRows; + let timer: ReturnType | undefined; + let showingIntro = options.intro; + + const pilot = new Autopilot(automata, { + onUniverse: (change) => flash(`new universe: ${change.rule} (#${change.ruleNumber})`), + }); + + const out = (text: string): void => void process.stdout.write(text); + + const flash = (message: string): void => { + status = message; + statusUntil = Date.now() + 2500; + }; + + const draw = (): void => { + const frame = showingIntro + ? renderIntro(automata.clone(), render) + : frameTo( + automata.width, + automata.height, + renderFrame(automata, render), + options.quiet + ? '' + : statusLine( + automata, + { + title, + playing, + auto, + fps: Math.round(fps), + saved: status && Date.now() < statusUntil ? status : undefined, + }, + automata.width, + ), + ); + out(showingIntro ? `\u001B[H${frame}` : frame); + }; + + const tick = (): void => { + if (playing && !showingIntro) { + automata.step(); + if (auto) pilot.tick(); + } + draw(); + if (options.generations && automata.generation >= options.generations) return finish(0); + timer = setTimeout(tick, Math.max(4, 1000 / fps)); + }; + + const resize = (): void => { + const width = options.width ?? (process.stdout.columns || automata.width); + rows = process.stdout.rows || rows; + const height = options.height ?? gridHeightForRows(rows, options.quiet ? 0 : 1, options.ascii); + if (width === automata.width && height === automata.height) return; + automata = automata.resized(width, height); + out(CLEAR_SCREEN); + }; + + const finish = (code: number): void => { + if (timer) clearTimeout(timer); + process.stdout.off('resize', resize); + if (process.stdin.isTTY) process.stdin.setRawMode(false); + process.stdin.pause(); + out(`${SHOW_CURSOR}${ALT_SCREEN_OFF}${RESET}`); + + if (options.save) { + writeFileSync(options.save, JSON.stringify(snapshot(automata, options.name ?? title), null, 2)); + process.stdout.write(`saved ${options.save}\n`); + } else if (options.name) { + openLibrary().save(automata, { name: options.name }); + process.stdout.write(`saved "${options.name}" to your library\n`); + } + resolve(code); + }; + + const onKey = (data: Buffer): void => { + const key = data.toString('utf8'); + + if (showingIntro) { + showingIntro = false; + out(CLEAR_SCREEN); + if (key === 'q' || key === '\u0003') return finish(0); + return; + } + + switch (key) { + case 'q': + case '\u0003': // ctrl-c + return finish(0); + case ' ': + playing = !playing; + flash(playing ? 'playing' : 'paused'); + break; + case 'n': + automata.randomRule(); + flash(`rule ${automata.ruleString} (#${automata.ruleNumber})`); + break; + case 'a': + auto = !auto; + flash(auto ? 'automatic mode on' : 'automatic mode off'); + if (auto) pilot.next(); + break; + case 'r': + automata.randomize(options.density ?? 0.28); + flash('random soup'); + break; + case 'c': + automata.clear(); + flash('cleared'); + break; + case 'p': { + const stampable = patterns.filter((p) => p.cells.length > 0); + const pattern = stampable[Math.floor(automata.random() * stampable.length)]!; + automata.stampRandom(pattern); + flash(`stamped ${pattern.name}`); + break; + } + case 'd': { + demoIndex = (demoIndex + 1) % demos.length; + const demo = demos[demoIndex]!; + automata = demo.build({ width: automata.width, height: automata.height, wrap: options.wrap }); + flash(`${demo.name} — ${demo.rule}`); + break; + } + case 's': { + const name = autoName(automata, title); + openLibrary().save(automata, { name }); + flash(`saved "${name}"`); + break; + } + case '+': + case '=': + fps = Math.min(120, fps * 1.5); + flash(`${Math.round(fps)} fps`); + break; + case '-': + case '_': + fps = Math.max(1, fps / 1.5); + flash(`${Math.round(fps)} fps`); + break; + case '\u001B': // esc + flash('press q to quit'); + break; + } + }; + + if (process.stdin.isTTY) process.stdin.setRawMode(true); + process.stdin.resume(); + process.stdin.on('data', onKey); + process.stdout.on('resize', resize); + process.on('SIGINT', () => finish(0)); + process.on('SIGTERM', () => finish(0)); + + out(`${ALT_SCREEN_ON}${HIDE_CURSOR}${CLEAR_SCREEN}`); + tick(); + }); +} + +// Only run when invoked as a program, so tests can import `main` without +// starting an animation. Matching on the file name rather than the path keeps +// a directory called `complexity-automata-cli` from counting as an invocation. +const invokedAs = process.argv[1] ? basename(process.argv[1]) : ''; +if (['cli.js', 'cli.ts', 'automata', 'complexity-automata'].includes(invokedAs)) { + main().then( + (code) => process.exit(code), + (error: Error) => { + process.stderr.write(`automata: ${error.message}\n`); + process.exit(1); + }, + ); +} diff --git a/packages/complexity-automata-cli/src/intro.ts b/packages/complexity-automata-cli/src/intro.ts new file mode 100644 index 0000000..5827313 --- /dev/null +++ b/packages/complexity-automata-cli/src/intro.ts @@ -0,0 +1,59 @@ +/** + * The intro screen: Conway's Game of Life running behind a panel that says + * what you are looking at and which keys do what. + * + * The panel is cut out of the grid itself — the cells under it are cleared so + * the text sits in empty space — and then written with cursor positioning, so + * the animation keeps running around the edges of it. + */ + +import type { Automata } from 'complexity-automata'; +import { bold, dim, renderFrame, RESET, type RenderOptions } from './render.js'; + +const ESC = '\u001B['; + +export const INTRO_LINES = [ + '', + " C O M P L E X I T Y A U T O M A T A", + '', + " Conway's Game of Life, and 262,143 other universes.", + '', + ' A cell is born when it has exactly 3 live neighbours,', + ' and survives with 2 or 3. That is the whole rule — the', + ' gliders, guns and fractals all follow from it.', + '', + ' space play/pause n next rule a automatic mode', + ' p stamp pattern r random soup s save universe', + ' d next demo + / - speed q quit', + '', + ' press any key to begin', + '', +]; + +/** Clear a box in the middle of the grid and write the intro text into it. */ +export function renderIntro(automata: Automata, options: RenderOptions = {}): string { + const lines = INTRO_LINES; + const boxWidth = Math.max(...lines.map((l) => l.length)) + 2; + const boxHeight = lines.length; + + // Terminal rows are two grid rows tall, so the box is measured in char rows. + const charRows = Math.ceil(automata.height / 2); + const left = Math.max(0, Math.floor((automata.width - boxWidth) / 2)); + const top = Math.max(0, Math.floor((charRows - boxHeight) / 2)); + + for (let row = top; row < Math.min(charRows, top + boxHeight); row++) { + for (let x = left; x < Math.min(automata.width, left + boxWidth); x++) { + automata.set(x, row * 2, false); + automata.set(x, row * 2 + 1, false); + } + } + + const out: string[] = [renderFrame(automata, options)]; + lines.forEach((line, i) => { + const row = top + i + 1; // ANSI rows are 1-based + const column = left + 1; + const text = i === 1 ? bold(line) : i === lines.length - 2 ? dim(line) : line; + out.push(`${ESC}${row};${column}H${RESET}${text}${RESET}`); + }); + return out.join(''); +} diff --git a/packages/complexity-automata-cli/src/library.ts b/packages/complexity-automata-cli/src/library.ts new file mode 100644 index 0000000..ad5ae0b --- /dev/null +++ b/packages/complexity-automata-cli/src/library.ts @@ -0,0 +1,40 @@ +/** + * The CLI's shelf of saved universes, kept in `~/.complexity-automata/universes.json` + * so a universe saved in the terminal survives the session. + */ + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { homedir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { UniverseLibrary, type UniverseStorage } from 'complexity-automata'; + +/** Where saved universes live, overridable with `COMPLEXITY_AUTOMATA_HOME`. */ +export function libraryPath(): string { + const home = process.env.COMPLEXITY_AUTOMATA_HOME ?? join(homedir(), '.complexity-automata'); + return join(home, 'universes.json'); +} + +/** A {@link UniverseStorage} backed by a single JSON file. */ +export function fileStorage(path = libraryPath()): UniverseStorage { + return { + getItem(): string | null { + try { + return existsSync(path) ? readFileSync(path, 'utf8') : null; + } catch { + return null; + } + }, + setItem(_key, value): void { + mkdirSync(dirname(path), { recursive: true }); + writeFileSync(path, value, 'utf8'); + }, + removeItem(): void { + if (existsSync(path)) writeFileSync(path, '[]', 'utf8'); + }, + }; +} + +/** The saved-universe library for this machine. */ +export function openLibrary(path = libraryPath()): UniverseLibrary { + return new UniverseLibrary(fileStorage(path), 'universes'); +} diff --git a/packages/complexity-automata-cli/src/render.ts b/packages/complexity-automata-cli/src/render.ts new file mode 100644 index 0000000..f4f1599 --- /dev/null +++ b/packages/complexity-automata-cli/src/render.ts @@ -0,0 +1,147 @@ +/** + * Terminal rendering. + * + * A terminal cell is about twice as tall as it is wide, so every character + * shows two grid rows: the half block `▀` painted with the top cell's colour as + * foreground and the bottom cell's colour as background. That gives square + * pixels and doubles the vertical resolution for free. + * + * These functions are pure — they return the string to print, which is what + * makes the renderer testable without a terminal. + */ + +import type { Automata, RGB } from 'complexity-automata'; + +export type ColorMode = 'truecolor' | 'ansi256' | 'mono'; + +export interface RenderOptions { + /** How much colour the terminal can take. */ + color?: ColorMode; + /** Use `#`/`.` instead of block characters, for terminals without them. */ + ascii?: boolean; + /** Background colour for dead cells. Default black. */ + background?: RGB; + /** Colour for live cells in `mono` mode. Default the terminal's default. */ + foreground?: RGB; +} + +const ESC = '\u001B['; +export const RESET = `${ESC}0m`; +export const HIDE_CURSOR = `${ESC}?25l`; +export const SHOW_CURSOR = `${ESC}?25h`; +export const CLEAR_SCREEN = `${ESC}2J${ESC}H`; +export const HOME = `${ESC}H`; +export const ALT_SCREEN_ON = `${ESC}?1049h`; +export const ALT_SCREEN_OFF = `${ESC}?1049l`; + +const UPPER_HALF = '▀'; // ▀ +const FULL_BLOCK = '█'; // █ + +/** Nearest xterm-256 colour index for an RGB triple. */ +export function toAnsi256([r, g, b]: RGB): number { + // Greys have their own ramp at the end of the palette and look much better + // than the 6×6×6 cube's nearest match. + if (Math.abs(r - g) < 8 && Math.abs(g - b) < 8) { + if (r < 8) return 16; + if (r > 248) return 231; + return 232 + Math.round(((r - 8) / 247) * 24); + } + const level = (v: number): number => Math.round((v / 255) * 5); + return 16 + 36 * level(r) + 6 * level(g) + level(b); +} + +const fg = (color: RGB, mode: ColorMode): string => + mode === 'truecolor' + ? `${ESC}38;2;${color[0]};${color[1]};${color[2]}m` + : `${ESC}38;5;${toAnsi256(color)}m`; + +const bg = (color: RGB, mode: ColorMode): string => + mode === 'truecolor' + ? `${ESC}48;2;${color[0]};${color[1]};${color[2]}m` + : `${ESC}48;5;${toAnsi256(color)}m`; + +const sameColor = (a: RGB | null, b: RGB | null): boolean => + a === b || (a !== null && b !== null && a[0] === b[0] && a[1] === b[1] && a[2] === b[2]); + +/** + * One frame of the animation, without a trailing newline. + * + * Consecutive cells of the same colour share one escape sequence, which keeps + * a full-screen frame down to a few kilobytes instead of tens of them. + */ +export function renderFrame(automata: Automata, options: RenderOptions = {}): string { + const mode = options.color ?? 'truecolor'; + const background = options.background ?? ([0, 0, 0] as RGB); + + if (options.ascii) return renderAscii(automata, mode, options); + + const lines: string[] = []; + for (let y = 0; y < automata.height; y += 2) { + let line = ''; + let currentFg: RGB | null = null; + let currentBg: RGB | null = null; + + for (let x = 0; x < automata.width; x++) { + const topAlive = automata.get(x, y) === 1; + const bottomAlive = y + 1 < automata.height && automata.get(x, y + 1) === 1; + + if (mode === 'mono') { + line += topAlive && bottomAlive ? FULL_BLOCK : topAlive ? UPPER_HALF : bottomAlive ? '▄' : ' '; + continue; + } + + const top = topAlive ? cellColor(automata, x, y, options) : background; + const bottom = bottomAlive ? cellColor(automata, x, y + 1, options) : background; + if (!sameColor(top, currentFg)) { + line += fg(top, mode); + currentFg = top; + } + if (!sameColor(bottom, currentBg)) { + line += bg(bottom, mode); + currentBg = bottom; + } + line += UPPER_HALF; + } + lines.push(mode === 'mono' ? line : line + RESET); + } + return lines.join('\n'); +} + +function cellColor(automata: Automata, x: number, y: number, options: RenderOptions): RGB { + const color = automata.colorAt(x, y); + if (color[0] || color[1] || color[2]) return color; + return options.foreground ?? ([220, 220, 220] as RGB); +} + +function renderAscii(automata: Automata, _mode: ColorMode, _options: RenderOptions): string { + const lines: string[] = []; + for (let y = 0; y < automata.height; y++) { + let line = ''; + for (let x = 0; x < automata.width; x++) line += automata.get(x, y) ? '#' : ' '; + lines.push(line); + } + return lines.join('\n'); +} + +/** Grid rows that fit in a terminal of `rows` lines, leaving room for the status bar. */ +export function gridHeightForRows(rows: number, statusLines = 1, ascii = false): number { + const usable = Math.max(1, rows - statusLines); + return ascii ? usable : usable * 2; +} + +/** Dim grey text for the status line. */ +export function dim(text: string): string { + return `${ESC}2m${text}${RESET}`; +} + +/** Bold text for the status line. */ +export function bold(text: string): string { + return `${ESC}1m${text}${RESET}`; +} + +/** Move the cursor to the top-left without clearing — how frames are swapped. */ +export function frameTo(width: number, height: number, frame: string, status: string): string { + void width; + void height; + return `${HOME}${frame}\n${status}${ESC}0K`; +} diff --git a/packages/complexity-automata-cli/src/session.ts b/packages/complexity-automata-cli/src/session.ts new file mode 100644 index 0000000..b01245d --- /dev/null +++ b/packages/complexity-automata-cli/src/session.ts @@ -0,0 +1,179 @@ +/** + * Building the universe a run starts from, and the status line that describes + * it. Kept out of `cli.ts` so both can be tested without a terminal. + */ + +import { + Automata, + buildDemo, + demos, + getDemo, + getPattern, + loadUniverse, + NAMED_RULES, + parseUniverse, + patterns, + saveUniverse, + type UniverseFile, +} from 'complexity-automata'; +import type { CliOptions } from './args.js'; +import { bold, dim } from './render.js'; + +export interface BuildResult { + automata: Automata; + /** What the status line calls this universe. */ + title: string; +} + +/** + * Turn the parsed options into a running universe. Precedence is + * `--load` over `--rule`/`--pattern` over `--demo`, because a saved universe is + * the most specific thing the user can ask for. + */ +export function buildUniverse(options: CliOptions, size: { width: number; height: number }, source?: string): BuildResult { + const { width, height } = size; + + if (options.rule) checkRule(options.rule); + + if (options.load && source !== undefined) { + const file: UniverseFile = parseUniverse(source); + const automata = loadUniverse(file, { width, height, wrap: options.wrap, seed: options.seed }); + if (options.rule) automata.setRule(options.rule); + return { automata, title: file.name || 'saved universe' }; + } + + if (options.rule || options.pattern) { + const automata = new Automata({ + width, + height, + rule: options.rule ?? 'B3/S23', + wrap: options.wrap, + seed: options.seed, + }); + if (options.pattern) { + const pattern = getPattern(options.pattern); + if (!pattern) throw new Error(`unknown pattern: ${options.pattern} (try \`automata patterns\`)`); + automata.stampCentered(pattern); + } else { + automata.randomize(options.density ?? 0.28); + } + return { automata, title: options.pattern ?? automata.ruleString }; + } + + const demo = getDemo(options.demo ?? 'conway'); + if (options.demo && !demo) throw new Error(`unknown demo: ${options.demo} (try \`automata demos\`)`); + const automata = buildDemo(demo?.id ?? 'conway', { width, height, wrap: options.wrap, seed: options.seed }); + if (options.density !== undefined) automata.randomize(options.density); + return { automata, title: demo?.name ?? "Conway's Game of Life" }; +} + +/** + * Reject a rule nobody meant to ask for. The parser is deliberately forgiving — + * it reads anything with digits in it — so a typo like `--rule conwya` would + * otherwise become an empty rule and an empty screen. + */ +export function checkRule(rule: string): void { + if (/\d/.test(rule)) return; + if (NAMED_RULES[rule.trim().toLowerCase()]) return; + const names = Object.keys(NAMED_RULES).slice(0, 8).join(', '); + throw new Error(`unknown rule: ${rule} — try B3/S23, a rule number, or a name (${names}, …)`); +} + +export interface StatusInfo { + title: string; + playing: boolean; + auto: boolean; + fps: number; + saved?: string; +} + +/** + * The one-line status bar under the animation. + * + * It must never wrap onto a second line — that would push the frame up and + * leave a trail — so the hint is dropped, and then the text truncated, until + * what is left fits the terminal. + */ +export function statusLine(automata: Automata, info: StatusInfo, width: number): string { + const parts = [ + bold(info.title), + automata.ruleString, + `#${automata.ruleNumber}`, + `gen ${automata.generation}`, + `pop ${automata.population}`, + `${info.fps}fps`, + ]; + if (info.auto) parts.push('auto'); + if (!info.playing) parts.push('paused'); + + const left = parts.join(dim(' \u00b7 ')); + const leftWidth = stripAnsi(left).length; + if (leftWidth >= width) { + // No room for decoration: plain text, cut to the terminal. + return stripAnsi(left).slice(0, width); + } + + const hint = info.saved ?? 'space pause \u00b7 n rule \u00b7 a auto \u00b7 s save \u00b7 q quit'; + const hintWidth = stripAnsi(hint).length; + if (leftWidth + hintWidth + 1 > width) return left; + + return `${left}${' '.repeat(width - leftWidth - hintWidth)}${dim(hint)}`; +} + +/** Remove ANSI escapes so a coloured string can be measured in columns. */ +export function stripAnsi(text: string): string { + // eslint-disable-next-line no-control-regex + return text.replace(/\u001B\[[0-9;]*[A-Za-z]/g, ''); +} + +/** `automata demos` output. */ +export function demoList(): string { + return demos + .map((demo) => ` ${demo.id.padEnd(10)} ${bold(demo.name)} ${dim(demo.rule)}\n ${wrap(demo.about, 72, 6)}`) + .join('\n'); +} + +/** `automata patterns` output. */ +export function patternList(): string { + return patterns + .filter((p) => p.cells.length > 0) + .map((p) => ` ${p.name.padEnd(22)} ${dim(`${p.category}${p.rule ? ` · ${p.rule}` : ''}`)}\n ${wrap(p.about ?? '', 72, 6)}`) + .join('\n'); +} + +/** `automata saved` output. */ +export function savedList(files: UniverseFile[]): string { + if (files.length === 0) return dim(' no saved universes yet — press s while one is running'); + return files + .map( + (file) => + ` ${file.name.padEnd(28)} ${dim(`${file.rule} · ${file.width}×${file.height} · gen ${file.generation} · pop ${file.population ?? '?'}`)}`, + ) + .join('\n'); +} + +/** Name a universe gets when it is saved without one. */ +export function autoName(automata: Automata, title: string): string { + return `${title} ${automata.ruleString} gen${automata.generation}`; +} + +/** A universe ready to be written to disk. */ +export function snapshot(automata: Automata, name: string, note?: string): UniverseFile { + return saveUniverse(automata, { name, note }); +} + +function wrap(text: string, width: number, indent: number): string { + const words = text.split(/\s+/).filter(Boolean); + const lines: string[] = []; + let line = ''; + for (const word of words) { + if (line.length + word.length + 1 > width) { + lines.push(line); + line = word; + } else { + line = line ? `${line} ${word}` : word; + } + } + if (line) lines.push(line); + return lines.join(`\n${' '.repeat(indent)}`); +} diff --git a/packages/complexity-automata-cli/test/args.test.ts b/packages/complexity-automata-cli/test/args.test.ts new file mode 100644 index 0000000..934c142 --- /dev/null +++ b/packages/complexity-automata-cli/test/args.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, test } from 'bun:test'; +import { parseArgs } from '../src/args.js'; + +describe('parseArgs', () => { + test('defaults to running the Conway demo', () => { + const { options, errors } = parseArgs([]); + expect(errors).toEqual([]); + expect(options.command).toBe('run'); + expect(options.demo).toBeUndefined(); + expect(options.fps).toBe(20); + expect(options.wrap).toBe(true); + }); + + test('a bare word is a demo id', () => { + expect(parseArgs(['fractal']).options.demo).toBe('fractal'); + }); + + test('reads subcommands', () => { + expect(parseArgs(['demos']).options.command).toBe('demos'); + expect(parseArgs(['patterns']).options.command).toBe('patterns'); + expect(parseArgs(['saved']).options.command).toBe('saved'); + }); + + test('forget needs a name', () => { + expect(parseArgs(['forget', 'my', 'universe']).options.target).toBe('my universe'); + expect(parseArgs(['forget']).errors[0]).toMatch(/needs the name/); + }); + + test('reads long and short flags alike', () => { + const { options } = parseArgs(['-r', 'B36/S23', '-f', '30', '-p', 'Glider Gun', '--no-wrap']); + expect(options.rule).toBe('B36/S23'); + expect(options.fps).toBe(30); + expect(options.pattern).toBe('Glider Gun'); + expect(options.wrap).toBe(false); + }); + + test('validates numbers', () => { + expect(parseArgs(['--fps', 'fast']).errors[0]).toMatch(/needs a number/); + expect(parseArgs(['--density', '3']).errors[0]).toMatch(/between 0 and 1/); + expect(parseArgs(['--width', '1']).errors[0]).toMatch(/between 4 and 4096/); + }); + + test('reports unknown flags without throwing', () => { + expect(parseArgs(['--nope']).errors).toEqual(['unknown option: --nope']); + }); + + test('colour modes', () => { + expect(parseArgs(['--mono']).options.color).toBe('mono'); + expect(parseArgs(['--no-color']).options.color).toBe('mono'); + expect(parseArgs(['--color', '256']).options.color).toBe('256'); + expect(parseArgs(['--color', 'neon']).errors[0]).toMatch(/--color takes/); + }); + + test('help and version win over everything else', () => { + expect(parseArgs(['fractal', '--help']).options.command).toBe('help'); + expect(parseArgs(['-v']).options.command).toBe('version'); + }); +}); diff --git a/packages/complexity-automata-cli/test/helpers.ts b/packages/complexity-automata-cli/test/helpers.ts new file mode 100644 index 0000000..9f1a840 --- /dev/null +++ b/packages/complexity-automata-cli/test/helpers.ts @@ -0,0 +1,3 @@ +/** Re-exports so the tests import the same modules the built CLI does. */ +export { gridHeightForRows, renderFrame, toAnsi256 } from '../src/render.js'; +export { stripAnsi as stripAnsiForTest } from '../src/session.js'; diff --git a/packages/complexity-automata-cli/test/render.test.ts b/packages/complexity-automata-cli/test/render.test.ts new file mode 100644 index 0000000..27d2010 --- /dev/null +++ b/packages/complexity-automata-cli/test/render.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, test } from 'bun:test'; +import { Automata } from 'complexity-automata'; +import { gridHeightForRows, renderFrame, stripAnsiForTest, toAnsi256 } from './helpers.js'; + +const grid = (): Automata => { + const life = new Automata({ width: 4, height: 4, wrap: false }); + life.set(0, 0, true, [255, 0, 0]); + life.set(1, 1, true, [0, 255, 0]); + return life; +}; + +describe('renderFrame', () => { + test('two grid rows share one terminal row', () => { + expect(renderFrame(grid(), { color: 'mono' }).split('\n').length).toBe(2); + }); + + test('mono mode uses block characters and no escapes', () => { + const frame = renderFrame(grid(), { color: 'mono' }); + expect(frame).not.toContain('\u001B'); + expect(frame).toContain('▀'); + }); + + test('truecolor mode emits 24 bit colour for live cells', () => { + const frame = renderFrame(grid(), { color: 'truecolor' }); + // The top cell of a pair is the foreground, the bottom one the background. + expect(frame).toContain('38;2;255;0;0'); + expect(frame).toContain('48;2;0;255;0'); + }); + + test('256 colour mode emits palette indexes instead', () => { + const frame = renderFrame(grid(), { color: 'ansi256' }); + expect(frame).toContain('38;5;'); + expect(frame).not.toContain('38;2;'); + }); + + test('repeated colours are not repeated in the output', () => { + const life = new Automata({ width: 40, height: 2, wrap: false }); + for (let x = 0; x < 40; x++) life.set(x, 0, true, [10, 20, 30]); + const frame = renderFrame(life, { color: 'truecolor' }); + expect(frame.split('38;2;10;20;30').length - 1).toBe(1); + }); + + test('ascii mode falls back to plain characters', () => { + const frame = renderFrame(grid(), { ascii: true }); + expect(stripAnsiForTest(frame)).toBe(frame); + expect(frame.split('\n').length).toBe(4); + expect(frame[0]).toBe('#'); + }); + + test('an empty grid renders blank rows of the right width', () => { + const empty = new Automata({ width: 10, height: 6 }); + expect(renderFrame(empty, { color: 'mono' }).split('\n')).toEqual([' '.repeat(10), ' '.repeat(10), ' '.repeat(10)]); + }); +}); + +describe('sizing', () => { + test('block mode gets two grid rows per terminal row', () => { + expect(gridHeightForRows(24, 1)).toBe(46); + }); + + test('ascii mode gets one', () => { + expect(gridHeightForRows(24, 1, true)).toBe(23); + }); + + test('never returns a zero-height grid', () => { + expect(gridHeightForRows(1, 1)).toBe(2); + }); +}); + +describe('toAnsi256', () => { + test('maps greys onto the grey ramp', () => { + expect(toAnsi256([0, 0, 0])).toBe(16); + expect(toAnsi256([255, 255, 255])).toBe(231); + expect(toAnsi256([128, 128, 128])).toBeGreaterThanOrEqual(232); + }); + + test('maps colours into the 6×6×6 cube', () => { + expect(toAnsi256([255, 0, 0])).toBe(196); + expect(toAnsi256([0, 255, 0])).toBe(46); + }); +}); diff --git a/packages/complexity-automata-cli/test/session.test.ts b/packages/complexity-automata-cli/test/session.test.ts new file mode 100644 index 0000000..8a4af65 --- /dev/null +++ b/packages/complexity-automata-cli/test/session.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, test } from 'bun:test'; +import { saveUniverse } from 'complexity-automata'; +import { parseArgs } from '../src/args.js'; +import { autoName, buildUniverse, demoList, patternList, savedList, statusLine, stripAnsi } from '../src/session.js'; + +const options = (argv: string[]) => parseArgs(argv).options; +const size = { width: 60, height: 30 }; + +describe('buildUniverse', () => { + test('builds the named demo', () => { + const { automata, title } = buildUniverse(options(['fractal']), size); + expect(automata.ruleString).toBe('B1357/S1357'); + expect(title).toBe('Fractal Replicator'); + }); + + test('an unknown demo is an error, not a silent fallback', () => { + expect(() => buildUniverse(options(['--demo', 'nope']), size)).toThrow(/unknown demo/); + }); + + test('a rule without a pattern seeds a random soup', () => { + const { automata } = buildUniverse(options(['--rule', 'B36/S23', '--seed', '4']), size); + expect(automata.ruleString).toBe('B36/S23'); + expect(automata.population).toBeGreaterThan(0); + }); + + test('a pattern is stamped in the centre', () => { + const { automata } = buildUniverse(options(['--pattern', 'Glider']), size); + expect(automata.population).toBe(5); + expect(automata.liveCells().every(([x, y]) => x > 20 && y > 10)).toBe(true); + }); + + test('a rule that is not a rule is an error, not an empty screen', () => { + expect(() => buildUniverse(options(['--rule', 'conwya']), size)).toThrow(/unknown rule/); + expect(buildUniverse(options(['--rule', 'highlife']), size).automata.ruleString).toBe('B36/S23'); + expect(buildUniverse(options(['--rule', '6152']), size).automata.ruleString).toBe('B3/S23'); + }); + + test('an unknown pattern is an error', () => { + expect(() => buildUniverse(options(['--pattern', 'Banana']), size)).toThrow(/unknown pattern/); + }); + + test('a loaded universe wins over the demo', () => { + const source = JSON.stringify(saveUniverse(buildUniverse(options(['gun']), size).automata, { name: 'my gun' })); + const { automata, title } = buildUniverse(options(['--load', 'file.json', '--demo', 'fractal']), size, source); + expect(title).toBe('my gun'); + expect(automata.ruleString).toBe('B3/S23'); + }); + + test('--rule still applies on top of a loaded universe', () => { + const source = JSON.stringify(saveUniverse(buildUniverse(options(['gun']), size).automata, { name: 'g' })); + const { automata } = buildUniverse(options(['--load', 'f.json', '--rule', 'B2/S']), size, source); + expect(automata.ruleString).toBe('B2/S'); + }); + + test('the same seed builds the same universe twice', () => { + const a = buildUniverse(options(['soup', '--seed', '11']), size).automata; + const b = buildUniverse(options(['soup', '--seed', '11']), size).automata; + expect(a.toString()).toBe(b.toString()); + }); +}); + +describe('status line', () => { + test('fits the terminal width and names the rule', () => { + const { automata } = buildUniverse(options(['gun']), size); + const line = statusLine(automata, { title: 'Gosper Glider Gun', playing: true, auto: false, fps: 20 }, 120); + expect(stripAnsi(line).length).toBe(120); + expect(stripAnsi(line)).toContain('B3/S23'); + expect(stripAnsi(line)).toContain('gen 0'); + }); + + test('drops the hint, then truncates, rather than wrapping', () => { + const { automata } = buildUniverse(options(['gun']), size); + const info = { title: 'Gosper Glider Gun', playing: true, auto: false, fps: 20 } as const; + expect(stripAnsi(statusLine(automata, info, 60)).length).toBeLessThanOrEqual(60); + expect(stripAnsi(statusLine(automata, info, 20)).length).toBe(20); + expect(statusLine(automata, info, 20)).not.toContain('\u001B'); + }); + + test('says when it is paused and when automatic mode is on', () => { + const { automata } = buildUniverse(options(['gun']), size); + const line = stripAnsi(statusLine(automata, { title: 'x', playing: false, auto: true, fps: 20 }, 120)); + expect(line).toContain('paused'); + expect(line).toContain('auto'); + }); +}); + +describe('listings', () => { + test('demos, patterns and saved universes all render', () => { + expect(stripAnsi(demoList())).toContain("Conway's Game of Life"); + expect(stripAnsi(patternList())).toContain('Glider Gun'); + expect(stripAnsi(savedList([]))).toContain('no saved universes yet'); + const file = saveUniverse(buildUniverse(options(['gun']), size).automata, { name: 'kept' }); + expect(stripAnsi(savedList([file]))).toContain('kept'); + }); +}); + +describe('autoName', () => { + test('describes the universe it came from', () => { + const { automata } = buildUniverse(options(['gun']), size); + expect(autoName(automata, 'Gun')).toBe('Gun B3/S23 gen0'); + }); +}); diff --git a/packages/complexity-automata-cli/tsconfig.json b/packages/complexity-automata-cli/tsconfig.json new file mode 100644 index 0000000..28d1608 --- /dev/null +++ b/packages/complexity-automata-cli/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": ["ES2021"], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "sourceMap": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "test"] +} diff --git a/packages/complexity-automata-svelte/README.md b/packages/complexity-automata-svelte/README.md new file mode 100644 index 0000000..9fc80a2 --- /dev/null +++ b/packages/complexity-automata-svelte/README.md @@ -0,0 +1,83 @@ +# complexity-automata-svelte + +Svelte components for [`complexity-automata`](https://www.npmjs.com/package/complexity-automata): +a canvas view of a running universe, the rule as eighteen switches, a demo +picker, and a shelf of saved universes. + +This is the UI that ships as both the web app and the Android app of +[Complexity Automata](https://github.com/OpenSourceAGI/Automate-Automata). + +```bash +npm i complexity-automata-svelte +``` + +## The whole app + +```svelte + + + +``` + +| prop | | | +| --- | --- | --- | +| `demo` | `'conway'` | which demo to open with | +| `intro` | `true` | show the intro screen first | +| `cellSize` | `4` | CSS pixels per cell — bigger cells, smaller universe | +| `playing` | `true` | start running immediately | +| `storage` | `localStorage` | where saved universes live | + +It fills its parent, so give that parent a height. + +## Just the grid + +```svelte + + + console.log(e.detail.generation, e.detail.population)} + on:universe={(e) => console.log('new rule', e.detail.rule)} +/> +``` + +The canvas owns the frame loop and pointer drawing, nothing else. It exposes +`repaint()` and `nextUniverse()` for a parent that edits the grid directly. + +## The other pieces + +- `IntroScreen` — the explanation and the demo buttons, as an overlay over a + live grid. Events: `select`, `start`, `dismiss`. +- `RuleControls` — birth and survival counts as switches, plus the rule number. + Event: `change` with a rule. +- `UniverseShelf` — save, reopen, share, download and delete universes, on top + of a `UniverseLibrary`. +- `CanvasPainter`, `gridSizeFor`, `cellAtPointer` — the painting helpers, if you + would rather draw the grid yourself. + +Everything from `complexity-automata` is re-exported, so one dependency is +enough. + +## Rendering + +The grid is painted at one pixel per cell into an offscreen buffer and scaled up +with smoothing off, so a full redraw costs the number of cells rather than the +number of screen pixels — which is what keeps a 300×600 grid comfortable on a +phone. + +Components carry their own scoped styles and assume a dark background. No CSS +framework, no global stylesheet. + +MIT diff --git a/packages/complexity-automata-svelte/package.json b/packages/complexity-automata-svelte/package.json new file mode 100644 index 0000000..73ceedc --- /dev/null +++ b/packages/complexity-automata-svelte/package.json @@ -0,0 +1,47 @@ +{ + "name": "complexity-automata-svelte", + "version": "1.0.0", + "description": "Svelte components for complexity-automata — a canvas cellular automata view, rule controls, demo picker and a saved-universe library.", + "keywords": [ + "svelte", + "cellular-automata", + "game-of-life", + "canvas", + "component" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenSourceAGI/Automate-Automata.git", + "directory": "packages/complexity-automata-svelte" + }, + "type": "module", + "svelte": "./src/index.js", + "main": "./src/index.js", + "exports": { + ".": { + "svelte": "./src/index.js", + "default": "./src/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "src", + "README.md" + ], + "scripts": { + "test": "bun test" + }, + "dependencies": { + "complexity-automata": "workspace:*" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0" + }, + "devDependencies": { + "svelte": "^4.2.7" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/complexity-automata-svelte/src/AutomataApp.svelte b/packages/complexity-automata-svelte/src/AutomataApp.svelte new file mode 100644 index 0000000..a55a606 --- /dev/null +++ b/packages/complexity-automata-svelte/src/AutomataApp.svelte @@ -0,0 +1,545 @@ + + + + + +
+ {#if automata} + + {/if} + + {#if showIntro} + { + startDemo(event.detail.id); + showIntro = false; + }} + on:start={() => (showIntro = false)} + on:dismiss={() => (showIntro = false)} + /> + {/if} + + {#if toast} +
{toast}
+ {/if} + +
+ {ruleString} + gen {generation} + pop {population} + {#if auto}auto{/if} +
+ +
+ {#if panel === 'demos'} +
+

Demos

+
+ {#each demos as entry (entry.id)} + + {/each} +
+
+ {:else if panel === 'rules'} +
+

Rule

+ +
+ + +
+

+ {parsedRule.birth.length} birth {parsedRule.birth.length === 1 ? 'count' : 'counts'}, + {parsedRule.survive.length} survival — tap the grid to place a {pattern.toLowerCase()}. +

+
+ {:else if panel === 'saved'} +
+

Universes

+ flash(`saved "${event.detail.name}"`)} + /> +
+ {/if} + +
+ + + + + + + +
+ + + + + +
+
+
+ + diff --git a/packages/complexity-automata-svelte/src/AutomataCanvas.svelte b/packages/complexity-automata-svelte/src/AutomataCanvas.svelte new file mode 100644 index 0000000..bf2ec71 --- /dev/null +++ b/packages/complexity-automata-svelte/src/AutomataCanvas.svelte @@ -0,0 +1,152 @@ + + + + + + + + diff --git a/packages/complexity-automata-svelte/src/IntroScreen.svelte b/packages/complexity-automata-svelte/src/IntroScreen.svelte new file mode 100644 index 0000000..1199faf --- /dev/null +++ b/packages/complexity-automata-svelte/src/IntroScreen.svelte @@ -0,0 +1,208 @@ + + + + + + diff --git a/packages/complexity-automata-svelte/src/RuleControls.svelte b/packages/complexity-automata-svelte/src/RuleControls.svelte new file mode 100644 index 0000000..6963288 --- /dev/null +++ b/packages/complexity-automata-svelte/src/RuleControls.svelte @@ -0,0 +1,183 @@ + + + +
+
+ B + {#each counts as count} + + {/each} +
+ +
+ S + {#each counts as count} + + {/each} +
+ +
+ + + + {rule} +
+
+ + diff --git a/packages/complexity-automata-svelte/src/UniverseShelf.svelte b/packages/complexity-automata-svelte/src/UniverseShelf.svelte new file mode 100644 index 0000000..44e7e07 --- /dev/null +++ b/packages/complexity-automata-svelte/src/UniverseShelf.svelte @@ -0,0 +1,217 @@ + + + +
+
+ + +
+ + {#if universes.length === 0} +

+ Nothing saved yet. Saving keeps the rule and every live cell — reopen it here, or open the same file with + automata --load. +

+ {:else} +
    + {#each universes as universe (universe.name)} +
  • + +
    + + + +
    +
  • + {/each} +
+ {/if} +
+ + diff --git a/packages/complexity-automata-svelte/src/canvas.js b/packages/complexity-automata-svelte/src/canvas.js new file mode 100644 index 0000000..ee6ea19 --- /dev/null +++ b/packages/complexity-automata-svelte/src/canvas.js @@ -0,0 +1,96 @@ +/** + * Canvas painting for a running automaton. + * + * The grid is drawn at one device pixel per cell into a small offscreen canvas + * and then scaled up with smoothing off. That keeps a full redraw proportional + * to the number of cells rather than the number of screen pixels, which is what + * makes a 300×600 grid comfortable on a phone. + */ + +/** + * @typedef {object} PaintOptions + * @property {[number, number, number]} [background] Colour behind dead cells. + * @property {number} [cellSize] Screen pixels per cell. Defaults to a fit. + */ + +/** Create the offscreen buffer a renderer paints into. */ +export function createBuffer(width, height) { + if (typeof OffscreenCanvas === 'function') return new OffscreenCanvas(width, height); + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + return canvas; +} + +/** + * A painter bound to one visible canvas. Recreate it when the grid size + * changes; call `paint(automata)` once per generation. + */ +export class CanvasPainter { + /** + * @param {HTMLCanvasElement} canvas + * @param {{ width: number, height: number }} grid + * @param {PaintOptions} [options] + */ + constructor(canvas, grid, options = {}) { + this.canvas = canvas; + this.width = grid.width; + this.height = grid.height; + this.background = options.background ?? [8, 10, 16]; + this.context = canvas.getContext('2d', { alpha: false }); + this.buffer = createBuffer(this.width, this.height); + this.bufferContext = this.buffer.getContext('2d', { alpha: false }); + this.image = this.bufferContext.createImageData(this.width, this.height); + if (this.context) this.context.imageSmoothingEnabled = false; + } + + /** Draw the current state of the grid. */ + paint(automata) { + if (!this.context || !this.bufferContext) return; + const { data } = this.image; + const [br, bg, bb] = this.background; + const cells = automata.cells; + const colors = automata.colors; + const tracksColor = colors.length > 0; + + for (let i = 0; i < cells.length; i++) { + const p = i * 4; + if (cells[i]) { + data[p] = tracksColor ? colors[i * 3] : 235; + data[p + 1] = tracksColor ? colors[i * 3 + 1] : 235; + data[p + 2] = tracksColor ? colors[i * 3 + 2] : 235; + } else { + data[p] = br; + data[p + 1] = bg; + data[p + 2] = bb; + } + data[p + 3] = 255; + } + + this.bufferContext.putImageData(this.image, 0, 0); + this.context.imageSmoothingEnabled = false; + this.context.drawImage(this.buffer, 0, 0, this.width, this.height, 0, 0, this.canvas.width, this.canvas.height); + } +} + +/** + * How big the grid should be for a given viewport. + * + * `cellSize` is in CSS pixels; smaller cells mean a bigger universe. Phones get + * a larger cell so the simulation stays cheap and the cells stay visible. + */ +export function gridSizeFor(viewportWidth, viewportHeight, cellSize = 4) { + const size = Math.max(1, cellSize); + return { + width: Math.max(8, Math.floor(viewportWidth / size)), + height: Math.max(8, Math.floor(viewportHeight / size)), + }; +} + +/** Which cell a pointer at these client coordinates is over. */ +export function cellAtPointer(canvas, automata, clientX, clientY) { + const rect = canvas.getBoundingClientRect(); + const x = Math.floor(((clientX - rect.left) / rect.width) * automata.width); + const y = Math.floor(((clientY - rect.top) / rect.height) * automata.height); + return { x, y }; +} diff --git a/packages/complexity-automata-svelte/src/index.js b/packages/complexity-automata-svelte/src/index.js new file mode 100644 index 0000000..2209e15 --- /dev/null +++ b/packages/complexity-automata-svelte/src/index.js @@ -0,0 +1,22 @@ +/** + * complexity-automata-svelte — the UI half of the project. + * + * ```svelte + * + * + * + * ``` + * + * The engine itself lives in `complexity-automata`, which this package + * re-exports so an embedder only needs one dependency. + */ + +export { default as AutomataApp } from './AutomataApp.svelte'; +export { default as AutomataCanvas } from './AutomataCanvas.svelte'; +export { default as IntroScreen } from './IntroScreen.svelte'; +export { default as RuleControls } from './RuleControls.svelte'; +export { default as UniverseShelf } from './UniverseShelf.svelte'; +export { CanvasPainter, cellAtPointer, createBuffer, gridSizeFor } from './canvas.js'; +export * from 'complexity-automata'; diff --git a/packages/complexity-automata-svelte/test/canvas.test.js b/packages/complexity-automata-svelte/test/canvas.test.js new file mode 100644 index 0000000..69e46bc --- /dev/null +++ b/packages/complexity-automata-svelte/test/canvas.test.js @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'bun:test'; +import { Automata } from 'complexity-automata'; +import { cellAtPointer, gridSizeFor } from '../src/canvas.js'; + +describe('gridSizeFor', () => { + test('divides the viewport by the cell size', () => { + expect(gridSizeFor(800, 600, 4)).toEqual({ width: 200, height: 150 }); + }); + + test('a bigger cell size means a smaller, cheaper universe', () => { + expect(gridSizeFor(800, 600, 8)).toEqual({ width: 100, height: 75 }); + }); + + test('never returns a degenerate grid', () => { + expect(gridSizeFor(10, 10, 40)).toEqual({ width: 8, height: 8 }); + expect(gridSizeFor(0, 0, 0)).toEqual({ width: 8, height: 8 }); + }); +}); + +describe('cellAtPointer', () => { + const canvas = (rect) => ({ getBoundingClientRect: () => rect }); + const automata = new Automata({ width: 100, height: 50 }); + + test('maps client coordinates onto grid cells', () => { + const element = canvas({ left: 0, top: 0, width: 400, height: 200 }); + expect(cellAtPointer(element, automata, 0, 0)).toEqual({ x: 0, y: 0 }); + expect(cellAtPointer(element, automata, 200, 100)).toEqual({ x: 50, y: 25 }); + expect(cellAtPointer(element, automata, 399, 199)).toEqual({ x: 99, y: 49 }); + }); + + test('accounts for where the canvas sits on the page', () => { + const element = canvas({ left: 40, top: 20, width: 400, height: 200 }); + expect(cellAtPointer(element, automata, 40, 20)).toEqual({ x: 0, y: 0 }); + expect(cellAtPointer(element, automata, 240, 120)).toEqual({ x: 50, y: 25 }); + }); +}); diff --git a/packages/complexity-automata/README.md b/packages/complexity-automata/README.md new file mode 100644 index 0000000..b2050d8 --- /dev/null +++ b/packages/complexity-automata/README.md @@ -0,0 +1,170 @@ +# complexity-automata + +The cellular automata engine behind [Complexity Automata](https://github.com/OpenSourceAGI/Automate-Automata): +Conway's Game of Life and the 262,143 other rules that share its shape, with +colour inheritance, saveable universes, a pattern catalogue and ready-made demos. + +No DOM, no timers, no dependencies — the web app, the Android app and the +terminal animation all drive this same object. + +```bash +npm i complexity-automata +``` + +## Quick start + +```js +import { Automata } from 'complexity-automata'; + +const life = new Automata({ width: 80, height: 40, rule: 'B3/S23' }); +life.stampCentered('Glider Gun'); +life.run(120); + +console.log(life.toString()); // '#' for alive, '.' for dead +console.log(life.population); // live cells +``` + +## Rules + +A rule is the neighbour counts that create and preserve life. `B3/S23` is +Conway's: born with exactly three neighbours, survives with two or three. + +```js +import { parseRule, formatRule, ruleNumberToRuleSet } from 'complexity-automata'; + +parseRule('B3/S23'); // { birth: [3], survive: [2, 3] } +parseRule('highlife'); // { birth: [3, 6], survive: [2, 3] } +parseRule(6152); // the same rule, as a rule number +formatRule({ birth: [2], survive: [] }); // 'B2/S' +``` + +Every rule is also an **18 bit number** — nine birth counts, nine survival +counts — so the whole space of universes is addressable by an integer: + +```js +const life = new Automata({ width: 60, height: 60, rule: 6152 }); +life.ruleNumber; // 6152 +life.ruleString; // 'B3/S23' +life.randomRule(); // somewhere else in the 262,144 +``` + +`NAMED_RULES` has the ones worth knowing: `highlife`, `seeds`, `replicator`, +`fredkin`, `maze`, `coral`, `daynight`, `gnarl`, `diamoeba` and more. + +## The grid + +```js +const life = new Automata({ + width: 200, + height: 100, + rule: 'B3/S23', + wrap: true, // edges join into a torus, so gliders come back around + colors: true, // track a colour per cell + seed: 42, // deterministic RNG: same seed, same universe +}); + +life.set(10, 10, true); // bring a cell to life +life.toggle(10, 10); // flip it +life.get(10, 10); // 0 or 1 +life.stamp('Glider', 4, 4); // stamp a pattern by name +life.stampCentered('Pulsar'); +life.randomize(0.25); // a soup with a quarter of the cells alive +life.step(); // one generation +life.run(100); // a hundred more +life.isStagnant(); // dead, frozen, or a short oscillation? +life.resized(300, 150); // a new grid holding what still fits +``` + +Cells live in a `Uint8Array` (`life.cells`, row-major) and colours in a parallel +`Uint8Array` of RGB triples (`life.colors`), which is what makes a renderer a +single pass over memory rather than a call per cell. + +## Colour + +Colour is inheritance, not physics. A cell born beside living cells takes the +average of their colours, so colonies drift into their own hues and a collision +between two of them fades into a blend. A cell born alone gets a fresh colour +from the rule, the generation and its position. + +```js +life.colorAt(10, 10); // [r, g, b], black when the cell is dead +``` + +## Patterns + +```js +import { patterns, getPattern, patternsByCategory, rotatePattern } from 'complexity-automata'; + +getPattern('Glider Gun'); // Gosper's gun +patternsByCategory('fractal'); // seeds for replicator rules +rotatePattern(getPattern('Glider'), 1); // a quarter turn +``` + +Categories: `spaceship`, `gun`, `oscillator`, `still`, `methuselah`, `fractal`. + +## Demos + +Each demo is a rule plus a seeding function, so it builds at any grid size — +a phone screen, a canvas, or an 80×24 terminal. + +```js +import { demos, buildDemo } from 'complexity-automata'; + +const life = buildDemo('fractal', { width: 120, height: 120, seed: 1 }); +demos.map((d) => d.id); +// conway, gliders, gun, fractal, fredkin, gnarl, coral, maze, seeds, +// highlife, daynight, soup +``` + +## Saving universes + +A universe is its rule plus its live cells. Cells are stored as Life RLE — the +same encoding [conwaylife.com](https://conwaylife.com/book/#rle_files) uses — so +a saved universe opens in other Life software, and patterns from elsewhere load +here. + +```js +import { saveUniverse, loadUniverse, toShareString, UniverseLibrary } from 'complexity-automata'; + +const file = saveUniverse(life, { name: 'my gun' }); // a plain JSON object +const back = loadUniverse(file); // exactly as it was +const back2 = loadUniverse(file, { width: 400, height: 200 }); // recentred to fit + +const share = toShareString(life); // one URL-safe string for a ?u= link + +// A shelf on top of localStorage, or any get/set/remove store. +const library = new UniverseLibrary(localStorage); +library.save(life, { name: 'my gun' }); +library.list(); +library.remove('my gun'); +``` + +`parseUniverse` accepts all three: the JSON, a share string, or bare RLE. + +## Automatic mode + +Most of the 262,144 rules die out in ten generations or fill the screen with +static. `Autopilot` watches for both and moves on, so what you see is a tour of +the rules that do something. + +```js +import { Autopilot } from 'complexity-automata'; + +const pilot = new Autopilot(life, { + minGenerations: 40, + maxGenerations: 400, + onUniverse: ({ rule, survived }) => console.log(`${rule} after ${survived} generations`), +}); + +for (;;) { + life.step(); + pilot.tick(); +} +``` + +## See also + +- [`complexity-automata-cli`](https://www.npmjs.com/package/complexity-automata-cli) — the terminal animation +- [`complexity-automata-svelte`](https://www.npmjs.com/package/complexity-automata-svelte) — canvas view and controls + +MIT diff --git a/packages/complexity-automata/package.json b/packages/complexity-automata/package.json new file mode 100644 index 0000000..28e0638 --- /dev/null +++ b/packages/complexity-automata/package.json @@ -0,0 +1,55 @@ +{ + "name": "complexity-automata", + "version": "1.0.0", + "description": "Cellular automata engine — Conway's Game of Life and all 262,144 other rules, with colour inheritance, saveable universes, patterns and demos. Runs in the browser, on mobile and in the terminal.", + "keywords": [ + "cellular-automata", + "game-of-life", + "conway", + "life", + "wolfram", + "fractal", + "simulation", + "generative" + ], + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenSourceAGI/Automate-Automata.git", + "directory": "packages/complexity-automata" + }, + "homepage": "https://github.com/OpenSourceAGI/Automate-Automata/tree/master/packages/complexity-automata#readme", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "README.md" + ], + "sideEffects": false, + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "tsc -p tsconfig.json --watch", + "test": "bun test", + "typecheck": "tsc -p tsconfig.json --noEmit", + "prepublishOnly": "bun run build" + }, + "devDependencies": { + "@types/bun": "^1.1.10", + "typescript": "^5.5.4" + }, + "engines": { + "node": ">=18" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/complexity-automata/src/automata.ts b/packages/complexity-automata/src/automata.ts new file mode 100644 index 0000000..e6ec6ce --- /dev/null +++ b/packages/complexity-automata/src/automata.ts @@ -0,0 +1,434 @@ +/** + * The engine: a grid of cells, a rule, and a `step()` that advances one + * generation. No DOM, no timers, no rendering — the browser component, the + * terminal renderer and the tests all drive this same object. + */ + +import { blend, seedColor, type RGB } from './color.js'; +import { getPattern, type Pattern } from './patterns.js'; +import { + formatRule, + parseRule, + ruleSetToRuleNumber, + RULE_SPACE, + type RuleLike, + type RuleSet, +} from './rules.js'; + +export interface AutomataOptions { + width: number; + height: number; + /** Rule set, rule number or `B3/S23` string. Defaults to Conway's rules. */ + rule?: RuleLike; + /** Wrap the edges into a torus so gliders come back around. Default true. */ + wrap?: boolean; + /** Track a colour per cell so colonies inherit and blend hues. Default true. */ + colors?: boolean; + /** Seed for the built-in RNG, so random universes are reproducible. */ + seed?: number; +} + +/** A small deterministic PRNG — same seed, same universe, on every platform. */ +export function mulberry32(seed: number): () => number { + let a = seed >>> 0; + return () => { + a = (a + 0x6d2b79f5) >>> 0; + let t = Math.imul(a ^ (a >>> 15), 1 | a); + t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} + +const DEAD: RGB = [0, 0, 0]; + +export class Automata { + readonly width: number; + readonly height: number; + /** One byte per cell: 1 alive, 0 dead. Row-major, `y * width + x`. */ + cells: Uint8Array; + /** Three bytes per cell (r, g, b). Empty when `colors` is off. */ + colors: Uint8Array; + wrap: boolean; + /** Whether colour inheritance is tracked. */ + readonly tracksColor: boolean; + + generation = 0; + population = 0; + + private ruleSet: RuleSet; + private ruleNum: number; + private birthLookup = new Uint8Array(9); + private surviveLookup = new Uint8Array(9); + private history: number[] = []; + private rng: () => number; + private seedValue: number; + + constructor(options: AutomataOptions) { + this.width = Math.max(1, Math.floor(options.width)); + this.height = Math.max(1, Math.floor(options.height)); + this.wrap = options.wrap ?? true; + this.tracksColor = options.colors ?? true; + this.cells = new Uint8Array(this.width * this.height); + this.colors = new Uint8Array(this.tracksColor ? this.width * this.height * 3 : 0); + this.seedValue = options.seed ?? (Date.now() & 0xffffffff); + this.rng = mulberry32(this.seedValue); + this.ruleSet = parseRule(options.rule ?? 'B3/S23'); + this.ruleNum = ruleSetToRuleNumber(this.ruleSet); + this.refreshLookups(); + } + + // ---------------------------------------------------------------- rules + + get rule(): RuleSet { + return { birth: [...this.ruleSet.birth], survive: [...this.ruleSet.survive] }; + } + + set rule(rule: RuleLike) { + this.setRule(rule); + } + + /** The current rule as an 18 bit integer. */ + get ruleNumber(): number { + return this.ruleNum; + } + + /** The current rule in `B3/S23` notation. */ + get ruleString(): string { + return formatRule(this.ruleSet); + } + + /** Swap in a new rule mid-run; the grid keeps its cells. */ + setRule(rule: RuleLike): this { + this.ruleSet = parseRule(rule); + this.ruleNum = ruleSetToRuleNumber(this.ruleSet); + this.refreshLookups(); + this.history.length = 0; + return this; + } + + /** Toggle one neighbour count in the birth or survive set. */ + toggleRuleBit(kind: 'birth' | 'survive', count: number): this { + const list = kind === 'birth' ? this.ruleSet.birth : this.ruleSet.survive; + const next = list.includes(count) ? list.filter((n) => n !== count) : [...list, count].sort((a, b) => a - b); + return this.setRule(kind === 'birth' ? { ...this.ruleSet, birth: next } : { ...this.ruleSet, survive: next }); + } + + /** Jump to a random universe somewhere else in the 262,144 rule space. */ + randomRule(): this { + return this.setRule(Math.floor(this.rng() * RULE_SPACE)); + } + + private refreshLookups(): void { + this.birthLookup.fill(0); + this.surviveLookup.fill(0); + for (const n of this.ruleSet.birth) if (n >= 0 && n <= 8) this.birthLookup[n] = 1; + for (const n of this.ruleSet.survive) if (n >= 0 && n <= 8) this.surviveLookup[n] = 1; + } + + // ----------------------------------------------------------------- cells + + /** Index into `cells` for a coordinate, or -1 when it is off-grid and wrap is off. */ + index(x: number, y: number): number { + let px = Math.floor(x); + let py = Math.floor(y); + if (this.wrap) { + px = ((px % this.width) + this.width) % this.width; + py = ((py % this.height) + this.height) % this.height; + } else if (px < 0 || py < 0 || px >= this.width || py >= this.height) { + return -1; + } + return py * this.width + px; + } + + /** Is the cell at these coordinates alive? */ + get(x: number, y: number): 0 | 1 { + const i = this.index(x, y); + return i < 0 ? 0 : ((this.cells[i] ? 1 : 0) as 0 | 1); + } + + /** Bring a cell to life (or kill it), inheriting colour from its neighbours. */ + set(x: number, y: number, alive = true, color?: RGB): this { + const i = this.index(x, y); + if (i < 0) return this; + const was = this.cells[i]; + this.cells[i] = alive ? 1 : 0; + if (alive && !was) this.population++; + if (!alive && was) this.population--; + if (this.tracksColor) { + const rgb = alive ? (color ?? this.inheritedColor(x, y)) : DEAD; + this.colors[i * 3] = rgb[0]; + this.colors[i * 3 + 1] = rgb[1]; + this.colors[i * 3 + 2] = rgb[2]; + } + return this; + } + + /** Flip one cell — what a click or a tap does. */ + toggle(x: number, y: number): this { + return this.set(x, y, !this.get(x, y)); + } + + /** The colour of a cell; dead cells are black. */ + colorAt(x: number, y: number): RGB { + const i = this.index(x, y); + if (i < 0 || !this.tracksColor || !this.cells[i]) return DEAD; + return [this.colors[i * 3]!, this.colors[i * 3 + 1]!, this.colors[i * 3 + 2]!]; + } + + /** Average of the live neighbours' colours, or a fresh seed colour if alone. */ + private inheritedColor(x: number, y: number): RGB { + if (!this.tracksColor) return DEAD; + const parents: RGB[] = []; + for (let dy = -1; dy <= 1; dy++) { + for (let dx = -1; dx <= 1; dx++) { + if (dx === 0 && dy === 0) continue; + const i = this.index(x + dx, y + dy); + if (i < 0 || !this.cells[i]) continue; + const c: RGB = [this.colors[i * 3]!, this.colors[i * 3 + 1]!, this.colors[i * 3 + 2]!]; + if (c[0] || c[1] || c[2]) parents.push(c); + } + } + return parents.length ? blend(parents) : seedColor(this.ruleNum, this.generation, x, y); + } + + /** Live neighbours in the Moore neighbourhood, respecting `wrap`. */ + countNeighbors(x: number, y: number, grid: Uint8Array = this.cells): number { + let count = 0; + for (let dy = -1; dy <= 1; dy++) { + for (let dx = -1; dx <= 1; dx++) { + if (dx === 0 && dy === 0) continue; + const i = this.index(x + dx, y + dy); + if (i >= 0 && grid[i]) count++; + } + } + return count; + } + + // -------------------------------------------------------------- editing + + /** Empty the grid and reset the generation counter. */ + clear(): this { + this.cells.fill(0); + if (this.tracksColor) this.colors.fill(0); + this.population = 0; + this.generation = 0; + this.history.length = 0; + return this; + } + + /** Sprinkle live cells over the grid. `density` is the fraction alive. */ + randomize(density = 0.25): this { + this.clear(); + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + if (this.rng() < density) this.set(x, y, true, seedColor(this.ruleNum, y, x, y)); + } + } + return this; + } + + /** Stamp a pattern (or a pattern name) with its top-left corner at `x, y`. */ + stamp(pattern: Pattern | string, x = 0, y = 0): this { + const found = typeof pattern === 'string' ? getPattern(pattern) : pattern; + if (!found) return this; + const color = this.tracksColor ? seedColor(this.ruleNum, this.generation, x, y) : undefined; + for (const [dx, dy] of found.cells) this.set(x + dx, y + dy, true, color); + return this; + } + + /** Stamp a pattern in the middle of the grid. */ + stampCentered(pattern: Pattern | string, offsetX = 0, offsetY = 0): this { + const found = typeof pattern === 'string' ? getPattern(pattern) : pattern; + if (!found || found.cells.length === 0) return this; + let maxX = 0; + let maxY = 0; + for (const [px, py] of found.cells) { + if (px > maxX) maxX = px; + if (py > maxY) maxY = py; + } + return this.stamp( + found, + Math.floor((this.width - maxX) / 2) + offsetX, + Math.floor((this.height - maxY) / 2) + offsetY, + ); + } + + /** Stamp a pattern somewhere random, fully on-grid where it fits. */ + stampRandom(pattern: Pattern | string): this { + const found = typeof pattern === 'string' ? getPattern(pattern) : pattern; + if (!found || found.cells.length === 0) return this; + return this.stamp(found, Math.floor(this.rng() * this.width), Math.floor(this.rng() * this.height)); + } + + // ------------------------------------------------------------ simulation + + /** Advance one generation. Returns the new generation number. */ + step(): number { + const previous = this.cells; + const next = new Uint8Array(previous.length); + const nextColors = this.tracksColor ? new Uint8Array(this.colors.length) : this.colors; + let population = 0; + + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + const i = y * this.width + x; + const alive = previous[i]!; + const n = this.countNeighbors(x, y, previous); + const lives = alive ? this.surviveLookup[n]! || this.birthLookup[n]! : this.birthLookup[n]!; + if (!lives) continue; + + next[i] = 1; + population++; + if (!this.tracksColor) continue; + + if (alive) { + // A survivor keeps the colour it was born with. + nextColors[i * 3] = this.colors[i * 3]!; + nextColors[i * 3 + 1] = this.colors[i * 3 + 1]!; + nextColors[i * 3 + 2] = this.colors[i * 3 + 2]!; + } else { + const [r, g, b] = this.inheritedColor(x, y); + nextColors[i * 3] = r; + nextColors[i * 3 + 1] = g; + nextColors[i * 3 + 2] = b; + } + } + } + + this.cells = next; + if (this.tracksColor) this.colors = nextColors; + this.population = population; + this.generation++; + this.recordHistory(); + return this.generation; + } + + /** Run several generations at once. */ + run(generations: number): this { + for (let i = 0; i < generations; i++) this.step(); + return this; + } + + private recordHistory(): void { + this.history.push(this.hash()); + if (this.history.length > 6) this.history.shift(); + } + + /** FNV-1a over the live cells — cheap enough to run every generation. */ + hash(): number { + let h = 0x811c9dc5; + for (let i = 0; i < this.cells.length; i++) { + h ^= this.cells[i]!; + h = Math.imul(h, 0x01000193); + } + return h >>> 0; + } + + /** + * Has the universe stopped going anywhere — died out, frozen into still + * lifes, or fallen into a short oscillation? This is what the automatic mode + * watches for before moving on to the next rule. + */ + isStagnant(): boolean { + if (this.population === 0) return true; + if (this.history.length < 4) return false; + const current = this.history[this.history.length - 1]!; + for (let i = this.history.length - 4; i < this.history.length - 1; i++) { + if (this.history[i] === current) return true; + } + return false; + } + + // ----------------------------------------------------------- bookkeeping + + /** The RNG seed this universe was created with. */ + get seed(): number { + return this.seedValue; + } + + /** Restart the RNG so a replay produces the same random choices. */ + reseed(seed: number): this { + this.seedValue = seed >>> 0; + this.rng = mulberry32(this.seedValue); + return this; + } + + /** The next value from the universe's RNG, for callers driving it. */ + random(): number { + return this.rng(); + } + + /** Coordinates of every live cell — handy for sparse renderers and tests. */ + liveCells(): Array<[number, number]> { + const out: Array<[number, number]> = []; + for (let y = 0; y < this.height; y++) { + for (let x = 0; x < this.width; x++) { + if (this.cells[y * this.width + x]) out.push([x, y]); + } + } + return out; + } + + /** A copy with the same cells, colours, rule and seed. */ + clone(): Automata { + const copy = new Automata({ + width: this.width, + height: this.height, + rule: this.rule, + wrap: this.wrap, + colors: this.tracksColor, + seed: this.seedValue, + }); + copy.cells.set(this.cells); + if (this.tracksColor) copy.colors.set(this.colors); + copy.population = this.population; + copy.generation = this.generation; + return copy; + } + + /** A new grid of a different size holding as much of this one as fits. */ + resized(width: number, height: number): Automata { + const next = new Automata({ + width, + height, + rule: this.rule, + wrap: this.wrap, + colors: this.tracksColor, + seed: this.seedValue, + }); + const w = Math.min(this.width, next.width); + const h = Math.min(this.height, next.height); + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + if (this.get(x, y)) next.set(x, y, true, this.tracksColor ? this.colorAt(x, y) : undefined); + } + } + next.generation = this.generation; + return next; + } + + /** Render the grid as text — `.` for dead, `#` for alive. */ + toString(alive = '#', dead = '.'): string { + const rows: string[] = []; + for (let y = 0; y < this.height; y++) { + let row = ''; + for (let x = 0; x < this.width; x++) row += this.cells[y * this.width + x] ? alive : dead; + rows.push(row); + } + return rows.join('\n'); + } +} + +/** Build an automaton from an ASCII block — the inverse of `toString()`. */ +export function fromString(text: string, options: Partial = {}): Automata { + const rows = text.replace(/\r/g, '').split('\n').filter((r) => r.length > 0); + const height = Math.max(1, rows.length); + const width = Math.max(1, ...rows.map((r) => r.length)); + const automata = new Automata({ ...options, width: options.width ?? width, height: options.height ?? height }); + rows.forEach((row, y) => { + [...row].forEach((char, x) => { + if (char !== '.' && char !== ' ' && char !== '0') automata.set(x, y, true); + }); + }); + return automata; +} diff --git a/packages/complexity-automata/src/autopilot.ts b/packages/complexity-automata/src/autopilot.ts new file mode 100644 index 0000000..045d97c --- /dev/null +++ b/packages/complexity-automata/src/autopilot.ts @@ -0,0 +1,102 @@ +/** + * Automatic mode: keep the screen alive without anyone touching it. + * + * Most of the 262,144 rules are boring — they die out in ten generations or + * fill the grid with static. The autopilot watches for both and moves on, + * so what you actually see is a tour of the rules that do something. + */ + +import type { Automata } from './automata.js'; +import { patterns, type Pattern } from './patterns.js'; +import { RULE_SPACE } from './rules.js'; + +export interface AutopilotOptions { + /** Never switch rules before this many generations. Default 40. */ + minGenerations?: number; + /** Always switch by this many generations. Default 400. */ + maxGenerations?: number; + /** Fraction of the grid seeded when a new universe starts. Default 0.18. */ + density?: number; + /** Also stamp a few known patterns into each new universe. Default true. */ + stampPatterns?: boolean; + /** Called whenever the autopilot moves to a new universe. */ + onUniverse?: (info: UniverseChange) => void; +} + +export interface UniverseChange { + ruleNumber: number; + rule: string; + /** Why the previous universe ended. */ + reason: 'start' | 'stagnant' | 'timeout' | 'manual'; + /** How many generations the previous universe survived. */ + survived: number; +} + +const stampable = (): Pattern[] => patterns.filter((p) => p.cells.length > 0 && p.category !== 'fractal'); + +export class Autopilot { + private options: Required> & Pick; + private generationsInUniverse = 0; + private budget: number; + + constructor( + private automata: Automata, + options: AutopilotOptions = {}, + ) { + this.options = { + minGenerations: options.minGenerations ?? 40, + maxGenerations: options.maxGenerations ?? 400, + density: options.density ?? 0.18, + stampPatterns: options.stampPatterns ?? true, + onUniverse: options.onUniverse, + }; + this.budget = this.options.maxGenerations; + } + + /** + * Call once per generation, after `step()`. Returns true if it started a new + * universe on this tick. + */ + tick(): boolean { + this.generationsInUniverse++; + if (this.generationsInUniverse < this.options.minGenerations) return false; + if (this.automata.isStagnant()) return this.next('stagnant'); + if (this.generationsInUniverse >= this.budget) return this.next('timeout'); + return false; + } + + /** Jump to a new random universe right now. */ + next(reason: UniverseChange['reason'] = 'manual'): boolean { + const survived = this.generationsInUniverse; + this.automata.setRule(Math.floor(this.automata.random() * RULE_SPACE)); + this.automata.clear(); + this.automata.randomize(this.options.density); + + if (this.options.stampPatterns) { + const choices = stampable(); + const count = 1 + Math.floor(this.automata.random() * 3); + for (let i = 0; i < count; i++) { + this.automata.stampRandom(choices[Math.floor(this.automata.random() * choices.length)]!); + } + } + + this.generationsInUniverse = 0; + // Vary how long each universe gets, so the tour does not feel metronomic. + this.budget = + this.options.minGenerations + + Math.floor(this.automata.random() * Math.max(1, this.options.maxGenerations - this.options.minGenerations)); + + this.options.onUniverse?.({ + ruleNumber: this.automata.ruleNumber, + rule: this.automata.ruleString, + reason, + survived, + }); + return true; + } + + /** Generations the current universe has run under the autopilot. */ + get age(): number { + return this.generationsInUniverse; + } +} diff --git a/packages/complexity-automata/src/color.ts b/packages/complexity-automata/src/color.ts new file mode 100644 index 0000000..ef79af1 --- /dev/null +++ b/packages/complexity-automata/src/color.ts @@ -0,0 +1,91 @@ +/** + * Colour is not part of the automaton's physics — it is inheritance. + * + * A cell born next to living cells takes the average colour of those parents, + * so colonies drift into their own hues and collisions between two colonies + * fade into a blend. A cell born with no live neighbours (a stamped pattern, + * a random seed) gets a fresh colour derived from the rule and generation, so + * each universe looks different from the last. + */ + +export type RGB = [number, number, number]; + +const clampByte = (n: number): number => (n < 0 ? 0 : n > 255 ? 255 : Math.round(n)); + +/** Convert HSL (h in degrees, s and l in 0..1) to 8 bit RGB. */ +export function hslToRgb(h: number, s: number, l: number): RGB { + const hue = ((h % 360) + 360) % 360; + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((hue / 60) % 2) - 1)); + const m = l - c / 2; + const [r, g, b] = + hue < 60 + ? [c, x, 0] + : hue < 120 + ? [x, c, 0] + : hue < 180 + ? [0, c, x] + : hue < 240 + ? [0, x, c] + : hue < 300 + ? [x, 0, c] + : [c, 0, x]; + return [clampByte((r + m) * 255), clampByte((g + m) * 255), clampByte((b + m) * 255)]; +} + +/** `#rrggbb` for CSS, canvases and anything that wants a hex string. */ +export function rgbToHex([r, g, b]: RGB): string { + return `#${[r, g, b].map((v) => clampByte(v).toString(16).padStart(2, '0')).join('')}`; +} + +/** Parse `#rgb`, `#rrggbb` or `rgb(r,g,b)` into a tuple. Returns null if it can't. */ +export function parseColor(input: string): RGB | null { + const text = input.trim(); + const short = /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i.exec(text); + if (short) return [0x11 * parseInt(short[1]!, 16), 0x11 * parseInt(short[2]!, 16), 0x11 * parseInt(short[3]!, 16)]; + const long = /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(text); + if (long) return [parseInt(long[1]!, 16), parseInt(long[2]!, 16), parseInt(long[3]!, 16)]; + const fn = /^rgba?\(([^)]+)\)$/i.exec(text); + if (fn) { + const parts = fn[1]!.split(',').map((p) => clampByte(Number(p))); + if (parts.length >= 3) return [parts[0]!, parts[1]!, parts[2]!]; + } + return null; +} + +/** Average a handful of parent colours into the colour a child inherits. */ +export function blend(colors: RGB[]): RGB { + if (colors.length === 0) return [0, 0, 0]; + let r = 0; + let g = 0; + let b = 0; + for (const c of colors) { + r += c[0]; + g += c[1]; + b += c[2]; + } + const n = colors.length; + return [clampByte(r / n), clampByte(g / n), clampByte(b / n)]; +} + +/** + * The colour a spontaneously-born cell starts with. + * + * The rule number sets the palette, so successive universes look different; + * position drifts the hue about a degree per cell, so colonies on opposite + * sides of the grid are clearly different colours while neighbours — which + * will end up blending into each other anyway — stay close; and the generation + * moves the whole thing slowly through the spectrum as a universe runs. + */ +export function seedColor(ruleNumber: number, generation: number, x = 0, y = 0): RGB { + const hue = (ruleNumber * 137.508 + generation * 2.5 + x * 0.7 + y * 1.1) % 360; + const saturation = 0.55 + ((ruleNumber >> 5) % 40) / 100; + const lightness = 0.42 + ((ruleNumber >> 11) % 22) / 100; + return hslToRgb(hue, saturation, lightness); +} + +/** Mix a colour toward another by `amount` (0..1) — used for fade-out trails. */ +export function mix(a: RGB, b: RGB, amount: number): RGB { + const t = amount < 0 ? 0 : amount > 1 ? 1 : amount; + return [clampByte(a[0] + (b[0] - a[0]) * t), clampByte(a[1] + (b[1] - a[1]) * t), clampByte(a[2] + (b[2] - a[2]) * t)]; +} diff --git a/packages/complexity-automata/src/demos.ts b/packages/complexity-automata/src/demos.ts new file mode 100644 index 0000000..f9d8b0f --- /dev/null +++ b/packages/complexity-automata/src/demos.ts @@ -0,0 +1,249 @@ +/** + * Demos: ready-made universes worth watching. + * + * Each one is a rule plus a seeding function, so the same demo can be built at + * any grid size — a phone screen, a browser canvas or an 80×24 terminal. + */ + +import { Automata } from './automata.js'; +import { getPattern } from './patterns.js'; + +export interface DemoContext { + width: number; + height: number; + /** Seed for reproducible builds; demos that scatter cells use it. */ + seed?: number; + wrap?: boolean; +} + +export interface Demo { + /** Stable id used by `--demo ` and `?demo=` links. */ + id: string; + name: string; + /** The rule this demo runs under, in `B3/S23` notation. */ + rule: string; + /** One or two sentences on what you are looking at. */ + about: string; + /** Suggested generations per second. */ + fps?: number; + build(context: DemoContext): Automata; +} + +const create = (context: DemoContext, rule: string): Automata => + new Automata({ + width: context.width, + height: context.height, + rule, + wrap: context.wrap ?? true, + seed: context.seed, + }); + +export const demos: Demo[] = [ + { + id: 'conway', + name: "Conway's Game of Life", + rule: 'B3/S23', + fps: 20, + about: + 'The original 1970 rule: a cell is born with exactly 3 neighbours and survives with 2 or 3. ' + + 'Gliders walk, the gun fires, and the pulsar beats — all from those two numbers.', + build(context) { + const life = create(context, 'B3/S23'); + const gun = getPattern('Glider Gun'); + const pulsar = getPattern('Pulsar'); + const glider = getPattern('Glider'); + + if (gun && life.width > 40 && life.height > 12) life.stamp(gun, 2, 2); + if (pulsar && life.width > 60) life.stamp(pulsar, life.width - 20, Math.floor(life.height / 2)); + if (glider) { + life.stamp(glider, Math.floor(life.width / 3), Math.floor(life.height / 2)); + life.stamp(glider, Math.floor(life.width / 2), life.height - 12); + } + life.stamp('Acorn', Math.floor(life.width / 2), Math.floor(life.height / 3)); + return life; + }, + }, + { + id: 'gliders', + name: 'Glider Parade', + rule: 'B3/S23', + fps: 24, + about: + 'Gliders in all four diagonals on a wrapping grid, so they come back around and crash into each other. ' + + 'Every collision either annihilates them or leaves debris that starts something new.', + build(context) { + const life = create(context, 'B3/S23'); + const glider = getPattern('Glider')!; + const spacing = Math.max(12, Math.floor(Math.min(life.width, life.height) / 4)); + for (let i = 0; i < 4; i++) { + for (let turn = 0; turn < 4; turn++) { + const x = 4 + ((i * spacing + turn * 7) % Math.max(1, life.width - 6)); + const y = 4 + ((turn * spacing + i * 5) % Math.max(1, life.height - 6)); + life.stamp({ ...glider, cells: rotate(glider.cells, turn) }, x, y); + } + } + return life; + }, + }, + { + id: 'gun', + name: 'Gosper Glider Gun', + rule: 'B3/S23', + fps: 20, + about: + 'The pattern that proved Life can grow forever: a 36-cell machine that emits a new glider every 30 generations.', + build(context) { + const life = create(context, 'B3/S23'); + life.stamp('Glider Gun', 2, 2); + return life; + }, + }, + { + id: 'fractal', + name: 'Fractal Replicator', + rule: 'B1357/S1357', + fps: 12, + about: + 'One single cell. Under this rule every cell copies itself into its empty neighbours, and the copies of ' + + 'copies cancel out in exactly the places that draw a Sierpinski triangle.', + build(context) { + const life = create({ ...context, wrap: context.wrap ?? false }, 'B1357/S1357'); + life.set(Math.floor(life.width / 2), Math.floor(life.height / 2), true); + return life; + }, + }, + { + id: 'fredkin', + name: 'Fredkin Fractal', + rule: 'B1357/S02468', + fps: 12, + about: + "Fredkin's replicator: any starting shape reproduces itself in a fractal lattice of copies, doubling in " + + 'scale every power of two generations.', + build(context) { + const life = create({ ...context, wrap: context.wrap ?? false }, 'B1357/S02468'); + life.stampCentered('Fractal Cross'); + return life; + }, + }, + { + id: 'gnarl', + name: 'Gnarl', + rule: 'B1/S1', + fps: 18, + about: + 'Born with one neighbour, survives with one. A handful of cells grows into an endlessly detailed woven ' + + 'texture that never repeats.', + build(context) { + const life = create({ ...context, wrap: context.wrap ?? false }, 'B1/S1'); + life.set(Math.floor(life.width / 2), Math.floor(life.height / 2), true); + return life; + }, + }, + { + id: 'coral', + name: 'Coral Growth', + rule: 'B3/S45678', + fps: 20, + about: 'Cells stick where they land and never let go — a random soup crystallises into branching coral.', + build(context) { + const life = create(context, 'B3/S45678'); + life.randomize(0.35); + return life; + }, + }, + { + id: 'maze', + name: 'Maze Builder', + rule: 'B3/S12345', + fps: 20, + about: 'A generous survival rule turns a scattering of cells into corridors and dead ends that keep extending.', + build(context) { + const life = create(context, 'B3/S12345'); + const cx = Math.floor(life.width / 2); + const cy = Math.floor(life.height / 2); + for (let i = 0; i < 40; i++) { + life.set(cx + Math.floor(life.random() * 10) - 5, cy + Math.floor(life.random() * 10) - 5, true); + } + return life; + }, + }, + { + id: 'seeds', + name: 'Seeds', + rule: 'B2/S', + fps: 30, + about: 'Nothing survives — every generation is entirely new cells. Two cells are enough to fill the screen.', + build(context) { + const life = create(context, 'B2/S'); + const cx = Math.floor(life.width / 2); + const cy = Math.floor(life.height / 2); + life.set(cx, cy, true).set(cx + 1, cy, true); + return life; + }, + }, + { + id: 'highlife', + name: 'HighLife', + rule: 'B36/S23', + fps: 20, + about: + 'Conway plus one extra birth condition, which is all it takes for a self-replicating pattern to exist ' + + 'inside an otherwise Life-like universe.', + build(context) { + const life = create(context, 'B36/S23'); + life.randomize(0.28); + return life; + }, + }, + { + id: 'daynight', + name: 'Day & Night', + rule: 'B3678/S34678', + fps: 18, + about: + 'Symmetric under swapping live and dead cells, so blobs of "day" and "night" behave identically and grind ' + + 'against each other along fractal borders.', + build(context) { + const life = create(context, 'B3678/S34678'); + life.randomize(0.5); + return life; + }, + }, + { + id: 'soup', + name: 'Primordial Soup', + rule: 'B3/S23', + fps: 24, + about: + 'Conway from pure noise. Most of it dies in the first hundred generations; what is left is still lifes, ' + + 'blinkers, and the occasional glider heading off into the dark.', + build(context) { + const life = create(context, 'B3/S23'); + life.randomize(0.32); + return life; + }, + }, +]; + +/** A demo by id or name, case-insensitively. */ +export function getDemo(id: string): Demo | undefined { + const key = id.trim().toLowerCase().replace(/\s+/g, ''); + return demos.find((d) => d.id === key || d.name.toLowerCase().replace(/\s+/g, '') === key); +} + +/** Build a demo's universe, falling back to Conway's if the id is unknown. */ +export function buildDemo(id: string, context: DemoContext): Automata { + return (getDemo(id) ?? demos[0]!).build(context); +} + +/** The demo shown on the intro screen. */ +export const INTRO_DEMO = 'conway'; + +function rotate(cells: Array<[number, number]>, quarterTurns: number): Array<[number, number]> { + let out = cells.map(([x, y]) => [x, y] as [number, number]); + for (let t = 0; t < ((quarterTurns % 4) + 4) % 4; t++) out = out.map(([x, y]) => [-y, x] as [number, number]); + const minX = Math.min(...out.map(([x]) => x)); + const minY = Math.min(...out.map(([, y]) => y)); + return out.map(([x, y]) => [x - minX, y - minY] as [number, number]); +} diff --git a/packages/complexity-automata/src/index.ts b/packages/complexity-automata/src/index.ts new file mode 100644 index 0000000..ee830dd --- /dev/null +++ b/packages/complexity-automata/src/index.ts @@ -0,0 +1,60 @@ +/** + * complexity-automata — a cellular automata engine for the browser, the phone + * and the terminal. + * + * ```ts + * import { Automata, demos, saveUniverse } from 'complexity-automata'; + * + * const life = new Automata({ width: 80, height: 40, rule: 'B3/S23' }); + * life.stampCentered('Glider Gun'); + * life.run(100); + * console.log(life.toString()); + * ``` + */ + +export { Automata, fromString, mulberry32, type AutomataOptions } from './automata.js'; +export { Autopilot, type AutopilotOptions, type UniverseChange } from './autopilot.js'; +export { blend, hslToRgb, mix, parseColor, rgbToHex, seedColor, type RGB } from './color.js'; +export { buildDemo, demos, getDemo, INTRO_DEMO, type Demo, type DemoContext } from './demos.js'; +export { + getPattern, + patterns, + patternsByCategory, + patternSize, + POINT, + rotatePattern, + type Pattern, + type PatternCategory, +} from './patterns.js'; +export { + CONWAY, + formatRule, + NAMED_RULES, + normalizeRuleNumber, + parseRule, + RULE_BITS, + RULE_SPACE, + ruleNumberToRuleSet, + rulesEqual, + ruleSetToRuleNumber, + toRuleNumber, + type RuleLike, + type RuleSet, +} from './rules.js'; +export { + decodeRLE, + encodeRLE, + fromShareString, + loadUniverse, + measureRLE, + memoryStorage, + parseUniverse, + ruleToString, + saveUniverse, + toShareString, + UNIVERSE_FORMAT, + UNIVERSE_VERSION, + UniverseLibrary, + type UniverseFile, + type UniverseStorage, +} from './universe.js'; diff --git a/packages/complexity-automata/src/patterns.ts b/packages/complexity-automata/src/patterns.ts new file mode 100644 index 0000000..d063829 --- /dev/null +++ b/packages/complexity-automata/src/patterns.ts @@ -0,0 +1,532 @@ +/** + * A catalogue of starting shapes, as `[x, y]` offsets from where you stamp them. + * + * Most are the classic Conway patterns; the `fractal` group are seeds that only + * do something interesting under a replicator-style rule (`B1357/S1357` and + * friends), where a single cell copies itself into a Sierpinski triangle. + * + * More patterns, and the RLE files this format mirrors: + * https://conwaylife.com/book/#rle_files + */ + +export type PatternCategory = 'spaceship' | 'gun' | 'oscillator' | 'still' | 'methuselah' | 'fractal' | 'tool'; + +export interface Pattern { + /** Display name, also the lookup key for {@link getPattern}. */ + name: string; + /** Live cells as `[x, y]` offsets. Empty for the freehand drawing tool. */ + cells: Array<[number, number]>; + category: PatternCategory; + /** One line on what it does — shown in tooltips and `--list` output. */ + about?: string; + /** Rule this pattern is designed for, when it isn't Conway's. */ + rule?: string; +} + +/** Freehand drawing: toggles the single cell under the cursor. */ +export const POINT: Pattern = { + name: 'Point On/Off', + cells: [], + category: 'tool', + about: 'Draw cell by cell.', +}; + +export const patterns: Pattern[] = [ + POINT, + { + name: 'Glider', + category: 'spaceship', + about: 'The smallest spaceship — walks diagonally forever.', + cells: [ + [0, 2], + [1, 0], + [1, 2], + [2, 1], + [2, 2], + ], + }, + { + name: 'Glider Fleet', + category: 'spaceship', + about: 'Four gliders heading out to the four corners.', + cells: [ + [0, 2], + [1, 0], + [1, 2], + [2, 1], + [2, 2], + [14, 2], + [13, 0], + [13, 2], + [12, 1], + [12, 2], + [0, 12], + [1, 14], + [1, 12], + [2, 13], + [2, 12], + [14, 12], + [13, 14], + [13, 12], + [12, 13], + [12, 12], + ], + }, + { + name: 'Lightweight Spaceship', + category: 'spaceship', + about: 'Travels straight across the grid, two cells every four generations.', + cells: [ + [1, 0], + [4, 0], + [0, 1], + [0, 2], + [4, 2], + [0, 3], + [1, 3], + [2, 3], + [3, 3], + ], + }, + { + name: 'Glider Gun', + category: 'gun', + about: "Gosper's gun — emits a glider every 30 generations, forever.", + cells: [ + [25, 0], + [22, 1], + [23, 1], + [24, 1], + [25, 1], + [30, 1], + [13, 2], + [21, 2], + [22, 2], + [23, 2], + [24, 2], + [30, 2], + [12, 3], + [14, 3], + [21, 3], + [24, 3], + [34, 3], + [35, 3], + [11, 4], + [15, 4], + [16, 4], + [21, 4], + [22, 4], + [23, 4], + [24, 4], + [34, 4], + [35, 4], + [0, 5], + [1, 5], + [11, 5], + [15, 5], + [16, 5], + [22, 5], + [23, 5], + [24, 5], + [25, 5], + [0, 6], + [1, 6], + [11, 6], + [15, 6], + [16, 6], + [25, 6], + [12, 7], + [14, 7], + [13, 8], + ], + }, + { + name: 'Pulsar', + category: 'oscillator', + about: 'A period-3 oscillator that beats like a heart.', + cells: [ + [2, 0], + [3, 0], + [4, 0], + [8, 0], + [9, 0], + [10, 0], + [0, 2], + [5, 2], + [7, 2], + [12, 2], + [0, 3], + [5, 3], + [7, 3], + [12, 3], + [0, 4], + [5, 4], + [7, 4], + [12, 4], + [2, 5], + [3, 5], + [4, 5], + [8, 5], + [9, 5], + [10, 5], + [2, 7], + [3, 7], + [4, 7], + [8, 7], + [9, 7], + [10, 7], + [0, 8], + [5, 8], + [7, 8], + [12, 8], + [0, 9], + [5, 9], + [7, 9], + [12, 9], + [0, 10], + [5, 10], + [7, 10], + [12, 10], + [2, 12], + [3, 12], + [4, 12], + [8, 12], + [9, 12], + [10, 12], + ], + }, + { + name: 'Tumbler', + category: 'oscillator', + about: 'Period-14 oscillator that rocks side to side.', + cells: [ + [0, 1], + [0, 2], + [1, 0], + [2, 1], + [2, 3], + [2, 4], + [3, 2], + [3, 4], + [5, 2], + [5, 4], + [6, 1], + [6, 3], + [6, 4], + [7, 0], + [8, 1], + [8, 2], + ], + }, + { + name: 'Octagon', + category: 'oscillator', + about: 'Breathes in and out on a period of 5.', + cells: [ + [0, 6], + [0, 7], + [1, 5], + [1, 8], + [2, 4], + [2, 9], + [3, 3], + [3, 10], + [4, 3], + [4, 10], + [5, 4], + [5, 9], + [6, 5], + [6, 8], + [7, 6], + [7, 7], + ], + }, + { + name: 'Pinwheel', + category: 'oscillator', + about: 'Four-fold rotor, period 4.', + cells: [ + [0, 4], + [0, 5], + [1, 4], + [1, 5], + [3, 4], + [3, 5], + [3, 6], + [3, 7], + [4, 3], + [4, 8], + [4, 10], + [4, 11], + [5, 3], + [5, 7], + [5, 8], + [5, 10], + [5, 11], + [6, 0], + [6, 1], + [6, 3], + [6, 5], + [6, 8], + [7, 0], + [7, 1], + [7, 3], + [7, 6], + [7, 8], + [8, 4], + [8, 5], + [8, 6], + [8, 7], + [10, 6], + [10, 7], + [11, 6], + [11, 7], + ], + }, + { + name: 'Beehive', + category: 'still', + about: 'A still life — nothing ever changes unless something hits it.', + cells: [ + [0, 0], + [0, 6], + [1, 0], + [1, 6], + [2, 1], + [2, 5], + [3, 2], + [3, 3], + [3, 4], + ], + }, + { + name: 'R-pentomino', + category: 'methuselah', + about: 'Five cells that take 1103 generations to settle down.', + cells: [ + [0, 1], + [1, 0], + [1, 1], + [1, 2], + [2, 0], + ], + }, + { + name: 'Acorn', + category: 'methuselah', + about: 'Seven cells that run for over 5000 generations.', + cells: [ + [1, 0], + [3, 1], + [0, 2], + [1, 2], + [4, 2], + [5, 2], + [6, 2], + ], + }, + { + name: 'Diehard', + category: 'methuselah', + about: 'Vanishes completely after exactly 130 generations.', + cells: [ + [6, 0], + [0, 1], + [1, 1], + [1, 2], + [5, 2], + [6, 2], + [7, 2], + ], + }, + { + name: 'Bar', + category: 'methuselah', + about: 'A ten cell line — simple input, complicated output.', + cells: [ + [0, 0], + [1, 0], + [2, 0], + [3, 0], + [4, 0], + [5, 0], + [6, 0], + [7, 0], + [8, 0], + [9, 0], + ], + }, + { + name: 'Goose', + category: 'spaceship', + about: 'A larger travelling shape.', + cells: [ + [0, 0], + [0, 1], + [0, 2], + [1, 0], + [1, 10], + [1, 11], + [2, 1], + [2, 8], + [2, 9], + [2, 10], + [2, 12], + [3, 3], + [3, 4], + [3, 7], + [3, 8], + [4, 4], + [5, 8], + [6, 4], + [6, 5], + [6, 9], + [7, 3], + [7, 5], + [7, 7], + [7, 8], + [8, 3], + [8, 5], + [8, 8], + [8, 10], + [8, 11], + [9, 2], + [9, 7], + [9, 8], + [10, 2], + [10, 3], + [11, 2], + [11, 3], + ], + }, + { + name: 'Unix', + category: 'oscillator', + about: 'Period-6 oscillator named for its shape.', + cells: [ + [0, 4], + [0, 5], + [1, 0], + [1, 1], + [1, 3], + [2, 0], + [2, 1], + [2, 4], + [2, 7], + [3, 5], + [3, 7], + [4, 6], + [6, 5], + [6, 6], + [7, 5], + [7, 6], + ], + }, + { + name: 'Lightsaber', + category: 'oscillator', + about: 'A diagonal blade of cells.', + cells: [ + [1, 0], + [1, 1], + [2, 0], + [2, 2], + [3, 1], + [3, 2], + [4, 3], + [4, 4], + [5, 3], + [6, 4], + [6, 6], + [8, 6], + [8, 8], + [9, 9], + [10, 8], + [10, 9], + ], + }, + { + name: 'Fractal Seed', + category: 'fractal', + rule: 'B1357/S1357', + about: 'One cell. Under a replicator rule it copies itself into a Sierpinski triangle.', + cells: [[0, 0]], + }, + { + name: 'Fractal Cross', + category: 'fractal', + rule: 'B1357/S1357', + about: 'A plus sign — replicator rules turn it into nested diamonds.', + cells: [ + [1, 0], + [0, 1], + [1, 1], + [2, 1], + [1, 2], + ], + }, + { + name: 'Fractal Triangle', + category: 'fractal', + rule: 'B1357/S02468', + about: 'A Sierpinski-triangle seed row for Fredkin replication.', + cells: [ + [0, 0], + [2, 0], + [4, 0], + [1, 1], + [3, 1], + [2, 2], + ], + }, + { + name: 'Fractal Comb', + category: 'fractal', + rule: 'B1/S1', + about: 'Gnarl rule seed — grows an endlessly detailed woven texture.', + cells: [ + [0, 0], + [2, 0], + [4, 0], + [6, 0], + ], + }, +]; + +/** Look a pattern up by name, case- and space-insensitively. */ +export function getPattern(name: string): Pattern | undefined { + const key = name.trim().toLowerCase().replace(/\s+/g, ''); + return patterns.find((p) => p.name.toLowerCase().replace(/\s+/g, '') === key); +} + +/** Every pattern in a category, in catalogue order. */ +export function patternsByCategory(category: PatternCategory): Pattern[] { + return patterns.filter((p) => p.category === category); +} + +/** Width and height of a pattern's bounding box. */ +export function patternSize(pattern: Pattern): { width: number; height: number } { + if (pattern.cells.length === 0) return { width: 0, height: 0 }; + let maxX = 0; + let maxY = 0; + for (const [x, y] of pattern.cells) { + if (x > maxX) maxX = x; + if (y > maxY) maxY = y; + } + return { width: maxX + 1, height: maxY + 1 }; +} + +/** Rotate a pattern by 90° steps, normalised back to the origin. */ +export function rotatePattern(pattern: Pattern, quarterTurns = 1): Pattern { + const turns = ((quarterTurns % 4) + 4) % 4; + let cells = pattern.cells.map(([x, y]) => [x, y] as [number, number]); + for (let t = 0; t < turns; t++) cells = cells.map(([x, y]) => [-y, x] as [number, number]); + let minX = Infinity; + let minY = Infinity; + for (const [x, y] of cells) { + if (x < minX) minX = x; + if (y < minY) minY = y; + } + if (!Number.isFinite(minX)) return { ...pattern, cells: [] }; + return { ...pattern, cells: cells.map(([x, y]) => [x - minX, y - minY] as [number, number]) }; +} diff --git a/packages/complexity-automata/src/rules.ts b/packages/complexity-automata/src/rules.ts new file mode 100644 index 0000000..e3d1559 --- /dev/null +++ b/packages/complexity-automata/src/rules.ts @@ -0,0 +1,138 @@ +/** + * Rule sets for 2-state, 9-neighbour (Moore neighbourhood) cellular automata. + * + * A rule is two sets of neighbour counts: + * - `birth` — counts that bring a dead cell to life + * - `survive` — counts that keep a live cell alive + * + * Conway's Game of Life is `B3/S23`: born with exactly 3 neighbours, + * survives with 2 or 3. + * + * Every rule also has a **rule number**: an 18 bit integer where bits 0..8 are + * the birth counts 0..8 and bits 9..17 are the survival counts 0..8. That makes + * the whole space of 262,144 universes addressable by a single integer, which is + * what the "random universe" mode walks through. + */ + +/** Neighbour counts that create and preserve life. */ +export interface RuleSet { + birth: number[]; + survive: number[]; +} + +/** Number of bits in a rule number: 9 birth counts + 9 survival counts. */ +export const RULE_BITS = 18; + +/** One past the largest valid rule number (2 ** 18). */ +export const RULE_SPACE = 1 << RULE_BITS; + +/** Anything that can be turned into a {@link RuleSet}. */ +export type RuleLike = RuleSet | number | string; + +const sortUnique = (counts: Iterable): number[] => + [...new Set(counts)].filter((n) => Number.isInteger(n) && n >= 0 && n <= 8).sort((a, b) => a - b); + +/** Expand a rule number into the neighbour counts it encodes. */ +export function ruleNumberToRuleSet(ruleNumber: number): RuleSet { + const n = normalizeRuleNumber(ruleNumber); + const birth: number[] = []; + const survive: number[] = []; + for (let i = 0; i <= 8; i++) { + if (n & (1 << i)) birth.push(i); + if (n & (1 << (i + 9))) survive.push(i); + } + return { birth, survive }; +} + +/** Pack neighbour counts back into an 18 bit rule number. */ +export function ruleSetToRuleNumber(rule: RuleSet): number { + let n = 0; + for (const b of sortUnique(rule.birth ?? [])) n |= 1 << b; + for (const s of sortUnique(rule.survive ?? [])) n |= 1 << (s + 9); + return n; +} + +/** Clamp any number into the rule space, wrapping rather than throwing. */ +export function normalizeRuleNumber(ruleNumber: number): number { + const n = Math.floor(Number(ruleNumber) || 0); + return ((n % RULE_SPACE) + RULE_SPACE) % RULE_SPACE; +} + +/** Render a rule set in the conventional `B3/S23` notation. */ +export function formatRule(rule: RuleSet): string { + return `B${sortUnique(rule.birth).join('')}/S${sortUnique(rule.survive).join('')}`; +} + +/** + * Accept a rule in any of the shapes people actually have one in: + * `{ birth, survive }`, a rule number, `"B3/S23"`, `"3/23"`, or `"23/3"` + * (the older Golly ordering, detected by the `S` prefix being absent and the + * first group being the survival counts). + */ +export function parseRule(rule: RuleLike): RuleSet { + if (typeof rule === 'number') return ruleNumberToRuleSet(rule); + if (rule && typeof rule === 'object') { + return { birth: sortUnique(rule.birth ?? []), survive: sortUnique(rule.survive ?? []) }; + } + + const text = String(rule).trim(); + if (text === '') return { birth: [], survive: [] }; + if (/^\d+$/.test(text) && !text.includes('/')) return ruleNumberToRuleSet(Number(text)); + + const named = NAMED_RULES[text.toLowerCase()]; + if (named) return parseRule(named); + + const [left = '', right = ''] = text.split('/'); + const digits = (part: string): number[] => sortUnique([...part.replace(/[^\d]/g, '')].map(Number)); + + // `S23/B3` and `B3/S23` are both common; so is the bare `3/23` (birth first) + // and Golly's `23/3` (survive first). + if (/^\s*s/i.test(left) || /^\s*b/i.test(right)) { + return { birth: digits(right), survive: digits(left) }; + } + return { birth: digits(left), survive: digits(right) }; +} + +/** Convenience: anything rule-like straight to a rule number. */ +export function toRuleNumber(rule: RuleLike): number { + return typeof rule === 'number' ? normalizeRuleNumber(rule) : ruleSetToRuleNumber(parseRule(rule)); +} + +/** Are these the same rule? */ +export function rulesEqual(a: RuleLike, b: RuleLike): boolean { + return toRuleNumber(a) === toRuleNumber(b); +} + +/** + * Rules worth knowing by name. Each one is a different universe with its own + * physics — Conway's is only the most famous of them. + */ +export const NAMED_RULES: Record = { + conway: 'B3/S23', + life: 'B3/S23', + highlife: 'B36/S23', + daynight: 'B3678/S34678', + seeds: 'B2/S', + replicator: 'B1357/S1357', + fredkin: 'B1357/S02468', + maze: 'B3/S12345', + mazectric: 'B3/S1234', + coral: 'B3/S45678', + coagulations: 'B378/S235678', + diamoeba: 'B35678/S5678', + amoeba: 'B357/S1358', + anneal: 'B4678/S35678', + assimilation: 'B345/S4567', + gnarl: 'B1/S1', + lifewithoutdeath: 'B3/S012345678', + serviettes: 'B234/S', + walledcities: 'B45678/S2345', + '34life': 'B34/S34', + '2x2': 'B36/S125', + morley: 'B368/S245', + stains: 'B3678/S235678', + vote: 'B5678/S45678', +}; + +/** Conway's Game of Life, the rule everything else is measured against. */ +export const CONWAY: RuleSet = parseRule('B3/S23'); diff --git a/packages/complexity-automata/src/universe.ts b/packages/complexity-automata/src/universe.ts new file mode 100644 index 0000000..1741e50 --- /dev/null +++ b/packages/complexity-automata/src/universe.ts @@ -0,0 +1,355 @@ +/** + * Saving universes. + * + * A universe is its rule plus its live cells, and that is all this format + * stores. Cells are held as Life RLE (`b`/`o`/`$`/`!`), the same encoding + * conwaylife.com uses, so a saved universe can be pasted into other Life + * software and patterns from elsewhere can be loaded here. + * + * Three ways out, all lossless for the parts that matter: + * - {@link saveUniverse} → a JSON object, for a file or localStorage + * - {@link toShareString} → one URL-safe string, for a `?u=` link + * - {@link encodeRLE} → just the cells, for other Life programs + */ + +import { Automata, type AutomataOptions } from './automata.js'; +import { formatRule, parseRule, type RuleLike } from './rules.js'; + +export const UNIVERSE_FORMAT = 'complexity-automata'; +export const UNIVERSE_VERSION = 1; + +/** A saved universe: everything needed to bring it back exactly as it was. */ +export interface UniverseFile { + format: typeof UNIVERSE_FORMAT; + version: number; + name: string; + /** Rule in `B3/S23` notation. */ + rule: string; + width: number; + height: number; + generation: number; + wrap: boolean; + /** Live cells in Life RLE. */ + cells: string; + /** Live cell count at save time — cheap to show in a list without decoding. */ + population?: number; + /** ISO timestamp. */ + savedAt?: string; + /** Free-form note the user typed. */ + note?: string; +} + +// ------------------------------------------------------------------ Life RLE + +/** Encode the live cells of a grid as Life RLE. */ +export function encodeRLE(automata: Automata): string { + const tokens: string[] = []; + let runChar = ''; + let runLength = 0; + + const flush = (): void => { + if (runLength === 0) return; + tokens.push(runLength === 1 ? runChar : `${runLength}${runChar}`); + runLength = 0; + }; + const push = (char: string): void => { + if (char === runChar) { + runLength++; + return; + } + flush(); + runChar = char; + runLength = 1; + }; + + for (let y = 0; y < automata.height; y++) { + // Trailing dead cells on a row are implied by the row terminator. + let lastLive = -1; + for (let x = automata.width - 1; x >= 0; x--) { + if (automata.cells[y * automata.width + x]) { + lastLive = x; + break; + } + } + for (let x = 0; x <= lastLive; x++) push(automata.cells[y * automata.width + x] ? 'o' : 'b'); + if (y < automata.height - 1) push('$'); + } + flush(); + return `${tokens.join('')}!`; +} + +/** Decode Life RLE onto a grid, ignoring `#` comments and an `x = …` header. */ +export function decodeRLE(rle: string, automata: Automata, offsetX = 0, offsetY = 0): Automata { + const body = rle + .split('\n') + .filter((line) => !line.trimStart().startsWith('#') && !/^\s*x\s*=/i.test(line)) + .join('') + .replace(/\s+/g, ''); + + let x = 0; + let y = 0; + let count = 0; + for (const char of body) { + if (char >= '0' && char <= '9') { + count = count * 10 + Number(char); + continue; + } + const repeat = count || 1; + count = 0; + if (char === '!') break; + if (char === '$') { + y += repeat; + x = 0; + } else if (char === 'b' || char === '.') { + x += repeat; + } else if (char === 'o' || char === 'A') { + for (let i = 0; i < repeat; i++) automata.set(x + i + offsetX, y + offsetY, true); + x += repeat; + } + } + return automata; +} + +/** Bounding-box dimensions of an RLE blob, without allocating a grid. */ +export function measureRLE(rle: string): { width: number; height: number } { + const body = rle + .split('\n') + .filter((line) => !line.trimStart().startsWith('#') && !/^\s*x\s*=/i.test(line)) + .join('') + .replace(/\s+/g, ''); + let x = 0; + let width = 0; + let rows = 1; + let count = 0; + for (const char of body) { + if (char >= '0' && char <= '9') { + count = count * 10 + Number(char); + continue; + } + const repeat = count || 1; + count = 0; + if (char === '!') break; + if (char === '$') { + rows += repeat; + if (x > width) width = x; + x = 0; + } else if (char === 'b' || char === '.' || char === 'o' || char === 'A') { + x += repeat; + } + } + if (x > width) width = x; + return { width, height: rows }; +} + +// ------------------------------------------------------------- save and load + +/** Capture a running universe as a saveable object. */ +export function saveUniverse( + automata: Automata, + meta: { name?: string; note?: string } = {}, +): UniverseFile { + return { + format: UNIVERSE_FORMAT, + version: UNIVERSE_VERSION, + name: meta.name?.trim() || `${formatRule(automata.rule)} @ gen ${automata.generation}`, + rule: automata.ruleString, + width: automata.width, + height: automata.height, + generation: automata.generation, + wrap: automata.wrap, + population: automata.population, + cells: encodeRLE(automata), + savedAt: new Date().toISOString(), + ...(meta.note ? { note: meta.note } : {}), + }; +} + +/** Restore a saved universe. Pass `width`/`height` to reflow it onto a different screen. */ +export function loadUniverse( + input: UniverseFile | string, + overrides: Partial = {}, +): Automata { + const file: UniverseFile = typeof input === 'string' ? parseUniverse(input) : input; + const width = Math.max(1, Math.floor(overrides.width ?? file.width)); + const height = Math.max(1, Math.floor(overrides.height ?? file.height)); + const automata = new Automata({ + width, + height, + rule: overrides.rule ?? file.rule, + wrap: overrides.wrap ?? file.wrap ?? true, + colors: overrides.colors ?? true, + seed: overrides.seed, + }); + + // Centre the saved pattern when it is being restored onto a larger screen. + const offsetX = Math.max(0, Math.floor((width - file.width) / 2)); + const offsetY = Math.max(0, Math.floor((height - file.height) / 2)); + decodeRLE(file.cells ?? '', automata, offsetX, offsetY); + automata.generation = file.generation ?? 0; + return automata; +} + +/** Parse a universe from JSON, a share string, or bare RLE. */ +export function parseUniverse(text: string): UniverseFile { + const trimmed = text.trim(); + + if (trimmed.startsWith('{')) { + const parsed = JSON.parse(trimmed) as UniverseFile; + if (parsed.format !== UNIVERSE_FORMAT) { + throw new Error(`Not a ${UNIVERSE_FORMAT} universe (format: ${String(parsed.format)})`); + } + return parsed; + } + + // Bare RLE, possibly with the `#N name` / `#r rule` header lines Life uses. + if (/[bo$!]/.test(trimmed) && !/^[A-Za-z0-9_-]+$/.test(trimmed)) { + const nameLine = /^#N\s+(.+)$/m.exec(trimmed); + const ruleLine = /rule\s*=\s*([^\s,]+)/i.exec(trimmed) ?? /^#r\s+(.+)$/m.exec(trimmed); + const { width, height } = measureRLE(trimmed); + return { + format: UNIVERSE_FORMAT, + version: UNIVERSE_VERSION, + name: nameLine?.[1]?.trim() || 'Imported pattern', + rule: formatRule(parseRule(ruleLine?.[1] ?? 'B3/S23')), + width, + height, + generation: 0, + wrap: true, + cells: trimmed, + }; + } + + return fromShareString(trimmed); +} + +// ------------------------------------------------------------ share strings + +const toBase64 = (text: string): string => { + const bytes = new TextEncoder().encode(text); + if (typeof globalThis.btoa === 'function') { + let binary = ''; + for (const byte of bytes) binary += String.fromCharCode(byte); + return globalThis.btoa(binary); + } + return Buffer.from(bytes).toString('base64'); +}; + +const fromBase64 = (text: string): string => { + if (typeof globalThis.atob === 'function') { + const binary = globalThis.atob(text); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); + return new TextDecoder().decode(bytes); + } + return Buffer.from(text, 'base64').toString('utf8'); +}; + +/** Pack a universe into one URL-safe string, for `?u=` links and QR codes. */ +export function toShareString(input: Automata | UniverseFile, meta?: { name?: string }): string { + const file = input instanceof Automata ? saveUniverse(input, meta) : input; + const compact = [file.name, file.rule, file.width, file.height, file.generation, file.wrap ? 1 : 0, file.cells]; + return toBase64(JSON.stringify(compact)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); +} + +/** Unpack a {@link toShareString} string. */ +export function fromShareString(share: string): UniverseFile { + const base64 = share.replace(/-/g, '+').replace(/_/g, '/'); + const [name, rule, width, height, generation, wrap, cells] = JSON.parse(fromBase64(base64)) as [ + string, + string, + number, + number, + number, + number, + string, + ]; + return { + format: UNIVERSE_FORMAT, + version: UNIVERSE_VERSION, + name, + rule, + width, + height, + generation, + wrap: Boolean(wrap), + cells, + }; +} + +// ---------------------------------------------------------------- the shelf + +/** The slice of `localStorage` (or any key/value store) this library needs. */ +export interface UniverseStorage { + getItem(key: string): string | null; + setItem(key: string, value: string): void; + removeItem(key: string): void; +} + +/** + * A named shelf of saved universes on top of any key/value store — + * `localStorage` in the browser, a JSON file in the CLI. + */ +export class UniverseLibrary { + constructor( + private storage: UniverseStorage, + private key = 'complexity-automata:universes', + ) {} + + /** Every saved universe, newest first. */ + list(): UniverseFile[] { + try { + const raw = this.storage.getItem(this.key); + const parsed: unknown = raw ? JSON.parse(raw) : []; + return Array.isArray(parsed) ? (parsed as UniverseFile[]) : []; + } catch { + return []; + } + } + + /** Look one up by name. */ + get(name: string): UniverseFile | undefined { + const key = name.trim().toLowerCase(); + return this.list().find((u) => u.name.trim().toLowerCase() === key); + } + + /** Save a universe, replacing any earlier one with the same name. */ + save(input: Automata | UniverseFile, meta: { name?: string; note?: string } = {}): UniverseFile { + const file = input instanceof Automata ? saveUniverse(input, meta) : { ...input, ...meta }; + const rest = this.list().filter((u) => u.name.trim().toLowerCase() !== file.name.trim().toLowerCase()); + this.write([file, ...rest]); + return file; + } + + /** Forget a universe. Returns whether there was one to forget. */ + remove(name: string): boolean { + const key = name.trim().toLowerCase(); + const kept = this.list().filter((u) => u.name.trim().toLowerCase() !== key); + const removed = kept.length !== this.list().length; + if (removed) this.write(kept); + return removed; + } + + /** Empty the shelf. */ + clear(): void { + this.write([]); + } + + private write(universes: UniverseFile[]): void { + this.storage.setItem(this.key, JSON.stringify(universes)); + } +} + +/** An in-memory {@link UniverseStorage}, for tests and for SSR. */ +export function memoryStorage(): UniverseStorage { + const map = new Map(); + return { + getItem: (key) => map.get(key) ?? null, + setItem: (key, value) => void map.set(key, value), + removeItem: (key) => void map.delete(key), + }; +} + +/** Rule-like value straight to the notation a saved file stores. */ +export function ruleToString(rule: RuleLike): string { + return formatRule(parseRule(rule)); +} diff --git a/packages/complexity-automata/test/automata.test.ts b/packages/complexity-automata/test/automata.test.ts new file mode 100644 index 0000000..b7ee877 --- /dev/null +++ b/packages/complexity-automata/test/automata.test.ts @@ -0,0 +1,155 @@ +import { describe, expect, test } from 'bun:test'; +import { Automata, fromString } from '../src/automata.js'; + +describe('Conway rules', () => { + test('a blinker oscillates with period 2', () => { + const life = fromString('.....\n.....\n.###.\n.....\n.....', { wrap: false }); + const start = life.toString(); + life.step(); + expect(life.toString()).not.toBe(start); + life.step(); + expect(life.toString()).toBe(start); + }); + + test('a block is a still life', () => { + const life = fromString('....\n.##.\n.##.\n....', { wrap: false }); + const start = life.toString(); + life.run(5); + expect(life.toString()).toBe(start); + }); + + test('a glider moves one cell diagonally every four generations', () => { + const life = new Automata({ width: 20, height: 20, rule: 'B3/S23', wrap: false }); + life.stamp('Glider', 2, 2); + const before = life.liveCells(); + life.run(4); + const after = life.liveCells(); + expect(after.length).toBe(before.length); + const shifted = before.map(([x, y]) => `${x + 1},${y + 1}`).sort(); + expect(after.map(([x, y]) => `${x},${y}`).sort()).toEqual(shifted); + }); + + test('lone cells die of loneliness', () => { + const life = new Automata({ width: 10, height: 10, wrap: false }); + life.set(5, 5, true); + life.step(); + expect(life.population).toBe(0); + }); +}); + +describe('grid mechanics', () => { + test('wrap makes the grid a torus', () => { + const wrapped = new Automata({ width: 8, height: 8, wrap: true }); + expect(wrapped.index(-1, -1)).toBe(7 * 8 + 7); + + const bounded = new Automata({ width: 8, height: 8, wrap: false }); + expect(bounded.index(-1, -1)).toBe(-1); + expect(bounded.get(-1, -1)).toBe(0); + }); + + test('population tracks set, toggle and clear', () => { + const life = new Automata({ width: 6, height: 6 }); + life.set(1, 1, true).set(1, 1, true).toggle(2, 2); + expect(life.population).toBe(2); + life.toggle(2, 2); + expect(life.population).toBe(1); + life.clear(); + expect(life.population).toBe(0); + }); + + test('neighbour counts ignore the cell itself', () => { + const life = fromString('###\n###\n###', { wrap: false }); + expect(life.countNeighbors(1, 1)).toBe(8); + expect(life.countNeighbors(0, 0)).toBe(3); + }); + + test('resizing keeps the cells that still fit', () => { + const life = new Automata({ width: 10, height: 10, wrap: false }); + life.stamp('Glider', 1, 1); + const bigger = life.resized(20, 20); + expect(bigger.liveCells()).toEqual(life.liveCells()); + expect(bigger.width).toBe(20); + }); + + test('clones are independent', () => { + const life = new Automata({ width: 8, height: 8 }); + life.set(0, 0, true); + const copy = life.clone(); + copy.set(1, 1, true); + expect(life.get(1, 1)).toBe(0); + expect(copy.get(0, 0)).toBe(1); + }); +}); + +describe('determinism', () => { + test('the same seed gives the same random soup', () => { + const a = new Automata({ width: 30, height: 30, seed: 42 }).randomize(0.3); + const b = new Automata({ width: 30, height: 30, seed: 42 }).randomize(0.3); + expect(a.toString()).toBe(b.toString()); + }); + + test('different seeds give different soups', () => { + const a = new Automata({ width: 30, height: 30, seed: 1 }).randomize(0.3); + const b = new Automata({ width: 30, height: 30, seed: 2 }).randomize(0.3); + expect(a.toString()).not.toBe(b.toString()); + }); +}); + +describe('stagnation', () => { + test('an empty grid is stagnant', () => { + const life = new Automata({ width: 10, height: 10 }); + expect(life.isStagnant()).toBe(true); + }); + + test('a still life is stagnant once it has a few generations of history', () => { + const life = fromString('....\n.##.\n.##.\n....', { wrap: false }); + life.run(5); + expect(life.isStagnant()).toBe(true); + }); + + test('a glider on a big grid is not stagnant', () => { + const life = new Automata({ width: 40, height: 40, wrap: false }); + life.stamp('Glider', 2, 2); + life.run(6); + expect(life.isStagnant()).toBe(false); + }); +}); + +describe('colour inheritance', () => { + test('a cell born beside a colony inherits its colour', () => { + const life = new Automata({ width: 12, height: 12, wrap: false }); + life.set(5, 5, true, [200, 0, 0]); + life.set(5, 6, true, [200, 0, 0]); + life.set(6, 5, true, [200, 0, 0]); + life.step(); + expect(life.colorAt(6, 6)).toEqual([200, 0, 0]); + }); + + test('dead cells have no colour', () => { + const life = new Automata({ width: 6, height: 6 }); + expect(life.colorAt(0, 0)).toEqual([0, 0, 0]); + }); + + test('colour tracking can be turned off', () => { + const life = new Automata({ width: 6, height: 6, colors: false }); + life.set(1, 1, true); + expect(life.colors.length).toBe(0); + expect(life.get(1, 1)).toBe(1); + }); +}); + +describe('rules on a running grid', () => { + test('toggling rule bits rewrites the rule string', () => { + const life = new Automata({ width: 8, height: 8, rule: 'B3/S23' }); + life.toggleRuleBit('birth', 6); + expect(life.ruleString).toBe('B36/S23'); + life.toggleRuleBit('survive', 2); + expect(life.ruleString).toBe('B36/S3'); + }); + + test('rule numbers survive a round trip through the engine', () => { + const life = new Automata({ width: 8, height: 8, rule: 6152 }); + expect(life.ruleString).toBe('B3/S23'); + expect(life.ruleNumber).toBe(6152); + }); +}); diff --git a/packages/complexity-automata/test/demos.test.ts b/packages/complexity-automata/test/demos.test.ts new file mode 100644 index 0000000..c8d7295 --- /dev/null +++ b/packages/complexity-automata/test/demos.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from 'bun:test'; +import { Autopilot } from '../src/autopilot.js'; +import { buildDemo, demos, getDemo } from '../src/demos.js'; + +describe('demos', () => { + test('every demo builds a living universe at terminal size', () => { + for (const demo of demos) { + const life = demo.build({ width: 80, height: 24, seed: 7 }); + expect(life.population).toBeGreaterThan(0); + expect(life.ruleString).toBe(demo.rule); + } + }); + + test('every demo survives its first generations at phone size', () => { + for (const demo of demos) { + const life = demo.build({ width: 60, height: 100, seed: 3 }); + life.run(10); + expect(life.width).toBe(60); + } + }); + + test('the replicator demo grows a fractal from one cell', () => { + const life = buildDemo('fractal', { width: 65, height: 65, seed: 1 }); + expect(life.population).toBe(1); + life.run(8); + // Replication doubles outward: by generation 8 the single cell has copied + // itself into a ring of descendants spread across many rows. + expect(life.population).toBeGreaterThanOrEqual(8); + const rows = new Set(life.liveCells().map(([, y]) => y)); + expect(rows.size).toBeGreaterThan(2); + }); + + test('demos are addressable by id and by name', () => { + expect(getDemo('conway')?.id).toBe('conway'); + expect(getDemo("Conway's Game of Life")?.id).toBe('conway'); + expect(getDemo('nope')).toBeUndefined(); + }); + + test('an unknown id falls back to the first demo', () => { + expect(buildDemo('nope', { width: 30, height: 30 }).ruleString).toBe(demos[0]!.rule); + }); +}); + +describe('autopilot', () => { + test('starts a new universe when the current one stagnates', () => { + const life = buildDemo('conway', { width: 40, height: 40, seed: 5 }); + const seen: string[] = []; + const pilot = new Autopilot(life, { minGenerations: 2, maxGenerations: 6, onUniverse: (i) => seen.push(i.rule) }); + + for (let i = 0; i < 40; i++) { + life.step(); + pilot.tick(); + } + expect(seen.length).toBeGreaterThan(0); + expect(life.population).toBeGreaterThan(0); + }); + + test('a manual jump reports why it happened', () => { + const life = buildDemo('soup', { width: 30, height: 30, seed: 9 }); + let reason = ''; + new Autopilot(life, { onUniverse: (i) => (reason = i.reason) }).next(); + expect(reason).toBe('manual'); + }); +}); diff --git a/packages/complexity-automata/test/rules.test.ts b/packages/complexity-automata/test/rules.test.ts new file mode 100644 index 0000000..bd5df11 --- /dev/null +++ b/packages/complexity-automata/test/rules.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from 'bun:test'; +import { + formatRule, + parseRule, + ruleNumberToRuleSet, + rulesEqual, + ruleSetToRuleNumber, + RULE_SPACE, +} from '../src/rules.js'; + +describe('rule parsing', () => { + test('reads B/S notation', () => { + expect(parseRule('B3/S23')).toEqual({ birth: [3], survive: [2, 3] }); + }); + + test('reads S/B notation in either order', () => { + expect(parseRule('S23/B3')).toEqual({ birth: [3], survive: [2, 3] }); + }); + + test('reads bare birth/survive digits', () => { + expect(parseRule('3/23')).toEqual({ birth: [3], survive: [2, 3] }); + }); + + test('reads names', () => { + expect(formatRule(parseRule('highlife'))).toBe('B36/S23'); + }); + + test('handles empty survive sets', () => { + expect(parseRule('B2/S')).toEqual({ birth: [2], survive: [] }); + }); + + test('drops out-of-range and duplicate counts', () => { + expect(parseRule({ birth: [3, 3, 9, -1], survive: [2] })).toEqual({ birth: [3], survive: [2] }); + }); +}); + +describe('rule numbers', () => { + test('round-trip every rule number', () => { + for (const n of [0, 1, 6152, 12345, RULE_SPACE - 1]) { + expect(ruleSetToRuleNumber(ruleNumberToRuleSet(n))).toBe(n); + } + }); + + test('Conway is bit 3 plus bits 11 and 12', () => { + expect(ruleSetToRuleNumber(parseRule('B3/S23'))).toBe((1 << 3) | (1 << 11) | (1 << 12)); + }); + + test('wraps out-of-range numbers into the rule space', () => { + expect(ruleNumberToRuleSet(RULE_SPACE + 8)).toEqual(ruleNumberToRuleSet(8)); + }); + + test('compares rules across representations', () => { + expect(rulesEqual('B3/S23', 6152)).toBe(true); + expect(rulesEqual('B3/S23', 'B36/S23')).toBe(false); + }); +}); diff --git a/packages/complexity-automata/test/universe.test.ts b/packages/complexity-automata/test/universe.test.ts new file mode 100644 index 0000000..1c27034 --- /dev/null +++ b/packages/complexity-automata/test/universe.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, test } from 'bun:test'; +import { Automata } from '../src/automata.js'; +import { + decodeRLE, + encodeRLE, + fromShareString, + loadUniverse, + memoryStorage, + parseUniverse, + saveUniverse, + toShareString, + UniverseLibrary, +} from '../src/universe.js'; + +const glider = (): Automata => new Automata({ width: 20, height: 20, wrap: false }).stamp('Glider', 3, 3); + +describe('RLE', () => { + test('round-trips a pattern', () => { + const life = glider(); + const restored = decodeRLE(encodeRLE(life), new Automata({ width: 20, height: 20, wrap: false })); + expect(restored.toString()).toBe(life.toString()); + }); + + test('encodes runs rather than single cells', () => { + const row = new Automata({ width: 30, height: 1, wrap: false }); + for (let x = 0; x < 10; x++) row.set(x, 0, true); + expect(encodeRLE(row)).toBe('10o!'); + }); + + test('reads RLE with a header and comments', () => { + const text = '#N Glider\nx = 3, y = 3, rule = B3/S23\nbob$2bo$3o!'; + const life = decodeRLE(text, new Automata({ width: 10, height: 10, wrap: false })); + expect(life.population).toBe(5); + }); +}); + +describe('save and load', () => { + test('a saved universe restores its cells, rule and generation', () => { + const life = glider(); + life.run(3); + const file = saveUniverse(life, { name: 'test glider' }); + const restored = loadUniverse(file); + expect(restored.toString()).toBe(life.toString()); + expect(restored.ruleString).toBe(life.ruleString); + expect(restored.generation).toBe(life.generation); + expect(file.name).toBe('test glider'); + }); + + test('restoring onto a bigger grid centres the pattern', () => { + const life = glider(); + const restored = loadUniverse(saveUniverse(life), { width: 60, height: 60 }); + expect(restored.population).toBe(life.population); + expect(restored.width).toBe(60); + }); + + test('share strings round-trip', () => { + const life = glider(); + const share = toShareString(life, { name: 'shared' }); + expect(share).not.toMatch(/[+/=]/); + const file = fromShareString(share); + expect(file.name).toBe('shared'); + expect(loadUniverse(file).toString()).toBe(life.toString()); + }); + + test('parseUniverse accepts JSON, share strings and bare RLE', () => { + const life = glider(); + expect(parseUniverse(JSON.stringify(saveUniverse(life))).rule).toBe('B3/S23'); + expect(parseUniverse(toShareString(life)).rule).toBe('B3/S23'); + const bare = parseUniverse('#N Glider\nx = 3, y = 3, rule = B3/S23\nbob$2bo$3o!'); + expect(bare.name).toBe('Glider'); + expect(loadUniverse(bare).population).toBe(5); + }); + + test('rejects JSON that is not a universe', () => { + expect(() => parseUniverse('{"format":"something-else"}')).toThrow(); + }); +}); + +describe('UniverseLibrary', () => { + test('saves, lists, replaces and removes', () => { + const library = new UniverseLibrary(memoryStorage()); + library.save(glider(), { name: 'one' }); + library.save(glider(), { name: 'two' }); + expect(library.list().map((u) => u.name)).toEqual(['two', 'one']); + + library.save(glider().run(2), { name: 'one' }); + expect(library.list().length).toBe(2); + expect(library.list()[0]!.name).toBe('one'); + expect(library.get('ONE')?.generation).toBe(2); + + expect(library.remove('one')).toBe(true); + expect(library.remove('one')).toBe(false); + expect(library.list().map((u) => u.name)).toEqual(['two']); + + library.clear(); + expect(library.list()).toEqual([]); + }); + + test('survives corrupted storage', () => { + const storage = memoryStorage(); + storage.setItem('complexity-automata:universes', 'not json'); + expect(new UniverseLibrary(storage).list()).toEqual([]); + }); +}); diff --git a/packages/complexity-automata/tsconfig.json b/packages/complexity-automata/tsconfig.json new file mode 100644 index 0000000..0761544 --- /dev/null +++ b/packages/complexity-automata/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2021", + "lib": ["ES2021", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "verbatimModuleSyntax": true, + "isolatedModules": true + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "test"] +} diff --git a/postcss.config.cjs b/postcss.config.cjs deleted file mode 100644 index 33ad091..0000000 --- a/postcss.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -} diff --git a/src/app.css b/src/app.css deleted file mode 100644 index 44f5a87..0000000 --- a/src/app.css +++ /dev/null @@ -1,12 +0,0 @@ -/* Write your global styles here, in PostCSS syntax */ -@tailwind base; -@tailwind components; -@tailwind utilities; - -body { - font-family: 'Roboto', sans-serif; - width: 100%; - height: 100%; - margin: 0; - padding: 0; -} \ No newline at end of file diff --git a/src/app.html b/src/app.html deleted file mode 100644 index e3887bb..0000000 --- a/src/app.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - %sveltekit.head% - Cellular Automata - - - -
%sveltekit.body%
- - diff --git a/src/lib/GameOfLife/GameOfLife.svelte b/src/lib/GameOfLife/GameOfLife.svelte deleted file mode 100644 index 90542cd..0000000 --- a/src/lib/GameOfLife/GameOfLife.svelte +++ /dev/null @@ -1,612 +0,0 @@ - - -
-
- -
- - {#if !isBottomBarMinimized} -
- -
-
- - - - - - {generation} - - - -
- -
-
- R{ruleNumber || ""} B -
- {#each Array(9) as _, i} - - {/each} -
-
- -
- S -
- {#each Array(9) as _, i} - - {/each} -
-
-
-
-
- {:else} - - {/if} -
- - diff --git a/src/lib/GameOfLife/patterns.js b/src/lib/GameOfLife/patterns.js deleted file mode 100644 index 8ff28e5..0000000 --- a/src/lib/GameOfLife/patterns.js +++ /dev/null @@ -1,44 +0,0 @@ -// Many more Patterns at https://conwaylife.com/book/0e0p - -export const patterns = [ - { name: "Point On/Off" }, - { name: "Glider", array: [[0,2],[1,0],[1,2],[2,1],[2,2]] }, - { name: "Spaceship", array: [[1,0],[4,0],[0,1],[0,2],[4,2],[0,3],[1,3],[2,3],[3,3]] }, - { name: "Pulsar", array: [[0,0],[2,0],[4,0],[0,1],[4,1],[0,2],[4,2],[0,3],[4,3],[0,4],[2,4],[4,4]] }, - { name: "Glider Gun", array: [ - [25,0],[22,1],[23,1],[24,1],[25,1],[30,1], - [13,2],[21,2],[22,2],[23,2],[24,2],[30,2],[12,3],[14,3],[21,3],[24,3],[34,3],[35,3], - [11,4],[15,4],[16,4],[21,4],[22,4],[23,4],[24,4],[34,4],[35,4], - [0,5],[1,5],[11,5],[15,5],[16,5],[22,5],[23,5],[24,5],[25,5],[0,6], - [1,6],[11,6],[15,6],[16,6],[25,6],[12,7],[14,7],[13,8] - ]}, - { name: "Goose", array: [ - [0,0],[0,1],[0,2],[1,0],[1,10],[1,11],[2,1],[2,8],[2,9],[2,10], - [2,12],[3,3],[3,4],[3,7],[3,8],[4,4],[5,8],[6,4],[6,5],[6,9],[7,3],[7,5], - [7,7],[7,8],[8,3],[8,5],[8,8],[8,10],[8,11],[9,2],[9,7],[9,8],[10,2],[10,3],[11,2],[11,3] - ]}, - { name: "Bar", array: [[0,0],[1,0],[2,0],[3,0],[4,0],[5,0],[6,0],[7,0],[8,0],[9,0]] }, - { name: "R-pentomino", array: [[0,1],[1,0],[1,1],[1,2],[2,0]] }, - { name: "Beehive", array: [[0,0],[0,6],[1,0],[1,6],[2,1],[2,5],[3,2],[3,3],[3,4]] }, - { name: "Tumbler", array: [ - [0,1],[0,2],[1,0],[2,1],[2,3],[2,4],[3,2],[3,4],[5,2], - [5,4],[6,1],[6,3],[6,4],[7,0],[8,1],[8,2] - ]}, - { name: "Octagon", array: [ - [0,6],[0,7],[1,5],[1,8],[2,4],[2,9],[3,3],[3,10],[4,3], - [4,10],[5,4],[5,9],[6,5],[6,8],[7,6],[7,7] - ]}, - { name: "Pinwheel", array: [ - [0,4],[0,5],[1,4],[1,5],[3,4],[3,5],[3,6],[3,7],[4,3],[4,8], - [4,10],[4,11],[5,3],[5,7],[5,8],[5,10],[5,11],[6,0],[6,1],[6,3],[6,5], - [6,8],[7,0],[7,1],[7,3],[7,6],[7,8],[8,4],[8,5],[8,6],[8,7],[10,6],[10,7],[11,6],[11,7] - ]}, - { name: "Unix", array: [ - [0,4],[0,5],[1,0],[1,1],[1,3],[2,0],[2,1],[2,4],[2,7],[3,5],[3,7], - [4,6],[6,5],[6,6],[7,5],[7,6] - ]}, - { name: "Lightsaber", array: [ - [1,0],[1,1],[2,0],[2,2],[3,1],[3,2],[4,3],[4,4],[5,3], - [6,4],[6,6],[8,6],[8,8],[9,9],[10,8],[10,9] - ]} - ]; \ No newline at end of file diff --git a/src/routes/+layout.js b/src/routes/+layout.js deleted file mode 100644 index 189f71e..0000000 --- a/src/routes/+layout.js +++ /dev/null @@ -1 +0,0 @@ -export const prerender = true; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte deleted file mode 100644 index 0a5459b..0000000 --- a/src/routes/+page.svelte +++ /dev/null @@ -1,6 +0,0 @@ - - - \ No newline at end of file diff --git a/svelte.config.js b/svelte.config.js deleted file mode 100644 index e9b840d..0000000 --- a/svelte.config.js +++ /dev/null @@ -1,27 +0,0 @@ -import adapter from "@sveltejs/adapter-static"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - // Consult https://kit.svelte.dev/docs/integrations#preprocessors - // for more information about preprocessors - preprocess: vitePreprocess(), - kit: { - adapter: adapter({ - config: 'wrangler.toml', - platformProxy: { - persist: './static' - } - }), - alias: { - $components: "src/lib/components", - $configs: "src/lib/configs", - $constants: "src/lib/constants", - $stores: "src/lib/stores", - $utils: "src/lib/utils", - $validations: "src/lib/validations", - } - } -}; - -export default config; diff --git a/tailwind.config.cjs b/tailwind.config.cjs deleted file mode 100644 index 0f5280a..0000000 --- a/tailwind.config.cjs +++ /dev/null @@ -1,11 +0,0 @@ -const config = { - content: ['./src/**/*.{html,js,svelte,ts}'], - - theme: { - extend: {} - }, - - plugins: [] -}; - -module.exports = config; diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 527d087..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "strict": false, - "allowUnreachableCode": false, - "noImplicitAny": true, - "noImplicitOverride": true, - "noImplicitReturns": true, - "noImplicitThis": true, - "noFallthroughCasesInSwitch": true, - "noUncheckedIndexedAccess": true, - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "lib": ["es2020", "DOM"], - "moduleResolution": "node", - "module": "es2020", - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "target": "es2020" - } -} diff --git a/wrangler.toml b/wrangler.toml deleted file mode 100644 index 5005a5f..0000000 --- a/wrangler.toml +++ /dev/null @@ -1,6 +0,0 @@ -name = "automata-game-of-life" -compatibility_date = "2024-02-08" -compatibility_flags = ["nodejs_compat"] -main = ".svelte-kit/cloudflare/_worker.js" -site.bucket = "./.cloudflare/public" -