Altitude 10,000 ft

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.

TRY IT · ONE AGENT STEP
1 · EmulatorRuns 4 NES frames with the chosen buttons held.
2 · RAM2,048 bytes of game memory are read.
3 · Observation30 numbers: where Mario is, how fast, what is near.
4 · PolicyA small neural net turns the 30 numbers into 10 probabilities, one per action.
5 · ActionOne action is sampled and becomes a button mask.
6 · RewardPixels gained, time spent, events. One number.
7 · LearningEvery 2,048 steps the net is nudged toward higher-reward choices.
Frame 0 of World 1-1. Mario stands at x = 40, timer 400, small, on the ground.

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.

Altitude 10,000 ft

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.
Altitude 10,000 ft

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.

TRY IT · ONE 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.

jump?
0.50

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.

Altitude 1,000 ft

System map

Two machines, a handful of processes, and the files that connect them. The arrows are what actually moves.

Mac Mini (M4) · where you sit 5090 box (WSL2) · unattended runs trainer (python -m mario_rl.train) collects rollouts runs PPO updates checks the curriculum polls control.json 16 env workers one process each one nes-py emulator each MarioEnv: step, reward, latches env 0 also publishes frames actions obs, reward, done shared memory (live channel) frame · game state · policy state every NES frame viewer window, keys panel browser, sliders reads at its own pace runs/<name>/control.json weights, lr, entropy, pause, takeover writes applied at each rollout runs/<name>/: metrics.csv · events.jsonl · checkpoints/ · videos/ · plots/ teachers/ram/<stage>.pt lead reviews, dispatches, starts runs tick every 10 min same trainer, 16 envs started by scripts/start-5090-run.sh tmux session train-<name> CUDA torch for Phase 2 runs/ on the 5090 pulled by scripts/pull-run.sh rsync over ssh (watcher, every 30 min)
The trainer and its 16 environment workers are the loop. Env 0 additionally streams every frame into a shared-memory channel that the viewer and the browser panel read; both write a control file that the trainer applies at the next rollout boundary. The 5090 runs the identical trainer for long unattended runs and its results are synced back.

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.

Altitude 1,000 ft

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.

Altitude 100 ft

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.

MeaningAddressHow it is read
x position in the stage$006D, $0086page × 256 + on-screen x. 1-1 is 3,168 pixels long.
y position$03B8, $00B5sprite y; $00B5 > 1 means Mario fell below the playfield
horizontal, vertical speed$0057, $009Fsigned bytes, pixels per NES frame
on ground / in the air / on a pole$001D0 ground, 1 jumping, 2 falling, 3 climbing a vine or the flagpole
facing$00331 right, 2 left
size$07560 small, 1 big, 2 fire
player state$000E0x08 normal, 0x0B dying, 0x06 dead, 0x02–0x07 pipe transit and auto-walk
game mode$07700 title, 1 playing, 2 victory (the castle axe), 3 game over
lives, coins$075A, $075Ecoins wrap 99 → 0 with a 1-up, which the reward accounts for
score$07DE–$07E3six decimal digits; a coin adds 200, verified at boot
world, stage, area$075F, $075C, $07600-based; 1-1 is (0, 0, 0); the area changes inside pipes
timer$07F8–$07FAthree digits, 400 down to 0
enemy slots 0–4$000F, $0016, $001E, $006E/$0087, $00CFexists, type, state (bit 0x20 = defeated), x, y
level tiles$0500–$069Ftwo 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.

TRY IT · BUTTON ENCODER

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.

Altitude 100 ft

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

  1. Hold the action's buttons for 4 NES frames.
  2. 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.
  3. 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.
  4. Decode everything, detect events, compute the reward.
  5. Decide how the step ends.
1 · 4 frames hold the action's buttons for 4 NES frames, then read all 2,048 bytes 2 · latches first death? $000E dying, or below the playfield clear? pole grab or the castle axe 3 · busy skip pipe transit, auto-walk, the area-load frame: no-op until over; those frames are not steps 4 · reward Δx, masked and clipped −0.25 time events: coins, kills, blocks, pipes, power-ups 5 · ending terminated: death, clear truncated: stuck, warp, 2,500 cap otherwise: the observation goes to the policy, the next action comes back, and step 1 runs again latches before the skip: the pole grab starts an auto-walk, and skipping first would miss the clear
The order inside 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

EndingKindDetected byWhat the learner does
Deathterminateddying state, below playfield, or timer reached 0value of the next state is 0
Clearterminatedflagpole grab or castle axevalue of the next state is 0
Stucktruncatedno 8-pixel progress for 120 steps (200 in 1-4)bootstraps from the value of the last state
Warptruncatedworld or stage changed without a clearbootstraps; the curriculum ignores it
Captruncated2,500 stepsa 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.

