# Projects Autopilot
Ideas get rolled at random out of the idea hub at
and built end to end — designed, tested
in a real browser, deployed, and written up.
- **Overview page:**
- **Idea hub data:** ` (the root page
reads this; it is the list the dice are rolled against)
## Layout
```
projects-autopilot/
index.html overview page -> /projects/autopilot/
thumbs/ screenshots for the overview (webp)
deploy.mjs FTPS deploy for the overview + every project
/ one project per folder -> /projects//
_tools/ test + screenshot harness (never deployed)
```
Anything in a top-level folder that has an `index.html` **or a `deploy.json`**
and does not start with `_` is treated as a project and deployed to
`/projects//`. `deploy.json` may say `{"dir":"dist","extra":["server/x.php"],"skip":["test","docs"]}`
for projects with a build step (Expo web exports). `data/` is never uploaded —
it is live server state. Each project folder is its own git repo.
## The loop
1. **Roll.** Pick a random index from the hub. Reject what genuinely cannot be
built from a keyboard (hardware, real multiplayer infrastructure, marketing
stunts) and record which ones and why — the overview page shows the rolls.
2. **Build.** Take the idea at its word. No placeholder art, no "coming soon".
3. **Test for real.** Drive it in Chromium with a scripted bot. Play every
level, profile every frame, fix what that turns up.
4. **Ship.** `node deploy.mjs `, verify the live URL and any endpoint,
then add a section to `index.html` with screenshots.
## Commands
```bash
# serve a project locally (pick a free port — 8123 is often taken)
node _tools/serve.mjs slow-rain 8447
# play the whole game with a bot: level flow, waves, win state, leaderboard
node _tools/fullrun.mjs http://localhost:8447/ # honest run
node _tools/fullrun.mjs http://localhost:8447/ --tank # survive to the end
# per-level bot run with a per-stage frame profile
node _tools/botplay.mjs http://localhost:8447/ --full --seconds 12
# load/console-error sweep with screenshots
node _tools/playtest.mjs http://localhost:8447/ --shots ./out
# curated screenshots for the overview, then shrink them
node _tools/shoot.mjs http://localhost:8447/ ./thumbs
node _tools/optimize.mjs ./thumbs 1440 0.82 # png -> webp, ~6.8 MB -> 0.2 MB
# One Reader (Expo) — phone-sized walkthrough of the web build, incl. live weave
node _tools/serve.mjs one-reader/dist 8451
node _tools/or-walk.mjs http://localhost:8451/ ./out --weave
# deploy
node deploy.mjs # overview + all projects
node deploy.mjs slow-rain # one project
node deploy.mjs --index # overview only
```
Every harness runs **headless by default** so it never takes over the desktop.
`HEADED=1` in front of a command opens a real window — only do that for FPS
profiling (headless Chromium uses software rasterisation, so its frame numbers
are meaningless), and one run at a time.
Credentials come from `../shared-config/secrets.env` (`LIMACITY_FTP_*`) or the
environment. Occasional `curl: (28) FTP response timeout` lines during deploy
are lima-city being slow; `--retry 3` covers it, but check the file listing
afterwards.
## Projects
### 01 · slow-rain — "max payne vibe game" (idea #111)
A four-level top-down film-noir shooter: bullet time, shootdodging, four
weapons, procedurally painted comic-panel cutscenes, fully synthesised audio,
and a leaderboard. ~5,000 lines, 220 KB, zero dependencies, zero asset files.
Live:
Things worth remembering from this build:
- **Bake static lights.** Per-frame silhouette-edge shadow casting cost 5.6 ms
in the biggest level. Level lights and walls are both static, so each light's
shadowed falloff is baked into its own world-space texture and blitted;
flicker rides on `globalAlpha`. 0.3 ms, 24 fps → 60 fps. See
`src/render.js: lightTexture()`.
- **A multiply lightmap cannot brighten anything.** Surfaces have to be painted
at their *fully lit* colour, with a second additive pass on top for
highlights. The first pass at the art was painted at the final brightness and
came out unreadably black.
- **Trace bullets from the shooter's centre.** Spawning them at the muzzle 22 px
ahead meant standing against cover put the spawn point inside the wall and
every round died instantly. Line of sight and line of fire must use the same
origin.
- **Test for unwinnable states.** A bot run found "every weapon empty, hostiles
alive, no way out". There is now a spare-clip fallback.
- **Leaderboard pattern:** `store.php` follows
`../shared-config/headless-json-storage.md` (fixed schema, `X-Store-Key`
gate, deny-all `data/`, atomic write). It accepts one run per POST and does
the merge/sort/cap server-side, so a client can add a row but never wipe the
board. Key is in both `store.php` and `src/cloud.js`; it is
obfuscation-grade by design.
### 02 · one-reader — "autobiographer" (idea #24) — Expo app
An interview-yourself autobiography app: 120 questions in ten chapters,
local-first, reading view, AI "weave" to prose via a rate-limited Gemini proxy,
HTML/Markdown/JSON export and restore. Expo SDK 57 + expo-router + TypeScript.
Live web build: · native
builds via `eas build` (not submitted to any store — needs a human with the
Apple account).
Things worth remembering from this build:
- **Verify Expo apps through the web export.** `npx expo export --platform web`
+ Playwright with `devices['iPhone 13']` drives the identical code at phone
size with real clicks, downloads and file pickers. No emulator needed.
- **Expo + subpath hosting:** set `expo.experiments.baseUrl` *and* ship an
Apache rewrite to `index.html` (`scripts/htaccess.mjs`). Fonts end up in
`dist/assets/node_modules/…` — the deploy walk must not skip that folder.
- **`Alert.alert` is silent on web.** Wrap it (`notify()` in `src/ui.tsx`).
- **One write path for the store.** An erase via `removeItem` raced a debounced
`setItem` and won after a restore. Route everything through one `set()` and
flush on `AppState` background.
- **lima-city drops cold long requests** (502 from the host itself). Retry
once client-side on 502/503/504/network errors.
- **Harness selectors:** RN-web keeps tab screens mounted, so `getByText` often
matches twice; give Buttons `accessibilityRole`/`accessibilityLabel` and use
`getByRole('button', {name})`.
### 10 · squint — "UI AI" (idea #149)
Drop in a screenshot of an interface and it measures it: spacing, padding,
alignment, type scale, WCAG contrast per line, near-identical colours, and a
squint test. Nine layers over the picture, every number in the CSS pixels of
the original interface, everything in the browser.
Live:
Things worth remembering from this build:
- **The most common colour is not the background.** Cover half a screen in
cards and the cards become "the page". Take it from the border ring instead.
- **Text needs a second mask.** The first asks "is this pixel the page?", which
makes a card read as solid and hides every word on it. The second asks "is
this pixel the surface it is sitting on?".
- **Ink-to-ink is not a gap.** One 12 px margin measures 16, 17 or 18 depending
on the descenders in the heading above it. Line boxes have to be rebuilt from
the baseline and the measured leading, or a design built on eights reads as
noise. Take the *smallest* baseline step as the leading — the median is a
paragraph break.
- **A tolerance that cannot fail is not a measurement.** At the ±2 px this
technique is good for, a 3 px grid matches nearly every integer. So the
headline is whether the values repeat, not which unit fits.
- **Two fixtures.** `test/lab.html` has known mistakes planted in it,
`test/grid.html` has none. Half the tuning came from the control.