jevsnes.git / README.md
README.mdpreviewREADME.mdsource251 lines · 13.0 KB · raw
1# jevsnes: a guide in thirty chapters
2
3**Jev plays Zelda.** Or, to be exact about it: a bot plays *The Legend of
4Zelda: A Link to the Past*, and every time it has to decide where to go
5next, it asks Jev.
6
7Jev is TypeSafe AI's "System One" model. It does not write, and you cannot
8ask it to explain itself. You hand it a question whose answers are already
9written down (yes or no; one of these six; somewhere on this scale), and it
10tells you how likely each answer is, with probabilities you can trust. The
11three kinds of question are called `Noul`, `Choice` and `Score`. A Link to
12the Past turns out to be full of `Choice`s: the chest, the north door, or
13the pot by the wall?
14
15This repository is the whole apparatus: a Super Nintendo, the game running
16on it, a bot that plays the game, the place where Jev is asked, a window to
17watch it all in, and a recorder that can rewind any of it.
18
19> **Try it.** There is no live site for this one; the game runs in a window
20> on a desktop. But the thinking parts are tested without a window, a ROM or
21> a network. After the setup below:
22>
23>     nix develop -c buck2 test //packages/alttp:test //packages/zbanks:test
24>
25> runs the tests that read Hyrule out of work RAM and the ones that decide
26> when the bot has stopped playing.
27
28## The idea in three sentences
29
301. **The SNES is a library, not a program beside us.** The emulator core
31   ([jgenesis](https://github.com/jsgroth/jgenesis)' `snes-core`) is linked
32   into the same process, so the game's RAM, its picture and its controller
33   are ordinary Rust values we can read and write between frames
34   (chapter 2).
352. **Somebody else's bot plays.** [zbanks/alttp](https://github.com/zbanks/alttp)
36   is a C bot from 2020 that reads SNES memory and, in its author's words,
37   "clears Eastern Palace" in a Randomizer seed. We compile its own sources,
38   carry nineteen recorded patches to it, and host it the way its patched
39   Snes9x did (chapters 4, 17 and 18).
403. **Jev makes the bot's one real decision.** The bot picks its next goal by
41   lowest score. A carried patch lets the host make that pick instead, and
42   `packages/decisions` turns the offered goals into sentences and asks Jev
43   one `Choice` (chapter 5). With Jev off, the bot is upstream's, byte for
44   byte.
45
46> **Aside: why not let Jev drive the controller?** That was the first
47> attempt. A Rust "brain" asked Jev which way to walk, and it did not get
48> far: once Link stood still for four minutes while Jev gave the right
49> answer over and over, and once a soldier nearby made it ask seventeen
50> questions in seven seconds and trip the rate limiter (both are kept in
51> chapter 22). On 2026-09-21 it was deleted in favour of a bot that already
52> knew how to walk, and Jev moved up a level, to the question a bot is
53> actually bad at: *what is worth doing next?*
54
55How far that gets, measured headless on 2026-09-21 (run `j10` in
56[research/zbanks-alttp.md](research/zbanks-alttp.md), "How far it plays"):
57the sword by frame 2,719, the bow from the Eastern Palace's big chest, the
58Armos Knights beaten, the Pendant of Courage, and Sahasrahla's Pegasus Boots
59by frame 62,499. Jev was asked 70 questions on the way, for $0.0017 in all.
60
61## The whole story in one picture
62
63What happens in one emulated frame of the window, about sixty times a
64second:
65
66```mermaid
67sequenceDiagram
68  participant W as Window (apps/native)
69  participant B as Bot host (packages/zbanks)
70  participant C as C bot (third-party/c)
71  participant D as Decisions (packages/decisions)
72  participant J as Jev
73  participant S as SNES (packages/console)
74  W->>B: tick(your pad)
75  B->>C: ap_tick(frame, pad)
76  opt the bot is choosing its next goal
77    C->>B: the goals within reach, cheapest first
78    B->>D: which one?
79    D->>J: one Choice, the goals as sentences
80    J-->>D: a probability for each
81    D-->>C: the pick (or upstream's own, if Jev is off or out)
82  end
83  C-->>B: the pad to hold
84  B-->>W: the pad
85  W->>S: run_frame()
86  S-->>W: picture, sound, work RAM
87  W->>W: panels, replay recording, MCP answers
88```
89
90In words: the window hands the bot the human's pad; the bot thinks and hands
91back a pad of its own; the console runs one frame with it; and everything we
92want to watch (what the bot says, what Jev was asked, the game state in
93words) is read out of the same machine and drawn *beside* the picture,
94never over it.
95
96## How to read this
97
98Every folder in this repository is a chapter, and each ends with a link to
99the next. Read them in order and you will know how all of it works; jump in
100anywhere and the chapter says what it is and what is beside it. Each starts
101with the idea, in plain words, and ends with what a maintainer needs (files,
102invariants, commands). The `CLAUDE.md` beside each README is what an AI agent
103working there is told on top of the chapter.
104
105| Chapter | Folder | What you learn |
106| --- | --- | --- |
107| 1 | [packages/](packages/) | The libraries, and the line between the portable ones and the rest. |
108| 2 | [packages/console/](packages/console/) | A Super Nintendo you can keep in a variable, and copy. |
109| 3 | [packages/alttp/](packages/alttp/) | Reading Hyrule out of 128 KiB of work RAM. |
110| 4 | [packages/zbanks/](packages/zbanks/) | Hosting a C bot: its memory, its pad, its stalls. |
111| 5 | [packages/decisions/](packages/decisions/) | The one place Jev is asked anything, and what happens when it is not. |
112| 6 | [packages/replay/](packages/replay/) | A VCR for a machine: record, seek, branch. |
113| 7 | [packages/panels/](packages/panels/) | Everything drawn beside the picture. |
114| 8 | [packages/mcp/](packages/mcp/) | A window that answers an agent's questions. |
115| 9 | [apps/](apps/) | The programs, and which libraries each one is. |
116| 10 | [apps/headless/](apps/headless/) | Does the game even boot? |
117| 11 | [apps/zbanks/](apps/zbanks/) | The bot playing with the lights off. |
118| 12 | [apps/native/](apps/native/) | The window. |
119| 13 | [apps/jevprobe/](apps/jevprobe/) | One question to Jev, out loud. |
120| 14 | [apps/replay-probe/](apps/replay-probe/) | Proving the same thing happens twice. |
121| 15 | [apps/hotdemo/](apps/hotdemo/) | A program that changes its mind while running. |
122| 16 | [third-party/](third-party/) | Other people's code, and how it is kept honest. |
123| 17 | [third-party/c/](third-party/c/) | The bot's own source, built as upstream built it. |
124| 18 | [third-party/c/patches/](third-party/c/patches/) | Every change made to the bot, and why each one had to be. |
125| 19 | [third-party/rust/](third-party/rust/) | How Rust crates reach the build. |
126| 20 | [third-party/rust/fixups/](third-party/rust/fixups/) | The answers the crate converter cannot work out. |
127| 21 | [third-party/rust/top/](third-party/rust/top/) | An empty file with a job. |
128| 22 | [research/](research/) | The source-cited notes the code was built from. |
129| 23 | [research/fixtures/](research/fixtures/) | Evidence kept in a jar. |
130| 24 | [tools/](tools/) | Programs about the build, not the game. |
131| 25 | [tools/mcp/](tools/mcp/) | How a client reaches the window in milliseconds. |
132| 26 | [tools/watch/](tools/watch/) | One line of status, for a watchdog. |
133| 27 | [tools/hotpatch/](tools/hotpatch/) | Editing a running program without restarting it. |
134| 28 | [nix/](nix/) | The Mesen launcher. |
135| 29 | [toolchains/](toolchains/) | Which compiler, and why always `-O3`. |
136| 30 | [platforms/](platforms/) | Where build actions run, and an optional cache. |
137
138## If you know web frameworks
139
140None of this is a website, but most of its parts have a counterpart you
141already know.
142
143| Here | What it is | In web terms |
144| --- | --- | --- |
145| Rust crate | One library or program, with its own dependencies. | An npm package. |
146| buck2 | The build system: every crate, the C bot and the generated third-party graph are targets in one graph, built and cached by content. | Nx or Turborepo, but it also compiles the C. |
147| `BUCK` file | A folder's build targets. | A `project.json`, or the build half of a `package.json`. |
148| reindeer | Turns a `Cargo.toml` of third-party crates into buck2 targets. | Nothing quite; imagine generating your bundler config from `package-lock.json`. |
149| Nix devshell | Every tool at an exact version, from `nix develop`. | `package.json` plus a Node version manager, for every tool and not only Node. |
150| egui | An immediate-mode UI: the whole window is redrawn from the app's state every frame. | A React render function, called sixty times a second, with no virtual DOM. |
151| A snapshot | The entire machine, serialised: restore it and you are back on that frame. | The Redux store, if it held the CPU too. |
152| MCP server | Tools an AI agent can call on the running window: read RAM, take a picture, press buttons. | A dev-tools API, like the one React DevTools talks to. |
153| The C bot | Native code linked into the program. | A node-gyp native addon. |
154
155## Running it yourself
156
157The repository is served over git by the lmjtfy site, with the jevcrates
158submodule it uses:
159
160    git clone --recurse-submodules https://lmjtfy.fun/jevsnes.git
161    cd jevsnes
162    nix develop          # or let direnv do it: .envrc says `use flake`
163
164Everything the build needs is in the devshell ([flake.nix](flake.nix) says
165what each tool is for); nothing is assumed from the host.
166
167> **Aside: one input is private.** The devshell's `buck2` comes from the
168> owner's private package set (`nix-pkgs`, fetched over SSH), because the
169> buck2 in nixpkgs drops its forkserver under test load (fixed upstream and
170> released 2026-09-15). Without access to that repository, `nix develop`
171> stops at fetching it. Every other input is public.
172
173### The cartridge
174
175ROMs are not in git and never will be. Dump your own cartridge (any SNES
176cartridge dumper makes a headerless `.sfc`) and put it in `roms/`, which is
177gitignored. The window, the headless bot and the probes all target the USA
178release, the one the community disassembly and RAM maps describe:
179
180| SHA-1 | File |
181| --- | --- |
182| `6d4f10a8b10e10dbe624cb23cf03b88bb8252973` | `roms/Legend of Zelda, The - A Link to the Past (USA).sfc` |
183| `7c073a222569b9b8e8ca5fcb5dfec3b5e31da895` | `roms/Legend of Zelda, The - A Link to the Past (Europe).sfc` |
184
185`sha1sum` your dump against the table before blaming anything else.
186
187### Building and running
188
189    nix develop -c buck2 build //...
190    nix develop -c buck2 test //...
191    nix develop -c buck2 run //apps/headless:headless -- \
192      "roms/Legend of Zelda, The - A Link to the Past (USA).sfc" 1500 run/out.ppm
193
194The first build compiles a few hundred crates, a C bot and a cycle-accurate
195SNES, all at `-O3` (chapter 29 says why there is no debug build). The window
196itself is chapter 12.
197
198### The Jev key
199
200Jev needs a TypeSafe API key in `TYPESAFE_API_KEY`, in the environment of the
201one process that asks. The owner's machine puts it there with `op-env-run`, a
202host wrapper (not part of this repository) that puts it in one child
203process's environment and nowhere else:
204
205    op-env-run -- nix develop -c buck2 run //apps/jevprobe:jevprobe
206
207Anywhere else, get a key from TypeSafe (<https://docs.typesafe.ai>) and give
208it to that one command's environment the same way, rather than exporting it
209into your shell.
210
211Without a key, everything plays exactly the same; Jev is simply off, and the
212window's Jev panel says so.
213
214### Mesen, the second opinion
215
216The devshell also carries [Mesen 2](https://github.com/SourMesen/Mesen2), a
217known-good emulator to check our core against: the same ROM, the same RAM
218addresses, a picture to compare. Jev never plays in it. `Mesen` on the
219devshell's `PATH` is a wrapper (chapter 28).
220
221    Mesen "roms/Legend of Zelda, The - A Link to the Past (USA).sfc"
222    Mesen --testRunner --enableStdout --timeout=60 script.lua "roms/….sfc"
223
224The second form is headless, driven by a Lua script, and exits with the code
225the script passes to `emu.stop`. [research/mesen-automation.md](research/mesen-automation.md)
226is the manual.
227
228## What gets no chapter
229
230Some folders are not part of the story: `buck-out/` and `target/` (build
231output), `run/` (pids, logs, captured frames, recordings: everything a run
232writes), `roms/` (your cartridge and its saves) and `.claude/` (an agent's
233local settings). All of them are gitignored or untracked, so a clone does not
234have them until something makes them. Upstream code vendored under
235`third-party/` gets no chapters of ours either; chapter 16 explains why.
236
237## Files at the top
238
239| File | What |
240| --- | --- |
241| [flake.nix](flake.nix) | The devshell: Rust from fenix (with the wasm32 target), buck2, reindeer, clang and lld, Mesen, ffmpeg, gdb, and the two variables that reach the GPU under WSL. |
242| [flake.lock](flake.lock) | The exact revision of every Nix input. |
243| [.buckconfig](.buckconfig) | buck2's cells, the bundled prelude, the execution platform and the thread count. |
244| [.buckroot](.buckroot) | Empty; marks the buck2 project root. |
245| [.envrc](.envrc) | direnv: `use flake`. |
246| [.gitmodules](.gitmodules) | The jevcrates submodule, by a relative URL. |
247| [.gitignore](.gitignore) | Build output, `run/`, ROMs and saves, snapshots. |
248| [.mcp.json](.mcp.json) | Tells an MCP client (Claude Code) to start `tools/mcp/launch.sh` (chapter 25). |
249| [CLAUDE.md](CLAUDE.md) | What an agent working anywhere here must not break. |
250
251Next: [Chapter 1, packages/](packages/) →