Mario Learns to Run
A program plays Super Mario Bros. on an NES emulator, thousands of times an hour, and gets a little better each time. This guide explains how, from the whole machine down to the bytes, with pieces you can operate as you read.
One sentence
An agent looks at the game, chooses a button press, gets a small number back that says whether that was a good idea, and adjusts its habits so that good ideas become more likely. Repeat ten million times.
The loop
Everything in this project is one loop, drawn below. Press Step to walk one agent step through it with the real values from the first frame of World 1-1.
Two phases, one plan
Phase 1 (this guide, running now) lets the agent read the game's memory directly: exact positions and speeds, no pixels. That makes learning fast, and it produces a teacher. Phase 2 will train a second agent on screen pixels only, the way a person sees the game, and use the Phase 1 teacher to get it started. Both phases play the same stages in the same order: 1-1, 1-2, 1-3, 1-4, 2-1.
Where things stand
Phase 1 is complete
The second run of the day cleared and passed every stage from 1-1 to 2-1 in 4.7 million steps, 37 minutes on the Mac Mini. A teacher file exists for each stage.
The first run taught us the reward
Run 1 solved 1-1 and then stalled at a pipe in 1-2 for eight million steps. The reason was a flaw in the reward, explained at ground level below; fixing it is what made run 2 fly.
You can watch and tune
A live window and a browser panel show one training environment; every reward weight and learning knob can be changed while it runs, with help on each one.
Eight lessons for a first-timer
Watch the teacher, start a run, break the reward on purpose and see what breaks. Open the lessons.
Terminology
The words used everywhere below. Each is defined the way this project uses it.
- Agent
- The thing that chooses actions. Concretely: a neural network plus the code that samples from it.
- Environment
- The game wrapped in a fixed interface: reset, step, reward, done. Here, one emulator per environment.
- Observation
- What the agent sees at one step. In Phase 1, 30 numbers decoded from RAM (65 with the tile window on).
- Action
- One of 10 button combinations, held for 4 NES frames.
- Agent step
- 4 NES frames, about 1/15 of a second. Every count in this project is in agent steps.
- Reward
- The number the environment hands back after a step. Mostly pixels moved right, minus a little for time, plus event bonuses and penalties.
- Return
- The sum of rewards over an episode, or discounted into the future from a given step.
- Episode
- One life: from a stage's first controllable frame until death, clear, stall, or the step cap. No extra lives.
- Policy
- The mapping from observation to action probabilities. The actor network.
- Value
- A second network's estimate of the return expected from a state. The critic. Used to judge whether an action turned out better than expected.
- Advantage
- How much better an action's outcome was than the value predicted. Positive: do it more. Computed with GAE.
- PPO
- Proximal Policy Optimization. The learning rule: move the policy toward higher-advantage actions, but only a little per update.
- Rollout
- 128 steps from each of 16 environments, collected before one update. 2,048 transitions.
- Entropy
- How spread out the action probabilities are. High: exploring. Near zero: always the same button.
- Curriculum
- The stage ladder and the rules for moving up (promotion) or back (demotion).
- Greedy eval
- Playing with the most likely action every time, no sampling. The exam for promotion.
- Teacher
- The Phase 1 policy saved at the moment a stage was passed. Phase 2 learns from it.
- Latch
- A one-frame detector for an event (death, clear) derived from RAM bytes.
- Snapshot
- The emulator's full state, saved in memory. Every episode starts by restoring the stage's snapshot.
- SPS
- Agent steps per second across all environments. About 1,300 on the 5090, 2,200 on the Mini.
- Checkpoint
- Everything needed to resume a run exactly: both networks, optimizer, normalizer, curriculum pointer, random state.
How a neural network learns
Before the map of the machine, the one idea everything rests on. A network is a big pile of dials, and learning is turning them, a little at a time, in the direction that would have made the last decision better.
A neuron is a weighted vote
Start with one neuron. It has a few inputs, each a number: say, how close the next pit is, and how fast Mario is moving. Each input has a weight, a number that says how much that input matters and in which direction. The neuron multiplies each input by its weight, adds the results up, and pushes the sum through a squashing function so the answer lands between 0 and 1. Read the answer as "how much do I want to jump right now?". A big positive weight on "pit is close" means a nearby pit pushes the answer up. A negative weight would push it down. That is the entire mechanism. There is nothing else inside a neuron.
Two inputs, two weights, one output. Drag the weights and watch the output bar. Then press learn: one gradient step toward "jump", the same move the trainer makes 75,000 dials at a time.
A network is neurons stacked
The Phase 1 policy is 30 inputs feeding 256 neurons, feeding another 256, feeding 10 outputs. Every arrow between two layers is one weight, so this small network has about 75,000 dials (30 × 256, plus 256 × 256, plus 256 × 10, plus one bias per neuron). The first layer's neurons learn to notice simple things, such as "an enemy is close and to the right". The second layer combines those notices into situations. The last layer turns situations into a score for each of the ten buttons, and the scores become probabilities. Nobody designs what each neuron notices. That is what the learning does.
Learning is turning the dials, a little, in a known direction
Suppose the network chose "run right" and the outcome was better than the critic expected (a positive advantage). We want "run right" to be a bit more likely next time in this situation. Calculus can say, for every one of the 75,000 dials, which way to turn it to raise that probability, and how strongly each dial matters. That list of directions is the gradient. The learner turns every dial a small amount along it; the learning rate sets how small. If the outcome was worse than expected, it turns them the other way. Try it above: press learn and watch both weights move toward "jump", the one on the bigger input moving more.
Why the steps are small
One rollout is 2,048 steps of a noisy game. Some of what looked good was luck. If the network took a big step after each rollout it would swing wildly, and it would forget how to play the start of the stage while it learned the end. So the steps are small, the learning rate shrinks over the run, and PPO adds a fence: no action's probability may change by more than about 20% in one update. Slow and steady, ten thousand times.
The pieces you will meet below
- The actor is the network described above: observation in, ten probabilities out.
- The critic is a second network with one output: its guess at how many points are still to come. It exists so the learner can tell a good outcome from a good outcome for this spot.
- The advantage is what happened minus the critic's guess. Its sign decides which way the dials turn; its size decides how far.
- Entropy is how spread out the ten probabilities are. A small bonus for it keeps them from collapsing onto one button before the network has seen enough.
Everything else in this guide is plumbing that gets the right numbers to this one operation, fast, without lying. If you would rather learn by doing, the eight lessons start with watching a finished policy play.
System map
Two machines, a handful of processes, and the files that connect them. The arrows are what actually moves.
The people and agents
This project is built by a small team of AI sessions with one human owner. Eric owns the goal and tunes. The lead (a Claude session) holds the design, reviews every commit against it, dispatches tasks, and starts runs. Opus (another Claude session) writes the trainer code, test-first. Grok writes machine tooling: scripts for the 5090, the recorder, plots, the browser panel, the emulator bridge. Chief relays between Eric's voice line and the lead through files in comms/. A launchd job wakes the lead every ten minutes so the project moves between conversations.
The components
Each card is one Python module or tool with one responsibility. The 100-foot sections below open each one.
emulator.py
Wraps nes-py: one NES core, RAM view, snapshot and restore, the button-order translation, and the pause-button mask. Verifies the ROM's hash before it will boot.
ram.py
The map from memory addresses to meaning: x, y, speeds, timer, score, coins, enemies, player state. The death and clear latches. The tile map reader.
stages.py
Boot recipes that bring the emulator to the first controllable frame of any stage, and pinned hashes so a stage always starts identically.
env.py
The Gymnasium environment: 4 frames per step, busy-frame skipping, death, clear, warp, stuck and cap endings, and the info dictionary with every reward component.
obs.py
The 30-number observation vector (65 with the tile window) and the running normalizer.
reward.py
Pure function of one step's facts: pixels, time, power-ups, enemies, coins, death, clear, flag height, stuck, warp. Anti-farming rules. Live-tunable weights.
ppo.py
Actor and critic networks, the rollout buffer, GAE, and the clipped PPO update.
curriculum.py · eval.py
Promotion after a 5-of-5 greedy exam, demotion on stagnation, the thrash freeze, the 5M gate. Greedy evaluation and eval videos.
train.py · logging.py
The collector, checkpoints that resume bit-for-bit, CSV and TensorBoard logging, teacher export, control-file polling.
live.py · tools/live.py · tools/panel.py
Shared-memory channel from env 0, the native viewer window with overlay and takeover, the browser panel with sliders.
tools/
play.py (watch a checkpoint), plot_run.py, alerts.py (the failure-mode checks), boot_asserts.py, find_clear.py (a scripted 1-1 clear), record_inputs.lua and replay_inputs.py, bridge.py (FCEUX).
scripts/ · ops/ralph/
Mini and 5090 setup, sync, start, watch, pull. The ten-minute orchestration tick and its restart logic.
Emulator and RAM
The game runs inside nes-py, a C++ NES core with a Python interface. It gives us three things the loop needs: run one frame with buttons held, read all 2 KB of RAM, and save or restore the whole machine.
Why not just read the screen?
The screen is what a person sees, and Phase 2 will use it. But the game's memory already contains Mario's exact position, speed, and the positions of every enemy, in a form a small network can learn from in minutes instead of days. Phase 1 reads those bytes. The bytes below are addresses in the NES CPU's memory on this exact ROM; a different dump would move them, which is why the ROM's SHA-256 is pinned and any other file refuses to boot.
| Meaning | Address | How it is read |
|---|---|---|
| x position in the stage | $006D, $0086 | page × 256 + on-screen x. 1-1 is 3,168 pixels long. |
| y position | $03B8, $00B5 | sprite y; $00B5 > 1 means Mario fell below the playfield |
| horizontal, vertical speed | $0057, $009F | signed bytes, pixels per NES frame |
| on ground / in the air / on a pole | $001D | 0 ground, 1 jumping, 2 falling, 3 climbing a vine or the flagpole |
| facing | $0033 | 1 right, 2 left |
| size | $0756 | 0 small, 1 big, 2 fire |
| player state | $000E | 0x08 normal, 0x0B dying, 0x06 dead, 0x02–0x07 pipe transit and auto-walk |
| game mode | $0770 | 0 title, 1 playing, 2 victory (the castle axe), 3 game over |
| lives, coins | $075A, $075E | coins wrap 99 → 0 with a 1-up, which the reward accounts for |
| score | $07DE–$07E3 | six decimal digits; a coin adds 200, verified at boot |
| world, stage, area | $075F, $075C, $0760 | 0-based; 1-1 is (0, 0, 0); the area changes inside pipes |
| timer | $07F8–$07FA | three digits, 400 down to 0 |
| enemy slots 0–4 | $000F, $0016, $001E, $006E/$0087, $00CF | exists, type, state (bit 0x20 = defeated), x, y |
| level tiles | $0500–$069F | two screens of 13 × 16 metatiles; used by the optional tile window |
Two latches that took care to get right
Death fires on the first frame the player state reads dying or Mario drops below the playfield. The obvious alternative, waiting for the lives counter to go down, fires 243 frames later, after the whole death animation, which would make the reward for the death step ambiguous. Clear fires the frame Mario grabs the pole (climbing state with a flagpole object present) or touches the castle axe. The timer-drain latch from the first design fires 159 frames later, after the slide and the walk to the castle; both numbers were measured on the real ROM with a scripted clear.
Buttons
The NES controller is eight bits. This project writes them in the game's own order; nes-py wants them reversed, and the emulator layer translates. Start and Select are masked off outside boot so the agent can never pause. Toggle bits to see the two bytes and which of the ten actions the combination is.
Snapshots and stage starts
Every episode starts by restoring a snapshot of the emulator taken at the first controllable frame of the stage. The snapshot is built once per process by a recipe: power on, tap Start past the title screen, poke the desired world and stage into memory while the game initializes, and stop on the first frame with the timer at 400 and Mario in the normal state. The RAM at that frame is hashed and the hash is pinned, so a changed ROM or a drifted recipe fails loudly instead of training on the wrong level. One subtlety found on the real ROM: nes-py's reset is a soft reset that keeps RAM, so the layer restores a true power-on snapshot instead.
Environment
The environment is the game with a fixed contract: reset() gives an observation, step(action) gives the next observation, a reward, and whether the episode ended.
One step, in order
- Hold the action's buttons for 4 NES frames.
- Read RAM and check the death and clear latches first. The flagpole grab puts Mario into an auto-walk state; if busy frames were skipped before checking, the clear would be missed and the next stage's load would look like a warp.
- If Mario is in a state the player cannot control (pipe transit, auto-walk), run those frames out with no button held. They are not agent steps and pay nothing.
- Decode everything, detect events, compute the reward.
- Decide how the step ends.
step(). Death and clear are checked before any busy frames are skipped, the skipped frames pay nothing and count for nothing, and only then is the reward computed and the ending decided.How an episode ends
| Ending | Kind | Detected by | What the learner does |
|---|---|---|---|
| Death | terminated | dying state, below playfield, or timer reached 0 | value of the next state is 0 |
| Clear | terminated | flagpole grab or castle axe | value of the next state is 0 |
| Stuck | truncated | no 8-pixel progress for 120 steps (200 in 1-4) | bootstraps from the value of the last state |
| Warp | truncated | world or stage changed without a clear | bootstraps; the curriculum ignores it |
| Cap | truncated | 2,500 steps | a safety net; the timer normally ends the life first |
The distinction matters. A terminated episode really ended: there is no future to value. A truncated one was cut off by us, so the learner estimates what would have followed. Getting these backwards teaches the agent that a timeout is death.
Sixteen at once
Sixteen environments run in sixteen processes, one emulator each, so the trainer collects 16 steps at a time. When one episode ends, that environment resets immediately and its next observation is already the new episode's first frame; the ended episode's last frame travels alongside in the info so the learner can still value it.
Observation
Thirty numbers describe the moment, 65 with the tile window. The agent never sees a pixel in Phase 1.
| Slots | Content | Scale |
|---|---|---|
| 0–5 | x, y, horizontal speed, vertical speed, on-ground flag, facing | x/4096, y/240, speeds/4, 0 or 1, ±1 |
| 6–8 | size as three flags: small, big, fire | one-hot |
| 9 | timer | /400 |
| 10–29 | five enemy slots × (relative x, relative y, exists, hazard) | /256, /240, flags |
| 30–64 | optional: a 7 × 5 window of "is this tile solid" flags around Mario, one tile ahead | flags |
A running mean and variance normalize each number before it reaches the network, updated only from training data and frozen during exams. The tile window was added after today's run showed the agent cannot see a pipe: without it, the policy must memorize where obstacles are by x coordinate alone, which worked for 1-1 and failed at a pipe in 1-2.
Recorded from the emulator: the 1-1 snapshot, then run-right held for 60 NES frames (15 agent steps). All 2,048 RAM bytes are embedded in this page and decoded here with the same arithmetic as ram.py. Click any bar to see the bytes behind it.
Slots 30 to 64: the 7 × 5 solid-tile window, drawn as it sits around Mario (outlined). The window is centered one tile ahead of him, so he sees two tiles behind and four ahead. Filled = solid.
What the tile-aware 1-1 teacher outputs for this exact vector. The 2-1 teacher, given the same moment, puts 0.64 on run-right.
Reward
The reward is the whole specification of what "playing well" means. Every term here is a decision, and each one has been broken by the agent at least once in the design's history.
The terms
| Term | Amount | When | Why this shape |
|---|---|---|---|
| Progress | +1 per pixel | every step, the change in x | the dense signal that makes anything learnable. Zeroed on death, on area changes, on skipped busy frames, and on jumps larger than 15 pixels (pipe warps, spawn snaps), so teleports are neither gifts nor punishments |
| Time | −0.25 | every step alive | standing still is strictly negative; a sprint step earns 10 to 14, so movement wins 40 to 1 |
| Mushroom, flower | +150, +250 | size goes up | worth a small detour, not a long one |
| Damage | −25 | size goes down, not on the death step | hurts, does not stack with death |
| Enemies | 0.10 × points, ≤50 per step, ≤300 per life | score rises by more than the coins explain | a stomp is worth half a sprint step; the caps and a farm lock stop the infinite shell trick on the 1-1 stairs |
| Coins | +5 each, ≤20 per life | the coin tally rises | coin blocks respawn on death, so an uncapped coin reward is a die-and-repeat loop; capped it stays smaller than one death |
| Death | −100 | the death step | flat cost of ending the life |
| Clawback | −½ of progress earned this life | any ending that is not a clear: death, stuck, cap | makes suicide-at-the-flag lose to clearing; extended to stuck today, see ground level |
| Clear | +1,500 | the clear step | pays for the pole grab, which progress does not |
| Time bank | +3 × seconds left | the clear step | a fast clear beats a slow one by at most 1,200 |
| Flag height | +500 × how high on the pole | flagpole clears | the game's own scoring of the finish |
| Stuck | −20 | 120 steps without 8 pixels of new progress | ends a life that is going nowhere |
| Warp | −50 | leaving the stage without the flag | the 1-2 warp zone is not a clear |
Set what happened in one step, or over a whole life, and see each component and the total with the current weights.
Try: progress 1875, stuck on. Then progress 2130, death on. Before today's change, stuck cost 20 and death cost 1,165; the agent learned to stand still.
Not potential-based, on purpose
A textbook shaping reward uses a potential function so that any policy optimal under the shaped reward is optimal under the original one. This reward is deliberately not that: progress is undiscounted, clipped, and zeroed on teleports. The code says so in a comment so that nobody "fixes" it into a potential and silently retunes every weight.
PPO: how the numbers change the network
Two small networks. The actor turns the observation into ten action probabilities; the critic guesses the return. Every 2,048 steps both are nudged.
The networks
Each is 30 → 256 → 256 → out, ReLU between, orthogonal initialization. The actor's last layer starts near zero so the first policy is nearly uniform over the ten actions. The critic's output is multiplied by a fixed 100 because returns here are in the thousands and a unit-scale head would take a hundred thousand updates to reach them; that was measured, not guessed. They share no weights, so the +1,500 clear bonus cannot drag the policy's features around.
Advantage: was that better than expected?
For each step the learner asks how much better the outcome was than the critic predicted. GAE blends one-step surprises with longer horizons using two knobs: γ (how much the future counts) and λ (how far the credit spreads). Edit the toy trajectory and move the knobs.
| t | reward r | critic V(s) | δ | advantage | return target |
|---|
The update
With advantages in hand, PPO raises the probability of actions with positive advantage and lowers the others, but clips the change to a ratio of 0.8 to 1.2 per action, so one lucky rollout cannot overwrite the policy. It also adds a small bonus for entropy, keeping exploration alive, and trains the critic toward the return targets. If the policy moves too far in one update (approximate KL above 0.03) the epoch loop stops early. Advantages are standardized per minibatch; rewards are never normalized, because the weights above were chosen in raw units.
| Knob | Value | Live-tunable |
|---|---|---|
| learning rate | 2.5e-4, decaying linearly to 0 over the run's horizon | yes |
| γ, λ | 0.99, 0.95 | no |
| clip | 0.2 | no |
| entropy coefficient | 0.01 | yes |
| rollout | 128 steps × 16 envs, 4 epochs, minibatch 512 | no |
| target KL | 0.03 | no |
Curriculum
Stages are learned in order. The rules for moving decide how the agent's time is spent; the diagram is the whole rulebook.
Simulate the 100k check. Each episode is a coin flip with your clear probability; the exam aborts at the first failure.
When a stage is passed, the actor and its normalizer are saved as teachers/ram/<stage>.pt and never overwritten; Phase 2 will learn from exactly the policy that passed, not from a later one that has moved on to harder stages and plays 1-1 wrong.
Live view and tuning
Training is headless by nature: sixteen processes stepping emulators as fast as they can. The live view opens a window into one of them and a channel back.
Environment 0 copies every frame it renders, plus a small state record, into a shared-memory block. Any number of readers can attach: the native window (tools/live.py), the browser panel (tools/panel.py). The block has one writer per region and a sequence lock, so a reader never sees half a frame. A reader can also write two things back: an override for the buttons, which lets you drive Mario while the other fifteen environments keep training, and a request to save or reload env 0's snapshot.
Tuning goes through runs/<name>/control.json. The trainer reads it at every rollout boundary and applies what changed: entropy coefficient, learning rate and its horizon, any reward weight, the stuck window, pause, slow motion for env 0, checkpoint now. Every applied change is written to events.jsonl with the step it took effect, and marks the CSV row, so a bend in a curve can always be traced to a knob.
| Key | Does |
|---|---|
| space | pause and resume training at the next rollout |
| h | take over env 0's controller (arrows, Z = A, X = B); press again to hand it back |
| s | slow motion for env 0 |
| F5 / F7 | save / load env 0's state |
| + / - | entropy coefficient ×1.5 / ÷1.5 |
| [ / ] | learning rate ÷1.5 / ×1.5 |
| c | checkpoint now |
Steps you drive are recorded with a human flag and excluded from the learning batch, so driving teaches nothing directly; it lets you put the agent in a situation and watch what it does next.
What happened today
The first real run, 10 million steps on the 5090, 2 hours 3 minutes. It solved 1-1 and then taught us something about the reward.
The diagnosis
Twelve greedy episodes with the final policy all ended stuck at x ≈ 1826 or x ≈ 1874 in 1-2: the base of a short pipe in the corridor after the piranha plants. Sampled play got past the pipe a quarter of the time and then died at the next hazard, x ≈ 2130. Compare the returns:
| Life | progress | time | ending | clawback | return |
|---|---|---|---|---|---|
| camp at the pipe until stuck | +1,875 | −110 | −20 | 0 | ≈ 1,745 |
| pass the pipe, die at 2,130 | +2,130 | −120 | −100 | −1,065 | ≈ 845 |
Under the reward as it stood, camping was worth twice as much as trying. The learner did exactly what it was paid to do. The clawback exists to make suicide at the flag lose to clearing, and it works; but because it grows with progress, a death late in a stage becomes far more expensive than a stall, and stalls were nearly free. 1-1 never exposed this because its hazards come early, when there is little to claw back.
The fix (decision D13): charge the same clawback on a stuck ending. Now camping at the pipe returns about 887 and trying returns about 845: a wash, so exploration decides, and the attempt has the upside of eventually clearing. The reward calculator above has both cases preloaded.
Run 2: the fix, and the rest of the curriculum
Run 2 started from scratch on the Mac Mini with three changes: the clawback on stuck endings (D13), the score-style rewards (D14), and the tile window in the observation. It did not stop at 1-2.
| Stage | passed at step | steps on the stage | Run 1, for comparison |
|---|---|---|---|
| 1-1 | 901,120 | 901k | 1.9M |
| 1-2 | 2,000,896 | 1.1M | never (8.1M steps) |
| 1-3 | 2,400,256 | 400k | not reached |
| 1-4 (castle) | 2,801,664 | 400k | not reached |
| 2-1 | 4,700,160 | 1.9M | not reached |
The exam tallies say something about how promotion works: 1-1 passed on its ninth 100k check, 1-2 on its ninth, 2-1 on its sixteenth. Training play was clearing far earlier each time; the exam waits for the most likely action to be reliable on five fixed seeds in a row.
What the red team found
A second session spent an hour on the real ROM trying to earn reward without playing well. Five of its findings became design decisions D16 to D20 before any of them cost a run.
What a reward red team is
The reward is a contract: it says what "playing well" pays. The learner is a very literal reader of that contract, and it will find any clause that pays without the flag. A red team reads the contract the same way, on purpose, before the learner does. It scripts the suspicious move on the real game, runs it through the real reward code, and writes down what it paid next to what an honest clear pays. If a trick pays more than trying, or pays with no risk, that is a finding, and the fix goes into the design before the next run. The first run of the day found one such clause by itself, the pipe stall, and it cost eight million steps. This time the tricks were found in an afternoon by hand.
The five accepted findings
| Finding | What it paid | Why the agent would find it | Fix | Replay it |
|---|---|---|---|---|
| Warp was the only ending with no clawback, and it bootstrapped the value of a fresh spawn | ≈ +840 in value terms, against −1,530 for dying at the same spot | The 1-2 warp zone is a plain run-right along the ceiling; the observation has no stage feature, so the first frame of world 4-1 looks exactly like the 1-1 spawn | D16: a warp terminates, bootstrap 0, and pays the clawback | No script; read from the code and the trainer's bootstrap contract, numbers analytic |
| A shell re-stomp was a fresh kill every time, and the farm lock never saw kills | Up to 600 per life (30 kills × 20), plus uncapped 1-ups from the shell combo | Stomp, walk into the shell, stomp it again is three actions; measured on 1-2 at x ≈ 655 | D17: one kill per enemy slot lifetime; the farm lock gates every combat term; one 1-up per life | uv run python docs/redteam/scripts/koopa_shell_restomp.py --show |
| The bonus pipe paid 568 less than walking the same span, and the shortcut escaped the clawback | Pipe 1,093.5 vs walking 1,661.4; pipe-then-die beat walk-then-die by 199 | Nothing to find: PPO already prefers walking, which is why no teacher ever takes the pipe. The leak shows up only when a pipe life ends in a death | D18: shortcut capped by the stage instead of at 800, and added to the progress the clawback halves | uv run python docs/redteam/scripts/pipe_route.py --show |
| Any non-stomp kill while a fireball was airborne counted as a fireball kill | +30 per leaked kill, at most 900 per life | Needs fire Mario plus a shell or a star; low damage, but it corrupts the fireball-kill curve | D19: a fire kill only if a live fireball was within a tile of that enemy on the kill frame or the one before | uv run python docs/redteam/scripts/trace_recording.py --show (shows the bytes the detector reads) |
| The vine paid per grab with no cap | +100 per re-grab, unbounded until the stuck rule fired | Needs 1-2's vine; the action set has no Up, so Mario can only grab, drop, and grab again | D20: vine once per life | Same script as above, evidence only; the vine loop is not scriptable yet |
Two more were checked and closed without a fix: the question-block-then-die loop pays about 78 per episode and never beats continuing, so the clawback holds; and piranha plants never show a kill state through three full 1-2 clears, so their in-and-out cycle is not a leak. The whole audit, with the measured tables, is docs/redteam/2026-09-26-reward-audit.md; the code changes are task T13.
Why the warp bootstrap was wrong
Phase 2 preview: pixels
The same game, the same reward, the same stage ladder, but the agent sees the screen instead of the memory. The plan is docs/phase2/PLAN.md; tasks T20 to T25.
What changes is only the front of the network. Every agent step, the four NES frames are turned gray, merged by taking the brightest pixel at each spot (so flickering sprites do not vanish), shrunk to 84 by 84, and stacked with the three previous steps. Four small gray pictures, oldest first, are the whole observation: no positions, no speeds, no tile window. A convolutional network, the same shape used for Atari since 2015, has to learn to see Mario, the ground, the pits and the enemies in those pictures, and to infer speed from how they shift between frames. That is far harder than reading the numbers, so the budget is 50 million steps for 1-1 alone, with a cap of 100 million, and a stop-the-line gate: under 20% greedy clears at 15 million steps means the pipeline is broken, not underfunded. On the 5090 at about 1,200 steps per second, the gate is roughly three and a half hours in and 50 million is a working day.
The Phase 1 teachers make this tractable. While the pixel agent trains, the saved RAM teacher for the current stage watches the same moments through its own 65 numbers and says what it would do. An extra loss pulls the pixel network's ten probabilities toward the teacher's, weighted by a dial β that starts at 0.5 and fades to 0 over the first two million steps on each stage, after which the pixel agent is on its own. You will be able to watch all of it: the four stacked frames drawn beside the real frame in the viewer and the panel, the 32 first-layer filters as a grid image at every checkpoint (edge and ground detectors should appear by about two million steps), the teacher-agreement number rising, and eval videos with the stack composited under the picture so a clear can be seen from both views.
How to run it
Everything runs from ~/mario-rl with uv. The ROM lives at roms/smb1.nes and is never committed.
Watch a trained policy play
uv run python tools/play.py --model teachers/ram/1-1.pt --stage 1-1
uv run python tools/play.py --model runs/p1-1-1-10m/checkpoints/ckpt-0010000384.pt --stage 1-2
Train on the Mini with the live window
uv run python -m mario_rl.train --phase 1 --stage 1-1 --steps 10000000 --envs 16 --live --obs-tiles --out runs/live-1
# in another Terminal:
uv run python tools/live.py --run runs/live-1
uv run python tools/panel.py --run runs/live-1 # then open http://localhost:8765
Read a run
uv run python tools/plot_run.py runs/live-1 # PNGs in runs/live-1/plots/
uv run python tools/alerts.py runs/live-1 # the failure-mode checks, one line each
Unattended runs on the 5090
scripts/sync-5090.sh # copy the tree, run the tests there
scripts/start-5090-run.sh p1-x --phase 1 --stage 1-1 --steps 10000000 --envs 16
scripts/watch-5090.sh p1-x # pulls metrics and plots every 30 min
scripts/pull-run.sh p1-x --videos 6 --ckpt latest --teachers
Check the machine before a long run
uv run pytest -q # the whole suite, about 25 s
uv run python tools/boot_asserts.py # five checks on the real ROM
Every knob, reward, and chart, explained
The same text the panel shows behind each help icon, from one file: docs/help.json. Switch on plain words for a first-time reader.
Loading help.json…
Decision log
Every place this design departs from the first draft, with the reason. The full text is in DESIGN-v2.md.
| # | Decision | Why |
|---|---|---|
| D1 | nes-py as the training emulator; FCEUX and Mesen for watching and probing | no windowless FCEUX on macOS; per-frame Lua IPC caps throughput; nes-py runs thousands of frames per second in-process |
| D2 | stage starts by boot recipe plus a pinned RAM hash | reproducible from the ROM alone; tamper-evident |
| D3 | death latch on the first dying frame | the lives counter lags by 243 frames |
| D4 | clear latch at the pole grab | the timer-drain latch lags by 159 frames and pays for the auto-walk |
| D5 | busy frames are not agent steps | no control, no signal |
| D6 | coin tally wraps modulo 100 | a 1-up would otherwise read as −99 coins and fake 19,800 combat points |
| D7 | own PPO, not a library | separate trunks, kickstart loss, curriculum mix, and full checkpoints fight every abstraction |
| D8 | Python 3.13 via uv | nes-py 9 requires it |
| D9 | vision-language judge deferred | a ±5 nudge against returns of thousands; last thing that can help, first that can burn a week |
| D10 | Phase 1 on the Mini, Phase 2 on the 5090 | the MLP is CPU-bound and the Mini is faster and where you sit |
| D12 | critic output ×100 | measured: explained variance stayed near zero without it |
| D13 | clawback on stuck and cap, not only death | today's run: stalling was worth twice as much as trying |
| D14 | score-style rewards as live knobs, with caps | your goal: enemies, points, coins, time left; caps keep the coin farm unprofitable |
File index
| Path | What |
|---|---|
| DESIGN-v2.md | the design, with the decision log |
| mario_rl/ | the trainer: emulator, ram, stages, env, obs, reward, ppo, curriculum, eval, train, logging, vec, live |
| tests/ | 360+ tests; test_real_rom.py runs against the ROM when present |
| tools/ | play, live, panel, plot_run, alerts, boot_asserts, find_clear, record_inputs.lua, replay_inputs, bridge |
| scripts/ | setup-mini, setup-5090, sync-5090, run-5090, start-5090-run, watch-5090, pull-run |
| ops/ralph/ | the ten-minute orchestration tick and its restart logic |
| docs/tasks/ | one file per task with the lead's reviews; docs/TASKS.md is the board |
| comms/ | STATUS.md, the two inboxes, the Ralph snapshots |
| runs/ · teachers/ · stages/ · rom.sha256 | run outputs, the exported teachers, the stage hash pins, the ROM's pinned hash |