Altitude 100 ft

Observation

Thirty numbers describe the moment, 65 with the tile window. The agent never sees a pixel in Phase 1.

RAM: 2,048 bytes $006D $0086 0 40 x $03B8 176 y $0057 $009F 0 0 vx vy $001D 0 ground $0033 1 facing $0756 0 small $07F8..FA 4 0 0 timer $000F..13 0 0 0 0 0 enemies $0500..69F ... tiles 2,030 other bytes the agent never sees decode observation: 30 numbers 0 x 40/4096 = 0.010 1 y 176/240 = 0.733 2-3 vx vy 0/4 = 0 0 4 ground 1 5 facing +1 6-8 small big fire 1 0 0 9 timer 400/400 = 1.0 10-29 enemies 20 × 0 30-64 tiles 35 flags (65 with the tile window) then: subtract the running mean, divide by the running std, clip ±5 normalize the actor network 30 → 256 → 256 → 10, ReLU between softmax 10 probabilities the 1-1 teacher, at this frame
The first frame of 1-1, bytes to buttons. The addresses on the left are real; the numbers in the middle are what the decoder makes of them; the probabilities on the right are what the saved 1-1 teacher outputs when given exactly that vector. It is 84% sure it should run right.
SlotsContentScale
0–5x, y, horizontal speed, vertical speed, on-ground flag, facingx/4096, y/240, speeds/4, 0 or 1, ±1
6–8size as three flags: small, big, fireone-hot
9timer/400
10–29five enemy slots × (relative x, relative y, exists, hazard)/256, /240, flags
30–64optional: a 7 × 5 window of "is this tile solid" flags around Mario, one tile aheadflags

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.

TRY IT · A REAL OBSERVATION, 60 FRAMES IN

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.

Click a bar to see where the number came from.
Altitude 100 ft

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

TermAmountWhenWhy this shape
Progress+1 per pixelevery step, the change in xthe 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.25every step alivestanding still is strictly negative; a sprint step earns 10 to 14, so movement wins 40 to 1
Mushroom, flower+150, +250size goes upworth a small detour, not a long one
Damage−25size goes down, not on the death stephurts, does not stack with death
Enemies0.10 × points, ≤50 per step, ≤300 per lifescore rises by more than the coins explaina 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 lifethe coin tally risescoin blocks respawn on death, so an uncapped coin reward is a die-and-repeat loop; capped it stays smaller than one death
Death−100the death stepflat cost of ending the life
Clawback−½ of progress earned this lifeany ending that is not a clear: death, stuck, capmakes suicide-at-the-flag lose to clearing; extended to stuck today, see ground level
Clear+1,500the clear steppays for the pole grab, which progress does not
Time bank+3 × seconds leftthe clear stepa fast clear beats a slow one by at most 1,200
Flag height+500 × how high on the poleflagpole clearsthe game's own scoring of the finish
Stuck−20120 steps without 8 pixels of new progressends a life that is going nowhere
Warp−50leaving the stage without the flagthe 1-2 warp zone is not a clear
TRY IT · REWARD CALCULATOR

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.

Altitude 100 ft

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.

TRY IT · GAE EXPLORER
treward rcritic V(s)δadvantagereturn 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.

1 · rollout buffer 2,048 transitions: obs, action, log p_old, reward, critic value V 128 steps × 16 envs 2 · advantages (GAE) δ = r + γ·V(next) − V A = δ + γλ·A(next) critic target = A + V A standardized per minibatch 3 · clipped ratio ratio = p_new(a) / p_old(a) gain = min(ratio·A, clip(ratio, 0.8, 1.2)·A) + 0.01·entropy − 0.5·critic error 4 · gradient step Adam, lr 2.5e-4 → 0 gradient norm ≤ 0.5 all 75k weights move a little, together repeat: 4 epochs × 4 minibatches of 512; stop early if KL > 0.03 the clip when A > 0 (raise this action) 0.8 1.0 1.2 ratio no more gain past 1.2 gain ↑ the clip when A < 0 (lower this action) 0.8 1.0 1.2 ratio no more gain below 0.8 gain ↑
One update. The rollout is turned into advantages, each sampled action's probability ratio is clipped to 0.8 to 1.2, and one Adam step moves every weight. The two plots are the clip itself: once an action's probability has moved 20% in the helpful direction, moving it further earns nothing, so one lucky rollout cannot rewrite the policy.
KnobValueLive-tunable
learning rate2.5e-4, decaying linearly to 0 over the run's horizonyes
γ, λ0.99, 0.95no
clip0.2no
entropy coefficient0.01yes
rollout128 steps × 16 envs, 4 epochs, minibatch 512no
target KL0.03no
Altitude 100 ft

