Chapter 4: zbanks, a home for somebody else's bot
In 2020, Zach Banks wrote a bot that plays the A Link to the Past Randomizer the way a person would: from what is on screen, read out of SNES memory, never peeking inside a chest before opening it. It is C, written (its own README says) for rapid iteration, and in its author's video it plays for 24 minutes and clears the Eastern Palace (with infinite health, bombs and arrows, which its README owns up to; those cheats are why the bot writes work RAM every frame).
It was written to live inside a patched Snes9x emulator, and Snes9x gave it exactly two things:
base(addr): a pointer to the byte at a SNES address. The bot calls it once per address inap_init, keeps the pointers for ever, and reads and writes the game through them.ap_tick(frame, *joypad): called once per frame, before the frame runs, with the pad the human is holding. Whatever the bot leaves in*joypadis the pad for that frame.
This crate is those two things over chapter 2's Console. It is the
patched Snes9x, rewritten as a Rust host, so that upstream's C (chapter 17,
with the patches of chapter 18) plays our emulator.
sequenceDiagram participant H as Host (Bot::tick) participant M as WRAM mirror participant C as C bot participant S as Console H->>M: copy work RAM in H->>C: ap_tick(frame, human's pad) C->>M: reads and writes through base() C-->>H: the pad to hold H->>S: copy work RAM back out Note over H,S: the caller holds the pad (apply_pad) and runs the frame
Aside: why a mirror? The bot keeps raw pointers for its whole life, so the memory they point at must never move.
Bot::startleaks a 128 KiB buffer (and a copy of the ROM) on purpose, andtickcopies the console's RAM into it, lets the bot think, and copies it back. Rust gets to keep its console movable; the C gets memory that stays put.
One bot per process. The bot's whole state is C globals, so a second
Bot::start panics. That single fact shapes a lot of chapter 12: rewinding
the bot to an earlier frame means starting a new process.
Vanilla is not the Randomizer. The bot was written for a Randomizer seed,
which is built on the Japanese ROM and starts in open mode. We play the USA
cartridge. So the host answers the bot's Japanese ROM addresses from the USA
ROM (JP_TO_US), and rando patches the image the way the Randomizer
differs where the bot can tell (no item-get text boxes, uncle and the
Kakariko informant quiet) and presets a save in open mode. Patching the image
changes its CRC, so every snapshot for the bot is made from the patched image.
The host helps where it can, from outside. Everything here that is not in the C is a host decision, each with its evidence in research/zbanks-alttp.md:
- Goals given up on come back. Upstream gives up a goal after its fourth
failed attempt and never retries it, so a goal tried too early (the
Eastern Palace big chest, before the big key) is lost for good. The host
puts every given-up goal back whenever Link's possessions change
(
POSSESSIONS: items, keys, pendants, crystals, progress; not rupees or health).retry_given_up = falseis upstream's behaviour. - Goals that cannot or need not be done are not made. The shim declines
uncle once the save says he is gone, a one-time NPC whose reward is
already held, and the room
$0123shopkeeper whose gift a talk can never detect. - A stalled bot is noticed and nudged.
stalldecides whether the bot has stopped playing (manual mode, no task, or Link not moving, each for five minutes of play);recoverythen puts every given-up goal back regardless of possessions, with growing backoff, and gives up for good after five tries that make no progress. - Outcomes outlive the process. Each goal that leaves the list, completed
or given up, is appended to
goal_outcomes.jsonlbeside the bot's files and read back by the next bot, which otherwise remembers nothing.
Try it.
nix develop -c buck2 test //packages/zbanks:testruns the stall detector's, the recovery's and the outcome log's tests: pure state machines, no bot started. To watch the real bot play, see chapter 11.
For the people who maintain it
In this folder
| Path | What |
|---|---|
| src/lib.rs | Bot: start (upstream's ap_init against our console), tick (one ap_tick), report (info line, tasks, goals with scores, traps, stray reads), export_map, set_goal_chooser, retry_all_given_up. The GoalChooser trait and the Situation/GoalOption it is handed; the pad word (PAD_BITS, apply_pad); JP_TO_US; the States trait; POSSESSIONS; rando (the ROM PATCHES, patch_rom, the open-mode save PRESET). |
| src/stall.rs | Detector: whether the bot has stopped playing, against STALL_FRAMES (18,000, five minutes). Shared by the window and headless, so there is one definition of "stalled". |
| src/recovery.rs | Recovery: retries every given-up goal on a stall, with BACKOFF between tries and a CAP of five tries without progress. |
| src/outcomes.rs | Outcomes: each goal's latest outcome by identity (kind|node|screen), appended to goal_outcomes.jsonl and reloaded at start. |
| shim/host.c | The C side of the host: a SIGTRAP handler that is assert_bp's gdb continue; fopen/rename sent to the host's directory; readers of the goal and task lists; the goal-choice hook's host side; declining goals the save says are done; the given-up watch and the outcome ring. |
| BUCK | The shim as a cxx_library, the crate, and //packages/zbanks:test. |
Who uses it: apps/native (the window, chapter 12), apps/zbanks
(headless, chapter 11), apps/replay-probe (chapter 14), and
packages/decisions and packages/replay.
← Previous: Chapter 3, alttp/ · Up: packages · Next: Chapter 5, decisions/ →