# FlyRace A browser time-trial racer: three circuits, forgiving drifting, a chase camera, saved records, a ghost of your best lap, and an experimental fly lab. ## Play FlyRace 1.0 runs in a recent desktop or mobile browser with WebGL. No hosted copy is published yet (a GitHub Pages workflow is ready in `.github/workflows/pages.yml`); until then, run it locally as below. Pick a circuit and **Time trial**, wait for the 3-2-1, then drive three laps against the clock. WASD or the arrow keys drive, T respawns you at the last gate if you get stuck, R restarts, P pauses. Touch screens get on-screen steering and pedals, and a gamepad works too. Your best lap and 3-lap total on each circuit are saved in the browser, and your best lap comes back as a see-through ghost to race. **How to play** in the menu has the rest. **About the fly.** FlyRace set out to race against a network wired from the measured connectome of a fruit fly (MaleCNS) and trained to drive. That research stopped before any training ran: a pre-registered test, gate K1, found that none of ten camera and sensor setups let even an ordinary image network predict hard braking well enough on unseen courses. The [negative result](reports/2026-09-24/k1-final/README.md) names the gate, every score and the budget used (0 of 3 training runs). So FlyRace 1.0 has no fly opponents, and none is planned. The **Fly brain** lab panel stays as an untrained, experimental exhibit that works only with the optional local brain service below; the game plays fully without it. The [changelog](CHANGELOG.md) lists what is in 1.0. ## Play locally Node 26.9 or later in the 26 line is required (`.nvmrc` pins 26.9.0; `nvm use` picks it up). ```sh npm ci npm run dev ``` Open http://127.0.0.1:5188. WASD/arrows drive, R restarts the run, T respawns, P pauses, F toggles fullscreen, M mutes. On a touch screen, on-screen steer, gas and brake zones appear while driving. A standard gamepad also works: left stick steers, right trigger accelerates, left trigger brakes, Start pauses, Y (the top face button) restarts and X (the left face button) respawns. Sound is generated in the browser; there are no audio files. Steering and braking can induce recoverable slip. A short **How to play** overlay opens on your first visit and can be reopened from the menu. **Settings** (in the menu, or the HUD button, which pauses the run first): sound on/off and volume, reduce motion (keeps the chase camera's zoom and distance steady at every speed; on by default when the system asks for reduced motion), km/h or mph, key hints, show ghost, graphics quality (low, medium or high; picked for your device on first visit), and touch controls Auto/On/Off. Each is saved in this browser. **Credits** lists the MaleCNS, three.js and FlyRace licenses. Menus, dialogs and results work from the keyboard, and a screen reader hears the countdown, each lap, records and the result. Pick a circuit and a mode at startup, or use **Change circuit** while playing; choosing a course starts a fresh run. There is no reverse gear. If the car sits nearly stopped against a wall for 2 seconds, a **Respawn** prompt appears; T (or the button) puts the car at rest on the centreline of the last gate it passed, facing down the track. The clock keeps running, so the time lost is the penalty. Your best lap and best 3-lap total on each circuit are saved in this browser for human-driven time trials only (never free practice) and shown on the circuit cards; **Clear saved times** in the menu removes them. Records are written when a run ends: a finished run saves its best lap and total, a run you leave early (restart, menu, closing the page) saves its best lap, and a run where fly control drove at any point saves nothing. - **Time trial** — a 3-2-1 countdown (the car is held still), then three laps against the clock. The results screen shows the total, each lap and the best lap, with **Retry** and **Change circuit**. R restarts the trial; your session best lap on that circuit is kept until you reload the page (laps completed after fly control drove never count). - **Free practice** — lap timing with no countdown and no finish. - **Juniper Circuit** — 603 m, flowing sweepers and an infield section. - **Cinder Canyon** — 861 m, longer straights and a deep hairpin. - **Tidal Run** — 788 m, linked bends and quick direction changes. The course maps show your position. There are no opponents: time trials are solo. ## Host a static build `npm run build` writes a static site to `dist/`. It is served from the domain root by default. To host it under a sub-path, set the base at build time: ```sh FLYRACE_BASE=/flyrace/ npm run build npx vite preview --base /flyrace/ --host 127.0.0.1 --port 4173 --strictPort # serves http://127.0.0.1:4173/flyrace/ ``` The build lists bundled third-party licenses in `dist/third-party-licenses.md` and ships the fly research write-up, which the game links to, as `dist/fly-research.html` (a copy of the report's Markdown). A hosted copy plays without the fly: the optional brain service below only listens on loopback and only accepts the local dev origin, and the game connects to it only when you press **Connect local brain**. ## Connect the fly Python 3.12, `uv`, and an Apple GPU are required for the current neural service. The data is already prepared on the development machine; on a fresh checkout: ```sh uv sync --python 3.12 uv run flyrace-data fetch uv run flyrace-data prepare ``` The official source download is about 1.1 GB. Source hashes and the retained graph census are checked. Start the service in a second terminal: ```sh uv run flyrace-serve --allow-untrained ``` Open **Fly brain → Connect local brain** in the game. The preview shows the exact 64×32 grayscale view supplied to the neural network. **Try untrained fly control** switches the player car to that network for diagnostics. No conventional driver takes over when the brain stalls: it brakes and pauses. The network is untrained, and it stays that way in 1.0: the attempt to train it ended with the [K1 negative result](reports/2026-09-24/k1-final/README.md). It barely moves, may hit walls, and runs it drives never save records or ghosts. The development target was an M4 Pro Mac with 24 GB of memory. The service binds to loopback port 8765 and accepts the local game origin on port 5188. The current implementation uses 165,122 retained neurons and 25,563,197 measured directed pairs. This declared Traced subset excludes some annotated cells, including 1,984 photoreceptors. It is an engineered unsigned rate network; it does not reproduce measured fly dynamics or neurotransmitter signs. ## Research dashboard A local developer tool, not part of the game: with `npm run dev` running, open http://127.0.0.1:5188/dashboard.html (or run `npm run dashboard`, which starts the same server and opens it). It shows milestone and kill-criterion status, the training budget, the R2 results and candidate regrades, follows training and probe runs as they log progress, and replays recorded offline evaluation rollouts on a track map, in the game's chase camera and, when recorded, through the fly's 64×32 eye. It only reads files, through a dev-server-only API, and is not in the production build. See [docs/dashboard.md](docs/dashboard.md). ## Check the implementation The browser checks use Playwright's Chromium. Install it once after `npm ci`: ```sh npx playwright install chromium ``` ```sh npm test npm run build npm run test:e2e # circuits, foundation, recovery, training interface, time trial, records/respawn, input and audio, settings and accessibility npm run test:time-trial # 3-lap trials driven by a test-only controller (also in test:e2e) npm run test:records-respawn # saved records survive a reload; respawn after a wall hit (also in test:e2e) npm run test:input-audio # touch zones, mocked gamepad and generated audio (also in test:e2e) npm run test:settings-a11y # settings, How to play, credits, keyboard use and an axe-core scan (also in test:e2e) npm run test:sensor-golden # M4 Pro / Metal only uv run pytest -q # With both servers running; the integration script requests Apple Metal: node scripts/test-browser.mjs ``` GPU tests skip when MPS is unavailable; a skip is not a GPU pass. See the [measured results](reports/2026-09-17/README.md) and [implemented laboratory contract](docs/lab-v1.md). Offline experimental checkpoints now exist, but none is qualified for play and no learned lap has been completed. `npm test` includes physics and track-geometry parity goldens; the sensor golden compares exact fly-camera frames and runs only on the M4 Pro with Metal. They hash fixed trajectories, geometry and frames, so they catch product edits that change those samples under an unchanged version, not every change that could affect recorded data or checkpoints; [parity.md](docs/parity.md) explains how to update them with a declared version bump. The circuit, foundation, recovery and training-interface browser tests (`npm run test:e2e`) need the web server and use fixture transport; they do not need or benchmark the neural service. The training-interface test requires Chromium's Metal renderer and opens a visible window. Every browser script defaults to the dev server at `http://127.0.0.1:5188/`; set `FLYRACE_URL` to test another port or base path, for example `FLYRACE_URL=http://127.0.0.1:5191/?test=1 npm run test:e2e`. `.github/workflows/ci.yml` runs `npm ci`, `npm test`, `npm run build`, `npm run test:e2e:portable` and `uv run pytest -q` on a macOS runner. `npm run test:e2e` is `test:e2e:portable` plus `test:e2e:metal` (the training-interface and lab camera checks, which need Chromium's Metal renderer and a visible window); hosted runners cannot run `test:e2e:metal` or `test:sensor-golden`, so run both by hand on the M4 Pro before a release. `.github/workflows/pages.yml` builds with `FLYRACE_BASE=/flyrace/` and deploys `dist/` to GitHub Pages (https://nedcut.github.io/flyrace/) on every push to main or by hand. Current handling profile `arcade-lab-v4` reports positive steering and slip to the driver's right; positive world yaw turns left. The steering fix landed in v2; v3 changed the slip telemetry sign without changing vehicle motion. v4 makes wall contact forgiving: a counted impact scrubs 12% of the speed once, and sustained contact only rubs (about 26% of the speed per second) instead of scrubbing 12% on every 1/120 s tick, which used to pin the car at under 1 m/s. Race rules `directed-gates-v2` let a gate span the full road width, so a car sliding along a wall still gets credit for the gate. Clean driving is unchanged: the parity reference laps and the whole R1 teacher dataset (no wall contact in 6,865 episodes) replay identically. Earlier performance reports describe the original single-track build. [Fable's foundation review](docs/reviews/2026-09-19/README.md) and the later [Opus 5.5 braking review](docs/reviews/2026-09-22-braking/README.md) are recorded. The deterministic training API has episode resets, strict decision timing, directional checkpoints and verified camera parity. Experimental connectome checkpoints can move in autonomous offline tests, but none brakes and completes laps reliably enough to become an opponent. The [corrected braking evaluation and camera probes](reports/2026-09-23/README.md) show why another fit on the same small curriculum would be premature. The earlier [braking and recovery results](reports/2026-09-22/braking-recovery/README.md) remain historical evidence under their original criteria. - [Negative result: why there is no fly opponent (K1 final)](reports/2026-09-24/k1-final/README.md) - [Research and implementation sequence](RESEARCH.md) - [Proposed brain/game contract](docs/brain-interface.md) - [Procedural lab courses and the R1 dataset](docs/procedural-courses.md) - [Progress](progress.md) Large source data, generated graphs, and checkpoints stay out of Git. Dataset provenance and small benchmark summaries belong in the repository. ## License FlyRace's own code is released under the [MIT License](LICENSE), copyright 2026 Ned Cutler. That license does not cover the MaleCNS data or anything derived from it — prepared graphs, trained gains and checkpoints — which remain under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) and must keep the attribution in [THIRD_PARTY.md](THIRD_PARTY.md). Third-party npm and Python packages keep their own licenses.