Add a game backend to fmri-gym

VerifiedSafe

Complete procedure for integrating a new game engine (backend) into fmri-gym: adapter, gym package, curriculum config, pyproject extra, tests, and replay pitfalls.

Sby Skills Guide Bot
DevelopmentAdvanced
11/2/2026
Claude CodeCursorWindsurfCopilotCodex
#fmri#gymnasium#game-engine#adapter#reproducibility

Recommended for

Automated summary

Walks through adding a new game engine (backend) or Gymnasium adapter to the fmri-gym project, from adapter code to config, tests and CI wiring.

Strengths

  • Ordered, numbered checklist covering adapter, config, pyproject extras, tests and CI matrix
  • Clear separation between game logic (Gym env) and adapter, with existing files to imitate as models
  • Treats replay reproducibility as the real acceptance criterion, not just playability
  • Calls out concrete traps such as pygame key naming and game phases having no default key map

Limitations

  • Highly specific to the fmri-gym repository and its internal conventions
  • The provided text cuts off mid-sentence on action-space handling
  • Assumes familiarity with Gymnasium, uv, pytest and GitHub Actions workflows
When to use it

Reach for it when adding, porting or wiring a game into fmri-gym and you want to cover every required step.

When not to use it

Not useful outside fmri-gym or when no game environment needs integrating.

Security analysis

Safe
Quality score86/100

The skill is pure developer documentation for adding a game-engine backend: it describes file layout, hooks, config conventions and reproducibility pitfalls, and its only commands are local test/lint/play invocations (`pytest`, `uv run`, `ruff`). There is no destructive command, no obfuscated payload, no credential or token handling, and no instruction to disable safety checks. The single supply-chain surface is the pinned git dependency on a maintainer fork, which is disclosed in-line and used for a stated determinism reason rather than concealment.

Findings
  • •Step 4 introduces a pinned third-party dependency installed directly from a GitHub fork (`crafter @ git+https://github.com/chengfanbrain/crafter.git@deterministic`); this is a legitimate determinism fix but pulls code from a non-upstream personal repository, so provenance should be confirmed before wide use.
How this analysis is made

Examples

Add a new backend
Add a new backend for the game NetHack to fmri-gym: create the adapter, the curriculum config, the pyproject extra and the tests row, then run the backend test suite.
Check for an existing engine
Does an existing fmri-gym backend like ale or gym already play this ROM out of the box, or do I need to write a new adapter?
Fix replay failure
My new backend passes playable and recorded checks but fails the replays test — what should I look at in the curriculum keys map?

name: add-backend description: Add a new game engine (backend) to fmri-gym, or wrap a game that has no Gymnasium env: the adapter, the gym/ package, the config, the pyproject extra, the tests row, and the traps that make a backend look fine and replay wrong. Use when asked to add, port or wire up a game.

Adding a backend

Read AGENTS.md first: rule 1 (the game logic lives in the gym env, not the adapter) decides where every line below goes.

The checklist

  1. Check that no existing backend already plays the game, possibly out of the box: ale takes any Atari ROM, retro any libretro core, gym any registered Gymnasium env, vizdoom any Doom scenario. If one does, all you need is a config.
  2. fmri_gym/adapters/<BACKEND>.py: module docstring (what the engine is, why it was chosen, what its observation/action spaces look like, what is not supported), then a subclass of EnvAdapter overriding only the hooks you actually need. Everything else is inherited; do not re-state the defaults. adapters/vizdoom.py, baba.py and minihack.py are the models; stay under about 150 lines, one file.
  3. fmri_gym/adapters/__init__.py: one if backend == ...: branch with a lazy import.
  4. configs/curricula/<BACKEND>/<GAME>.json: a runnable curriculum, and configs/curricula/<BACKEND>/common/README.md with any install step that is not a plain pip install and what the action indices in its keys mean. Comments in the JSON ("## ..." keys) are for what is particular to one file, and few. Lay it out like the others: one ## comment per line, the message's text one screen line per line, fixations as one-liners, and the game block on a few lines with keys (and menu) inline, not one key per line. A game with several levels gets one curriculum per level, whose game phases __include a shared configs/curricula/<BACKEND>/common/<GAME>.json (the keys, the instructions, the settings) and state only the level and its title (README, "Shared phases").
  5. pyproject.toml: one extra under [project.optional-dependencies] (and a mention in dbp / all if it belongs there), then uv lock. A game without a Gymnasium env gets one first, as its own small package under gym/<GAME>/ (pyproject.toml, README.md, <game>_gym/{__init__,env}.py, a gym.register id), listed in [tool.uv.sources]; the extra installs that package. It wraps the game, it does not contain it.
  6. tests/backends.py: a Backend row (the extra, the module that proves it is installed, one or two short smoke configs, the gym/ dirs it runs on, its external/ checkout, a ci reason if a hosted Linux runner cannot play it), and a line in .github/workflows/tests.yml's matrix unless it has one. test_configs fails until the row exists.
  7. README.md: only if the backend needs setup beyond pip install (a repo checkout, a binary, an env var).

Then play it, through the suite and by hand:

uv run pytest --backend <BACKEND>
uv run fmri-play --subject sub-test --dummy-trigger --ses 1 --run 1 --rig configs/rigs/example/laptop.json \
    --curriculum configs/curricula/<BACKEND>/<GAME>.json
uv run ruff check fmri_gym fmri_play.py agents tests