Curriculum

Stages are learned in order. The rules for moving decide how the agent's time is spent; the diagram is the whole rulebook.

promote: every 100k steps, if there was a training clear in the last 100 episodes, play 5 greedy episodes on seeds 1000–1004; all 5 must clear demote: after 500k steps on a stage, if clears < 2% and median progress < 25% over 200 episodes; then 70% of envs on the previous stage for 1M steps a second demotion at the same boundary freezes promotion until a person clears it; below 50% greedy clears on 1-1 at 5M steps, the run stops itself
Promotion is a strict exam, demotion is a slow safety valve, and two demotions at one boundary mean something is wrong with the design rather than the luck.
TRY IT · PROMOTION EXAM

Simulate the 100k check. Each episode is a coin flip with your clear probability; the exam aborts at the first failure.

Passing needs all five. At 60% per episode that is 0.6⁵ ≈ 7.8% per check; at 90% it is 59%. This is why promotion tends to happen only once the policy is reliable, not merely lucky.

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.

Altitude 100 ft

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.

env 0 one of the 16 workers steps like the others, also publishes every frame override shared memory frame · game state · policy state buttons override · save/load one writer per region, seq lock read h, F5, F7 viewer window keys panel browser sliders, charts write a knob runs/<name>/control.json weights · lr · entropy · pause · slow-mo polled trainer applies changes at the next rollout boundary, logs each to events.jsonl the trainer owns env 0 like the other fifteen: actions out, observations back
Two directions, two mechanisms. Frames flow out through shared memory as fast as they are made; the viewer and panel read when they like. Tuning flows back through a small JSON file that the trainer reads once per rollout, so a knob never interrupts an update mid-way. Only the takeover and the save/load requests go straight to env 0.

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.

KeyDoes
spacepause and resume training at the next rollout
htake over env 0's controller (arrows, Z = A, X = B); press again to hand it back
sslow motion for env 0
F5 / F7save / load env 0's state
+ / -entropy coefficient ×1.5 / ÷1.5
[ / ]learning rate ÷1.5 / ×1.5
ccheckpoint 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.

Ground level

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.

Outcome rates over 7.5 million steps: clear rate rises to about 0.6 by 1.9M then drops to 0 as the stage changes; death rate falls from 1.0 toward 0.25; stuck rate rises to about 0.75 after 6M.
Outcome rates over the run. The clear rate (blue) climbs on 1-1 until step 1.9M, when the agent passed its exam and moved to 1-2, where it drops to zero. On 1-2 deaths (orange) fall over time while stuck endings (green) rise to three quarters of all episodes.
Progress as a fraction of stage length: rises to 0.8 on 1-1, resets to 0.2 at the promotion to 1-2, then climbs slowly to about 0.5.
How far along the stage each episode got. The cliff at 1.9M is the promotion: 1-2 is a new stage, and progress starts over.

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:

Lifeprogresstimeendingclawbackreturn
camp at the pipe until stuck+1,875−110−200≈ 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.

Entropy, approximate KL, and clip fraction over the run.
Policy statistics. Entropy stayed between 1.2 and 1.6 nats the whole run: the policy kept exploring; the stall was a preference, not a collapse.
Value loss and explained variance over the run.
The critic's explained variance rose to 0.8–1.0 within the first million steps, which is what the ×100 output scale bought.

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.

Stagepassed at stepsteps on the stageRun 1, for comparison
1-1901,120901k1.9M
1-22,000,8961.1Mnever (8.1M steps)
1-32,400,256400knot reached
1-4 (castle)2,801,664400knot reached
2-14,700,1601.9Mnot reached
Run 2 outcome rates: clear rate rises on each stage and resets at each of four promotions; death and stuck rates fall over the run.
Run 2's outcome rates. Each dip in the clear rate is a promotion to a new stage; each recovery is that stage being learned.
Run 2 progress fraction: a saw-tooth that climbs to near 1.0 on each stage and resets at each promotion.
The saw-tooth of a curriculum working: progress climbs on a stage, resets on promotion, climbs again.
Run 2 reward components per episode on a symmetric log axis: progress, clear bonus, time bank, flag height, coins, enemies, time, death, clawback.
Where the points came from, term by term, on a symmetric log scale. The clear bonus and the progress term dominate; the score-style terms are visible but small.

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.

Ground level

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

