jevsnes.git / packages / zbanks / README.md
README.mdpreviewREADME.mdsource101 lines · 6.0 KB · raw

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 in ap_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 *joypad is 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::start leaks a 128 KiB buffer (and a copy of the ROM) on purpose, and tick copies 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 = false is 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 $0123 shopkeeper whose gift a talk can never detect.
  • A stalled bot is noticed and nudged. stall decides whether the bot has stopped playing (manual mode, no task, or Link not moving, each for five minutes of play); recovery then 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.jsonl beside the bot's files and read back by the next bot, which otherwise remembers nothing.

Try it. nix develop -c buck2 test //packages/zbanks:test runs 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

PathWhat
src/lib.rsBot: 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.rsDetector: 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.rsRecovery: retries every given-up goal on a stall, with BACKOFF between tries and a CAP of five tries without progress.
src/outcomes.rsOutcomes: each goal's latest outcome by identity (kind|node|screen), appended to goal_outcomes.jsonl and reloaded at start.
shim/host.cThe 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.
BUCKThe 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/ →