The suite's four checks (playable, recorded, replays, runs) are the acceptance test; the testing skill says how to read a failure. A backend that passes everything but replays is not done: reproducibility is the product.

Things that are easy to get wrong

  • Key names are pygame names, upper-cased, without K_ ("LEFT", "SPACE", "Z"). Add unlisted keys to _PYGAME_KEY_NAMES in keys.py rather than mapping keycodes yourself; validate_curriculum refuses a config that names a key outside that table.
  • Every game phase writes its whole keys map; there is no default. A MultiBinary env takes button indices (held keys combine), anything else takes the action itself and a real-time phase also needs the "" entry, the action for no key held. One class per action space in adapters/keymap.py. Don't add a per-backend default or a merge step: the map depends on the site's input device, not on the engine.
  • Copy observations you keep. Several envs reuse their observation buffers, so capture must .copy() anything it stores (see minihack.py). The replay check is what found nethack logging each frame's variables one frame late: a view of a reused buffer, pickled after the loop had moved on.
  • render() is not always free of side effects. An engine that draws from the same RNG its dynamics use spends a draw on every render, so one extra call shifts everything after it and the run silently stops matching its own log. Return the frame step already produced rather than rendering again; crafter's night noise is the live example, and it is why gym/crafter caches the observation and hands that back.
  • Reproducibility is the product. A block must be replayable from episode_seeds + actions. If the env has no savestate of its own, wrap it in replay_state_gym.ReplayState (gym/replay_state/: the seed and the actions since, restored by replay) and make sure reset(seed=...) really determines the episode. Test it rather than assume it: uv run python -m tests.verify_replay --curriculum <config> records a block with the random policy, replays it in a second process under another PYTHONHASHSEED and compares every frame, every sound chunk and every savestate anchor bit for bit. A second process because the usual culprit is hash order, which is fixed for the life of one interpreter and would let a replay inside the recording process pass. Crafter passes every surface check (a seed argument, a seeded RandomState, deterministic worldgen) and still diverges, because one creature list is built from a Python set and the despawn pick therefore follows object id(), which moves with PYTHONHASHSEED. Sorting that list is the whole fix, and it lives in a fork the env package pins (gym/crafter/pyproject.toml: crafter @ git+https://github.com/chengfanbrain/crafter.git@deterministic), not in the adapter: when the bug is in the engine, fix the engine and pin the build, or the rig starts owning game behaviour.
  • Sound is part of the record. An engine that makes sound returns this step's PCM from sound() (the contract is in EnvAdapter.sound; retro.py is a two-line example); the loop plays it and logs it, and the replay check compares it chunk for chunk. ViZDoom's first episode after init() sounded different from one after new_episode(), which is why vizdoom_gym begins every episode the same way.
  • Turn-based games (grid worlds, puzzles) need "is_turn_based": true in the config.
  • Slow real-time games (a grid world that must keep ticking, so is_turn_based is out) need "has_latched_keys": true: the default real-time path polls held keys, and at a few frames per second a tap that begins and ends between two frames is never seen.

From the file to a first result

The download contains exactly the content displayed on this page. This copy may differ from the latest version in the source repository.

View the source and supporting files

This download contains SKILL.md only. Any scripts, references, templates and dependencies are not included. Get the required files from the source, preserving their directory structure, before continuing.

1. Download the file

Download SKILL.md

From the root of a test project, save the file as .skill-downloads/add-a-game-backend-to-fmri-gym-mvg6txds/SKILL.md, or use the command below. This folder is for inspecting the skill before installation.

Command for macOS/Linux or WSL: requires a POSIX shell and curl. On Windows without WSL, use the download button and create the folder manually.

mkdir -p '.skill-downloads/add-a-game-backend-to-fmri-gym-mvg6txds' && curl --fail --show-error --location 'https://skills-guides.com/api/skills/add-a-game-backend-to-fmri-gym-mvg6txds/download' --output '.skill-downloads/add-a-game-backend-to-fmri-gym-mvg6txds/SKILL.md.part' && mv '.skill-downloads/add-a-game-backend-to-fmri-gym-mvg6txds/SKILL.md.part' '.skill-downloads/add-a-game-backend-to-fmri-gym-mvg6txds/SKILL.md'

2. Check prerequisites

Read SKILL.md before using it. If it references scripts or external services, inspect them and prepare the required access in your test environment.

No recognized prerequisites section in this file. This does not mean it is self-contained: libraries, tools and access requirements may be mentioned elsewhere in the content or source repository.

The platforms on this page are catalog indications. The paths below follow tool documentation; they do not prove this skill has been run in those tools. No minimum tool version is confirmed for this listing.

3. Load the skill

This file does not provide a usable name/description header for this native procedure. It remains available to download and inspect; use the manual method below.

Manual method or another tool: attach SKILL.md to a conversation, or open it in an agent that can read files. Include any required supporting resources. No universal folder is recognized by every tool.

4. Try a first task

Use a short, synthetic input: an API endpoint, a small document or an interface brief. In a new conversation, attach the file or give the agent its path, then adapt this request:

Read .skill-downloads/add-a-game-backend-to-fmri-gym-mvg6txds/SKILL.md and apply its instructions to this example: [describe a test input]. Identify any missing files, dependencies or access. Produce the result requested by the skill without inventing data or running unreviewed scripts.

Expected result: output matching the skill instructions, or a precise list of missing prerequisites. A read confirmation alone does not validate a first use. If an external resource is unavailable, that path remains untested.

Related skills