FindingWhat it paidWhy the agent would find itFixReplay 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 spotThe 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 spawnD16: a warp terminates, bootstrap 0, and pays the clawbackNo 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 killsUp to 600 per life (30 kills × 20), plus uncapped 1-ups from the shell comboStomp, walk into the shell, stomp it again is three actions; measured on 1-2 at x ≈ 655D17: one kill per enemy slot lifetime; the farm lock gates every combat term; one 1-up per lifeuv 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 clawbackPipe 1,093.5 vs walking 1,661.4; pipe-then-die beat walk-then-die by 199Nothing 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 deathD18: shortcut capped by the stage instead of at 800, and added to the progress the clawback halvesuv 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 lifeNeeds fire Mario plus a shell or a star; low damage, but it corrupts the fireball-kill curveD19: a fire kill only if a live fireball was within a tile of that enemy on the kill frame or the one beforeuv 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 firedNeeds 1-2's vine; the action set has no Up, so Mario can only grab, drop, and grab againD20: vine once per lifeSame 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

Three ways to end a life at x ≈ 2,860 on 1-2, with 2,860 pixels of progress banked value target for the ending step = reward on that step + 0.99 × bootstrap warp, before D16 pipe +50, warp −50 = 0 truncated, not terminal bootstrap V(next observation) ≈ 840 the first frame of 4-1: x 40, timer 400, no enemies indistinguishable from the 1-1 spawn the critic values highly target ≈ 0 + 0.99 × 840 ≈ +830 beats dying, beats stalling, and beats clearing once the last section kills 1 in 3 death, same spot −100 − ½ × 2,860 = −1,530 terminal bootstrap 0 a terminal ending has no future to value target = −1,530 what D13 intended every non-clear ending to feel like warp, after D16 +50 − 50 − ½ × 2,860 = −1,430 terminal bootstrap 0 the fresh-spawn value no longer flows back target = −1,430 no better than stalling, far below the real exit pipe and the flag
A truncated ending borrows the critic's value of whatever comes next. After the warp pipe, what comes next looks like a brand-new stage, which the critic values at about 840, so the warp inherited a reward it never earned. Making the warp terminal cuts that flow to zero and charges the same clawback as any other failure to clear.
Next

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.

the student sees pixels 4 gray frames, 84 × 84, max over the 4 skipped frames, stacked oldest to newest Nature CNN (actor) conv 32 @ 8×8 stride 4 conv 64 @ 4×4 stride 2 conv 64 @ 3×3 stride 1 fc 512 → 10 logits a twin CNN is the critic π_CNN 10 probabilities sampled for play, trained by PPO (§9 knobs) the teacher reads RAM, frozen the same moment as 65 numbers (Phase 1 obs) RAM teacher (MLP) teachers/ram/<stage>.pt, 65→256→256→10 π_RAM 10 probabilities kickstart loss = β · CE(π_RAM ‖ π_CNN) β = 0.5 → 0 over the first 2M steps on each stage, added to the PPO loss; the teacher swaps on promotion pulls the student toward the teacher, then lets go budget: 50M steps for 1-1, cap 100M gate: greedy clears ≥ 20% at 15M or stop watch: stacked frames, first-layer filters, teacher agreement, composited eval videos
Two networks look at the same moment. The student sees four stacked gray frames; the frozen teacher sees the 65 numbers it was trained on. Early on, a cross-entropy term pulls the student's ten probabilities toward the teacher's; the weight β fades to zero over two million steps and the student is then trained by PPO alone.
Cockpit

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
Cockpit

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…

Appendix

Decision log

Every place this design departs from the first draft, with the reason. The full text is in DESIGN-v2.md.

#DecisionWhy
D1nes-py as the training emulator; FCEUX and Mesen for watching and probingno windowless FCEUX on macOS; per-frame Lua IPC caps throughput; nes-py runs thousands of frames per second in-process
D2stage starts by boot recipe plus a pinned RAM hashreproducible from the ROM alone; tamper-evident
D3death latch on the first dying framethe lives counter lags by 243 frames
D4clear latch at the pole grabthe timer-drain latch lags by 159 frames and pays for the auto-walk
D5busy frames are not agent stepsno control, no signal
D6coin tally wraps modulo 100a 1-up would otherwise read as −99 coins and fake 19,800 combat points
D7own PPO, not a libraryseparate trunks, kickstart loss, curriculum mix, and full checkpoints fight every abstraction
D8Python 3.13 via uvnes-py 9 requires it
D9vision-language judge deferreda ±5 nudge against returns of thousands; last thing that can help, first that can burn a week
D10Phase 1 on the Mini, Phase 2 on the 5090the MLP is CPU-bound and the Mini is faster and where you sit
D12critic output ×100measured: explained variance stayed near zero without it
D13clawback on stuck and cap, not only deathtoday's run: stalling was worth twice as much as trying
D14score-style rewards as live knobs, with capsyour goal: enemies, points, coins, time left; caps keep the coin farm unprofitable
Appendix

File index

PathWhat
DESIGN-v2.mdthe 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.sha256run outputs, the exported teachers, the stage hash pins, the ROM's pinned hash