Mario + Jev
September 17, 2026 · View on GitHub
A uv-managed Python prototype that plays NES Super Mario Bros. (level 1-1 by default). Jev receives structured RAM observations and answers focused questions about movement, starting a jump, and sustaining a jump, plus timing hops under low ceilings. Code composes their answers into controller buttons. The emulator pauses while Jev responds, then advances up to four game frames by default, stopping early on landing. The resizable game window opens at 800×600 by default. No JavaScript is required.
Setup
uv sync --locked
cp .env.example .env
Put your TypeSafe key in .env:
TYPESAFE_API_KEY=your-key-here
The key can also be supplied through your shell environment. .env and runs/
are ignored by Git. Do not put API keys in gameplay logs or commit them.
Python 3.13 is selected by .python-version; uv installs it if needed.
Play
Try the emulator without API calls:
uv run mario-jev --policy scripted
Make a short Jev run first (at most 25 API requests):
uv run mario-jev --decisions 25
Then run a longer attempt:
uv run mario-jev --decisions 500
Choose a world (1–8) and stage (1–4):
# Next level: 1-2
uv run mario-jev --world 1 --stage 2 --decisions 500
# World 2, stage 1
uv run mario-jev --world 2 --stage 1
Both options default to 1. Replay uses the level saved in the log.
Additional commands:
# Inspect exactly what Jev will see, without calling it
uv run mario-jev --dump-state --headless
# Run the baseline without a game window
uv run mario-jev --policy scripted --headless --episodes 3
# Tune the time each action is held
uv run mario-jev --frames 4 --decisions 200 --model jev-latest
uv run mario-jev --help
Each episode resets the selected level, and ends on death, completion, or the decision
limit. Ctrl-C stops the run. Jev calls use a 15-second HTTP timeout with automatic
retries disabled; an API error stops gameplay instead of consuming more requests.
Every Jev decision is a paid API request. --decisions limits calls per episode;
--episodes multiplies that limit. No API key is needed for the scripted policy
or state dump.
Replay
Replay a recorded gameplay log with no API calls or API key:
uv run mario-jev --replay runs/20260917T051444816158Z.jsonl
# Twice normal playback speed
uv run mario-jev --replay runs/20260917T051444816158Z.jsonl --speed 2
# Fast headless verification
uv run mario-jev --replay runs/20260917T051444816158Z.jsonl --headless
Substitute your own timestamped log path. Playback uses the recorded seed,
controller actions, and actual frames executed, including landing-shortened
intervals. It checks positions against the log and stops on divergence. Keep
uv.lock and the same game/emulator version for reproducible playback. Visible
playback defaults to normal game speed because there is no model wait. Logs are
local and excluded from Git; the sample filename refers to the verified local
successful run, not a bundled recording.
Observations and logs
state.py decodes Mario's position, motion, grounded state, nearby enemy slots,
and the visible portion of the two RAM metatile buffers. It adds measured Mario
and enemy velocity in pixels per game frame, approximate time to enemy contact,
body/feet coordinates, nearby obstacle height, empty terrain columns, overhead
clearance, blocked-forward detection, and the last twelve transitions by default. A jump corridor reports
ceiling spans up to 128 pixels ahead, available headroom, and whether low bricks
lie on the approach to an enemy. This is geometry rather than jump simulation. Velocity is
an average over the previous action interval; it is not a predicted trajectory.
Enemy estimates reset when the slot/type changes or a teleport is detected.
Tile columns are relative to Mario, with thirteen rows starting at screen y=32. The decoder uses a small set of known vanilla SMB1 solid tiles; raw metatile IDs remain available. Unreported terrain is unknown. It is specific to vanilla SMB1, not SMB2, SMB3, or ROM hacks. Empty columns can indicate pits or drops; geometric summaries and contact times are approximate, not collision guarantees.
Each transition includes before/after position, measured velocity, grounded state,
action, actual frames held, reward, and possible landing/head-bump/blocking events.
current_jump tracks takeoff, elapsed frames, distance and peak height across the
whole jump, even when takeoff leaves the recent-history window. last_jump
reports the previous completed jump. The last four consecutive frame samples are also included as recent_frames.
Memory resets at each episode. Velocities
are interval averages; collision events are estimates, not engine guarantees.
The runner checks RAM after every emulator step. It interrupts frame repetition
on landing and immediately asks Jev for the next action, recording the actual
frames held and landing_frame. Four is the maximum default action duration,
not a promise to always hold buttons for four frames. This avoids hiding a
brief grounded state between calls. It borrows the four-frame action interval
and observation history from the prior PPO pipeline, while landing interruption
is an additional safeguard for the API controller. Compared with six-frame
actions, four-frame actions need roughly 50% more calls per game second; landing
interruptions can add more.
landing_surfaces describes exposed solid tile tops and visible floor gaps,
including a far-bank height where available. These are candidates, not guaranteed
reachable surfaces. History and geometry help Jev reason about trajectories;
there is no forward physics simulation yet. --history 8 changes the recent
transition count. More history increases input-token cost, not requests per decision.
Jev answers movement (run/walk/brake/wait), start_jump, ceiling_hop, and
sustain_jump in one API call. Jump answers use Noul probabilities with a 0.5
threshold. On an enemy approach under low bricks, without a competing pipe or
pit, code uses the focused ceiling_hop answer instead of the ordinary
start_jump answer.
Code selects start-jump only when grounded and A previously released, and uses
sustain-jump while airborne. It adds no scripted hazard override. Separate jump
buttons allow braking or waiting while jumping. Logs record every model answer
and the composed action; the reported top-level confidence belongs to movement,
not to the complete controller action or probability of surviving.
Timestamped runs/*.jsonl files contain configuration, input state, controller
choice, Jev confidence and probabilities, token usage, API latency, actual frames
executed, reward, next position, and episode summaries. These are decision logs,
not saved emulator states or video recordings. The composed controller action is executed directly.
The scripted controller is a simple baseline. Local verification reached x=2471 before dying; it does not currently complete the level. A bounded live evaluation of the revised Jev controller passed the first Goomba, early pipes, and first pit, reaching x=1594 after 110 decisions without dying. The original controller died at x=315. These are individual runs, not a measured completion rate; full-level completion is not yet demonstrated. A targeted replay of a later ceiling/Goomba failure reached x=2902 alive with the ceiling timing improvement; the original recorded run died at x=2764. The replay restores the approach by executing recorded actions, then uses fresh Jev decisions. It is not a full run or training. With richer transition history and landing geometry, a replay of the later ditch approach landed on the upper stair, launched a second jump, and reached x=2567 alive beyond the gap; the previous runs died around x=2472–2474. With four-frame actions and per-frame landing interruption, another replay caught the upper-step landing at x=2431 after one frame, launched a new jump, and reached x=2551 alive.
Development
uv run pytest
uv run ruff check src tests
uv run ruff format --check src tests
src/mario_jev/cli.py: episode runner and JSONL loggingsrc/mario_jev/state.py: SMB1 memory decoding and terrain geometrysrc/mario_jev/history.py: bounded transition history and jump trackingsrc/mario_jev/runner.py: per-frame observations and landing interruptionssrc/mario_jev/policy.py: Jev choices, button mappings, and scripted baselineuv.lock: exact resolved dependency versions
References: TypeSafe SDK quick start, gym-super-mario-bros, SMB1 memory definitions. The installed emulator package supplies its game assets; this repository does not copy or distribute ROM files.