Metadata-Version: 2.4
Name: supermariobrosnes-turbo
Version: 0.5.0
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: MacOS
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Dist: gymnasium>=0.29
Requires-Dist: numpy>=1.26
Requires-Dist: textual>=8.2.8,<9
Requires-Dist: huggingface-hub>=1.8.0 ; extra == 'dev'
Requires-Dist: maturin>=1.9.3,<2 ; extra == 'dev'
Requires-Dist: pytest>=8 ; extra == 'dev'
Requires-Dist: stable-retro-turbo==1.0.1.post34 ; python_full_version == '3.14.*' and extra == 'dev'
Requires-Dist: stable-retro-turbo==1.0.1.post34 ; python_full_version == '3.14.*' and extra == 'oracle'
Requires-Dist: huggingface-hub>=1.8.0 ; extra == 'playback'
Requires-Dist: pytest>=8 ; extra == 'test'
Provides-Extra: dev
Provides-Extra: oracle
Provides-Extra: playback
Provides-Extra: test
License-File: LICENSE
License-File: NOTICE.md
Summary: Blazing fast SuperMarioBros-Nes environment for Reinforcement Learning.
Keywords: gymnasium,nes,reinforcement-learning,rust,super-mario-bros,vector-environment
Home-Page: https://github.com/tsilva/SuperMarioBros-Nes-turbo
Author: Tiago Silva
Maintainer: Tiago Silva
License-Expression: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/tsilva/SuperMarioBros-Nes-turbo/blob/main/CHANGES.md
Project-URL: Documentation, https://github.com/tsilva/SuperMarioBros-Nes-turbo#readme
Project-URL: Homepage, https://github.com/tsilva/SuperMarioBros-Nes-turbo
Project-URL: Issues, https://github.com/tsilva/SuperMarioBros-Nes-turbo/issues
Project-URL: Repository, https://github.com/tsilva/SuperMarioBros-Nes-turbo.git

<div align="center">
  <img src="logo.png" alt="SuperMarioBros-Nes-turbo logo" width="320" />

  **🚀 Blazing fast SuperMarioBros-Nes environment for Reinforcement Learning 🍄**
</div>

<div align="center">
  <img src="media/mario-promo/mario-throughput-comparison.gif" alt="Stable Retro and SuperMarioBros-Nes-turbo throughput comparison" width="640" />
</div>

**SuperMarioBros-Nes-turbo** is a Rust-backed Gymnasium vector environment for
reinforcement-learning researchers working with Super Mario Bros NES. In the
published `0.3.0` mapper 0/NROM benchmark, it measured **13.27× to 18.27×** the
end-to-end step and preprocessing throughput of
[Stable Retro](https://github.com/Farama-Foundation/stable-retro), depending on
the host and number of environments.

## ⚡ Why it is fast

- **Focused scope.** It specializes in the canonical Super Mario Bros mapper
  0/NROM workload.
- **Native vector engine.** One Rust engine owns all lanes, releases the GIL,
  and parallelizes batches of four or more environments with Rayon.
- **One efficient call.** Actions, emulation, preprocessing, frame stacks,
  rewards, termination, and infos share reused buffers across one
  Python-to-Rust call.
- **Optimized rendering.** Guarded game-routine fast paths, event-bounded PPU
  stepping, and direct grayscale rendering avoid unnecessary interpreter and
  image work.

*Unsupported fast-path cases fall back to the instruction interpreter.*

## 📦 Install

Install the prebuilt package from PyPI:

```bash
python -m pip install supermariobrosnes-turbo
```

Prebuilt wheels support Python `>=3.9` on macOS, Linux, and Windows without a
Rust toolchain. See [CONTRIBUTING.md](CONTRIBUTING.md) for the source checkout
and development setup.

**ROM setup:** ROM files are not included. Set `RETRO_DATA_PATH` to a
user-writable data directory, then import the supported ROM from a file,
directory, or ZIP archive.

On macOS or Linux:

```bash
export RETRO_DATA_PATH="${XDG_DATA_HOME:-$HOME/.local/share}/retro"
smb-turbo import /path/to/roms
```

On Windows PowerShell:

```powershell
$env:RETRO_DATA_PATH = "$env:LOCALAPPDATA\retro"
smb-turbo import C:\path\to\roms
```

The importer writes
`<RETRO_DATA_PATH>/stable/SuperMarioBros-Nes-v0/rom.nes`. If the variable is
unset, it uses the equivalent data tree inside the installed package instead.
`rom_path=` and the CLI's `--rom` remain available as overrides. The canonical
ROM SHA-256 is:

```text
f61548fdf1670cffefcc4f0b7bdcdd9eaba0c226e3b74f8666071496988248de
```

## 🎮 Use

```python
import numpy as np

from supermariobrosnes_turbo import (
    Actions,
    SuperMarioBrosNesTurboVecEnv,
    action_batch,
)

env = SuperMarioBrosNesTurboVecEnv(
    "SuperMarioBros-Nes-v0",
    state="Level1-1",
    num_envs=16,
    use_restricted_actions="basic",
    frame_skip=4,
    obs_grayscale=True,
    obs_crop=(32, 0, 0, 0),
    obs_resize=(84, 84),
    obs_layout="chw",
    frame_stack=4,
)

observations, infos = env.reset(seed=123)
observations, rewards, terminated, truncated, infos = env.step(
    action_batch("right", env.num_envs)
)

done = terminated | truncated
if done.any():
    state_indices = np.full(env.num_envs, -1, dtype=np.int32)
    state_indices[done] = 0
    observations, reset_infos = env.reset(
        options={"reset_mask": done.copy(), "state_indices": state_indices},
    )
```

**Important:** Autoreset is disabled. Selectively reset terminal lanes before
stepping again.

Live positions can be captured without advancing emulation and restored into
any lane of the same environment:

```python
capture_mask = np.zeros(env.num_envs, dtype=np.bool_)
capture_mask[0] = True
captured = env.capture_snapshots(capture_mask)

restore_mask = np.zeros(env.num_envs, dtype=np.bool_)
restore_mask[3] = True
starts = [None] * env.num_envs
starts[3] = captured[0]
observations, infos = env.reset(
    options={"reset_mask": restore_mask, "snapshots": starts},
)
env.close()
```

Handles are reusable, session-local, and intentionally not pickleable. A
single masked reset can mix snapshot starts with ordinary `state_indices`;
`infos["start_source"]` distinguishes `"snapshot"` from `"environment"`.

## 🔬 Processed research infos

The original `INFO_KEYS` remain the default. Additional semantic game state is
opt-in, so the environment only decodes and returns the extra keys a caller
requests:

```python
from supermariobrosnes_turbo import AreaType, PlayerMotion

env = SuperMarioBrosNesTurboVecEnv(
    "SuperMarioBros-Nes-v0",
    state="Level1-1",
    info_filter={
        "mode": "all",
        "keys": [
            "x_pos",
            "y_pos",
            "area_type",
            "player_motion",
            "enemy_active",
            "enemy_x_pos",
        ],
    },
)
observations, infos = env.reset()
in_water = infos["area_type"] == AreaType.WATER
climbing = infos["player_motion"] == PlayerMotion.CLIMBING
```

`EXTRA_INFO_KEYS` lists the opt-in catalog and `AVAILABLE_INFO_KEYS` combines it
with the legacy keys. Explicit selections reject unknown names, remove
duplicates, and return only selected game-state keys in catalog order. The
`terminal` and `none` modes retain their existing meaning; reset lifecycle
metadata and Gymnasium `_key` masks are not game-state selections.

All selectable game-state variables are listed below in their canonical
`AVAILABLE_INFO_KEYS` order. Legacy variables are returned by default; extra
variables are returned only when explicitly named in `info_filter["keys"]`.

| Key | Set | Shape | NumPy dtype | Meaning |
| --- | --- | --- | --- | --- |
| `x_pos` | Legacy/default | `(num_envs,)` | `np.int_` | Combined horizontal world position. |
| `coins` | Legacy/default | `(num_envs,)` | `np.int_` | Legacy coin counter. |
| `levelHi` | Legacy/default | `(num_envs,)` | `np.int_` | Legacy world-number component. |
| `levelLo` | Legacy/default | `(num_envs,)` | `np.int_` | Legacy level-number component. |
| `lives` | Legacy/default | `(num_envs,)` | `np.int_` | Signed legacy life counter; `-1` signals game over. |
| `score` | Legacy/default | `(num_envs,)` | `np.int_` | Decoded decimal game score. |
| `scrolling` | Legacy/default | `(num_envs,)` | `np.int_` | Legacy horizontal-scrolling signal. |
| `time` | Legacy/default | `(num_envs,)` | `np.int_` | Decoded decimal level timer. |
| `xscrollHi` | Legacy/default | `(num_envs,)` | `np.int_` | High/page component of horizontal scroll. |
| `xscrollLo` | Legacy/default | `(num_envs,)` | `np.int_` | Low component of horizontal scroll. |
| `area_id` | Extra/opt-in | `(num_envs,)` | `np.int16` | Stable internal subarea identifier. |
| `area_pointer` | Extra/opt-in | `(num_envs,)` | `np.int16` | Current SMB area-data pointer, used to distinguish route destinations that reuse coordinates. |
| `area_type` | Extra/opt-in | `(num_envs,)` | `np.int8` | `AreaType`: `UNKNOWN=-1`, `WATER=0`, `GROUND=1`, `UNDERGROUND=2`, `CASTLE=3`. |
| `y_pos` | Extra/opt-in | `(num_envs,)` | `np.int32` | Combined world-space vertical position. |
| `y_screen_pos` | Extra/opt-in | `(num_envs,)` | `np.int16` | Screen-relative vertical position. |
| `player_motion` | Extra/opt-in | `(num_envs,)` | `np.int8` | `PlayerMotion`: `UNKNOWN=-1`, `GROUND=0`, `JUMPING_OR_SWIMMING=1`, `FALLING=2`, `CLIMBING=3`. |
| `player_power` | Extra/opt-in | `(num_envs,)` | `np.int8` | `PlayerPower`: `UNKNOWN=-1`, `SMALL=0`, `BIG=1`, `FIRE=2`. |
| `is_large` | Extra/opt-in | `(num_envs,)` | `np.bool_` | Normalized large-player hitbox/size state. |
| `x_velocity` | Extra/opt-in | `(num_envs,)` | `np.int16` | Sign-extended horizontal velocity in SMB velocity units. |
| `y_velocity` | Extra/opt-in | `(num_envs,)` | `np.int16` | Sign-extended vertical velocity in SMB velocity units. |
| `facing` | Extra/opt-in | `(num_envs,)` | `np.int8` | `Direction`: `LEFT=-1`, `NONE=0`, `RIGHT=1`. |
| `is_crouching` | Extra/opt-in | `(num_envs,)` | `np.bool_` | Normalized crouching state. |
| `is_swimming` | Extra/opt-in | `(num_envs,)` | `np.bool_` | Normalized swimming state. |
| `injury_timer` | Extra/opt-in | `(num_envs,)` | `np.int16` | Injury/invulnerability countdown in game-timer ticks. |
| `star_timer` | Extra/opt-in | `(num_envs,)` | `np.int16` | Star-power countdown in game-timer ticks. |
| `game_mode` | Extra/opt-in | `(num_envs,)` | `np.int8` | `GameMode`: `UNKNOWN=-1`, `TITLE=0`, `GAMEPLAY=1`, `VICTORY=2`, `GAME_OVER=3`. |
| `player_task` | Extra/opt-in | `(num_envs,)` | `np.int8` | `PlayerTask`: `UNKNOWN=-1`, `ENTRANCE_TIMER_SETUP=0`, `VINE_AUTO_CLIMB=1`, `VERTICAL_PIPE_ENTRY=2`, `SIDE_PIPE_ENTRY=3`, `FLAGPOLE_SLIDE=4`, `LEVEL_END=5`, `LOSE_LIFE=6`, `PLAYER_ENTRANCE=7`, `PLAYER_CONTROL=8`, `CHANGE_SIZE=9`, `INJURY_BLINK=10`, `PLAYER_DEATH=11`, `FIRE_FLOWER_TRANSFORM=12`. |
| `enemy_active` | Extra/opt-in | `(num_envs, 6)` | `np.bool_` | Normalized active mask for the six enemy/object slots. |
| `enemy_type_id` | Extra/opt-in | `(num_envs, 6)` | `np.int16` | Stable SMB object-category ID; inactive slots are `-1`. |
| `enemy_x_pos` | Extra/opt-in | `(num_envs, 6)` | `np.int32` | Combined horizontal world positions; inactive slots are `-1`. |
| `enemy_y_pos` | Extra/opt-in | `(num_envs, 6)` | `np.int32` | Combined vertical positions; inactive slots are `-1`. |
| `enemy_x_velocity` | Extra/opt-in | `(num_envs, 6)` | `np.int16` | Signed horizontal velocities; inactive slots are `0`. |
| `enemy_y_velocity` | Extra/opt-in | `(num_envs, 6)` | `np.int16` | Signed vertical velocities; inactive slots are `0`. |
| `enemy_facing` | Extra/opt-in | `(num_envs, 6)` | `np.int8` | Normalized `Direction`; inactive slots are `0` (`NONE`). |
| `loop_command_active` | Extra/opt-in | `(num_envs,)` | `np.bool_` | Whether SMB's castle loop command is active. |
| `loop_correct_count` | Extra/opt-in | `(num_envs,)` | `np.int16` | Number of accepted steps in the active castle-loop route. |
| `loop_pass_count` | Extra/opt-in | `(num_envs,)` | `np.int16` | Number of completed castle-loop passes. |

The environment may also add lifecycle metadata independently of the selected
game-state variables:

| Key | When present | Meaning |
| --- | --- | --- |
| `state_index` | Reset lanes | Active state-catalog index. |
| `start_source` | Reset lanes | Whether the lane started from the environment or a snapshot. |
| `terminated` | Terminated lanes | The lane reached a game terminal state. |
| `truncated` | Truncated lanes | The lane reached an external episode limit. |
| `_<key>` | With each emitted key | Gymnasium boolean mask identifying lanes for which that key is valid. |

Returned game-state arrays are owned copies and cannot be changed by later
environment steps. Unknown categorical engine values become `UNKNOWN = -1`.

Researchers who intentionally need unprocessed state can call `env.ram()` for
an immutable owned `(num_envs, 2048)` `uint8` snapshot. RAM addresses and byte
decoding are not part of the semantic `info` contract.

## 🏁 Train and play

```bash
smb-turbo train Level1-1
smb-turbo play
```

**Training** searches observation-free `(action, duration)` programs with
Go-Explore and the `standard` action set by default. When an explicit state is
supplied, it consumes the transition budget as an anytime improvement search,
keeping the best completed trajectory locked and publishing only higher-return
completions; pass `--stop-on-completion` to stop after the first completed path.
Go-Explore uses the score-blind `speedrun` reward by default: it charges one
point per step, gives no progress or score reward, and treats life loss as
failure, so successful trajectories rank strictly by earliest completion.
Select the score-first alternative by ID with:

```bash
smb-turbo train Level1-1 --reward-function score-first
```

The default speedrun output is `runs/Level1-1/Level1-1.zip`; the non-default
score-first output is `runs/Level1-1-score-first/Level1-1.zip`. The reward ID is
also recorded in the run configuration, metrics, and policy metadata. Every
completion is appended to `successes.jsonl`.

A new default Go-Explore run replaces the existing canonical run; custom outputs
and explicit Beam runs remain protected unless `--overwrite` is passed.
`Level1-1` writes `runs/Level1-1/Level1-1.zip`; playback uses the matching trained
policy when available and switches policies as levels change. Running
`smb-turbo play` without a state starts from `Level1-1`; pass an exact state
identifier to start elsewhere. Playback defaults to 30 FPS; pass `--fps max` (or
its `--fpx max` alias) to run without an explicit delay or renderer-vsync cap.
Run either command with `--help` for configuration options. Policy playback
defaults to `--view raw`, which displays RGB directly from its sole emulator
without grayscale conversion, cropping, resizing, max-pooling, or frame stacking;
`--view preprocessed` instead shows the transformed policy observation.

State names are exact identifiers from the configured state catalog. This
includes canonical names such as `Level1-1`, packaged variants such as
`Level2-1-clouds-easy`, and imported names such as `Custom`; shorthand and case
normalization are intentionally unsupported.

In an interactive terminal, all trainers automatically open a full-screen
dashboard with live transition, throughput, search, best-path, and event stats.
Redirected output and CI use the existing plain logs; pass `--ui plain` to
select them explicitly. Press `q` or `Ctrl-C` in the dashboard for a safe stop:
the current vector step finishes, final metrics are written, and the best
policy is saved when a candidate exists.

The checkout-compatible `uv run python train.py Level1-1` and
`uv run python play.py` entry points remain available.

To use fixed-width Beam search instead, run:

```bash
smb-turbo train Level1-1 --algorithm beam --overwrite
smb-turbo play Level1-1
```

Beam ranks completed trajectories with the same return as Go-Explore's
`score-first` reward, retains incomplete alternatives by furthest progress, and
systematically moves splice mutations from the tail toward the root while
replaying the proven suffix.
A compatible `--initial-policy` with a smaller action table is remapped by action
name into the selected table, so historical `basic` policies can seed the
`standard` search without restricting it.

Go-Explore uses the same canonical `(action, duration)` ZIP format as Beam, so
the regular playback command needs no algorithm-specific mode. It performs
trajectory finding with exact archived-state restoration and no robustification.
Go-Explore cells are keyed by level, sublevel, and the raw bytes of the native
8x8 grayscale frame after HUD masking and 3-bit quantization; horizontal position
is not part of the cell key. Keeping the 64-byte visual value directly avoids
application-level hash collisions while remaining negligible beside a snapshot.
The optional `score-first` reward ranks paths with raw game-score gains first
and charges each step `1 / (max_episode_steps + 1)`, so the entire episode's
time charge is less than one score point. Higher score therefore always wins,
while fewer steps break ties; an explicit `--step-cost` overrides the selected
reward function's default.
After the first completion, half of archived restores continue novelty-weighted
exploration and half sample underused cells across the best successful
trajectory. Success return is propagated through parent-linked archived cells,
so score improvement can mutate the whole proven route instead of only states
near the flag.

Reset-time randomization is opt-in for every trainer:

```bash
smb-turbo train Level1-1 --noop-reset-max 120
```

The value is an inclusive maximum number of seeded, lane-local raw emulator
frames advanced with no controller input after each ordinary state reset.
`0` is the default and disables the feature. The value is not multiplied by
`frame_skip`; Go-Explore archived snapshot restores remain exact and do not
receive additional NOOP frames.

Omit the training state to process all 32 canonical levels in game order:

```bash
smb-turbo train
```

The transition budget applies independently to each level. Every policy and its
artifacts are written under `runs/<State>/`; with `--output <Directory>`, each
level instead uses `<Directory>/<State>/`. Interactive campaigns keep one TUI
open and show separate progress bars for the current level's transitions and the
overall 32-level campaign. By default, the campaign advances to the next level
as soon as the current level is completed; pass `--continue-after-completion` to
keep improving each completed level until its transition budget is exhausted. A
level that exhausts its budget without completing is reported and the campaign
continues to the next level; a safe stop ends the current level and does not
start another. The default `standard` action set keeps pipe-dependent levels
searchable; an explicit `--action-set` is respected.

New default runs use `runs/<State>/` regardless of algorithm. For compatibility,
playback still discovers historical algorithm-specific directories, preferring
`runs/<State>-beam/` over `runs/<State>-jerk/`.

## 🧰 Commands

```bash
smb-turbo import /path/to/roms        # import the supported ROM
smb-turbo train Level1-1              # train one state-keyed Go-Explore policy
smb-turbo train                       # train all 32 canonical levels in order
smb-turbo play                        # play Level1-1 manually or with its policy
uv sync --frozen --extra dev --group dev  # install development dependencies
uv run maturin develop --release      # build the optimized Rust extension
make test                             # run Rust and Python tests
make test-retro-oracle                # run ROM-backed parity and policy tests
make benchmark                        # benchmark SuperMarioBros-Nes-turbo locally
make benchmark-report                 # compare SuperMarioBros-Nes-turbo with Stable Retro
uv run python scripts/benchmark_info_filter.py --rom /path/to/rom.nes  # diagnostic infos overhead
```

## 📈 Benchmark

[![SuperMarioBros-Nes-turbo versus Stable Retro median environment throughput](media/benchmark-throughput.svg)](BENCHMARKS.md)

The chart records the published `0.3.0` comparison. See
[BENCHMARKS.md](BENCHMARKS.md) for exact results, protocol, and machine details.
`benchmark_info_filter.py` is a paired diagnostic for the optional research-info
path only; its output is never eligible for autoresearch acceptance records.

## Notes

- **Scope:** This emulator supports only `SuperMarioBros-Nes-v0` on mapper
  0/NROM; it is not a general NES or Stable Retro replacement.
- **States:** Packaged states cover `Level1-1` through `Level8-4`, with
  additional variants. `state=` accepts one name, path, or byte payload;
  `state_catalog=` preloads an ordered selection for explicit per-lane resets.
- **Actions:** `Actions.ALL` and `Actions.FILTERED` accept per-button masks;
  `Actions.DISCRETE` provides Stable Retro-compatible 36-way actions and
  `Actions.MULTI_DISCRETE` exposes the three restricted button groups. Named
  metadata presets (`basic`, `standard`, `right-jump`, `basic-start`) and inline button
  tables such as `[[], ["RIGHT"], ["RIGHT", "A"]]` produce exact discrete
  spaces through `use_restricted_actions`.
- **Playback:** Play commands require a discoverable native SDL2 library and
  open local gameplay windows.
- **Contributing:** See [CONTRIBUTING.md](CONTRIBUTING.md) and follow the
  [Code of Conduct](CODE_OF_CONDUCT.md).
- **Affiliation:** This unofficial research project is not affiliated with or
  endorsed by Nintendo. See [NOTICE.md](NOTICE.md).

## Architecture

![SuperMarioBros-Nes-turbo architecture diagram](architecture.png)

## License

Code is licensed under the [MIT License](LICENSE). Third-party names, marks, and
user-supplied content are excluded; see [NOTICE.md](NOTICE.md).

