Getting started with the Arcade Lab#

The Arcade Lab is a crowd of game timelines. Each walker is a complete emulator state carrying the current game state and its timeline. It tries a random sequence of button presses for a little while, earns a game-specific reward, and may be copied by another walker. The swarm reveals the complete story through many alternatives being tried, compared, and copied, so one lucky attempt becomes one part of a larger experiment.

This page gets you from a checkout to a running browser demo, then makes one small Mario run reproducible. For the larger picture—why Wave, Graph, FMC, and Jump Wave show different things—see Arcade Lab.

Arcade Lab overview showing the Mario game screen, sidebar settings, swarm statistics, level map, and reward plots.

Fig. 34 The Arcade workspace: one leading game view, the controls that define the experiment, the swarm overlaid on a level map, and plots that show how the population is changing.#

Install prerequisites and submodules#

There are two computers involved here. Your machine builds the C++ emulator and the swarm; the browser later runs those compiled pieces as WebAssembly. For a complete build you need Git, a C++17 compiler, CMake 3.16 or newer, Python 3, and an Emscripten SDK. You also need a browser with WebAssembly threads and SharedArrayBuffer support. Node.js and npm are optional for the browser smoke test and for creating the encrypted ROM vault.

The first checkout and the first Emscripten build may need internet access. The ROM itself is a separate matter: use a ROM you are permitted to use. Local development can use the plaintext files described below; a deployment can serve the encrypted vault instead.

The Arcade CMake project uses three submodules. nes-py supplies the NES core, ALE supplies Atari, and stable-retro supplies the Genesis core used by Sonic. Initialize them from the repository root:

git submodule update --init --recursive
git submodule status -- \
  fractal-gas-web/third_party/nes-py \
  fractal-gas-web/third_party/ale \
  fractal-gas-web/third_party/stable-retro

Warning

The C++ build can appear to be configured correctly while a submodule still awaits initialization. A healthy submodule supplies headers and source files under third_party/; check the submodule status whenever those files await initialization, before debugging CMake.

Build the native and WebAssembly artifacts#

The native and browser builds answer different questions. The native build is a quick way to compile the command-line runner and the dependency-free tests. The WebAssembly build is the one the page loads. You can build only the latter for a browser session, but the native tests are a useful sanity check when changing the C++ core.

Native build#

Run this from the repository root. The commands enter fractal-gas-web, create a Release build, and compile both fg_tests and fg_cli. The test suite runs its ordinary tests from the build artifacts alone; the FG_ROM invocation adds the NES smoke tests using the ROM shipped by the nes-py submodule.

cd fractal-gas-web
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/fg_tests
FG_ROM=third_party/nes-py/nes_py/tests/games/super-mario-bros-1.nes \
  ./build/fg_tests
./build/fg_cli --rom <smb.nes> --n 64 --iters 200 --seed 7 --cumulative --elite 2

fg_cli is the native experiment runner, while the browser launcher is built separately below. Give fg_cli a ROM with --rom, and it measures the C++ algorithm in a terminal, independently of a web page. The browser target writes its runtime modules into fractal-gas-web/web/.

WebAssembly build#

First activate your Emscripten SDK. The embuilder line prepares the Emscripten zlib port used by the ALE and Genesis portions of the build. Then emcmake configures the same CMake project for WebAssembly; -pthread is part of the Arcade target because the browser runs emulator work in parallel workers.

source /path/to/emsdk/emsdk_env.sh
cd fractal-gas-web
embuilder build zlib
emcmake cmake -B build-wasm
cmake --build build-wasm -j

The main build produces web/fractal_gas.js and web/fractal_gas.wasm; the Genesis path also produces the web/retro_shim.js and web/retro_shim.wasm worker module. The shell finds emcmake and embuilder once the SDK environment is active; re-run the source command there to activate it.

Serve the app with cross-origin isolation#

A WebAssembly module works together with the browser’s origin controls. The browser must be told that the page and its embedded resources belong to a controlled origin before it will allow the shared memory used by WebAssembly threads. The repository therefore contains a small server that launches index.html with those controls in place.

From fractal-gas-web, run serve.py and open the /web/ route:

cd fractal-gas-web
python3 serve.py
# Or choose another port:
python3 serve.py 8091

With the default command, open http://localhost:8000/web/arcade.html. The server adds Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp, and it serves .wasm and module files with the MIME types the browser expects.

The page checks crossOriginIsolated before it initializes the emulator. When the status says Not cross-origin isolated, correct the active URL or server: stop that server, start serve.py, and reload the page. A plain python3 -m http.server serves files with its standard headers, while this app gets its required headers from serve.py. The page includes a COI service-worker fallback for hosting environments that provide these headers through a service worker; local development should use serve.py directly.

Capture repeatable documentation screenshots#

The documentation capture is a small experiment with a fixed camera: it opens the real browser app, applies its scripted settings, runs a few iterations, and photographs the resulting workspace. From the repository root, run:

ARCADE_DOC_URL=http://127.0.0.1:8091/web/arcade.html ARCADE_SCREENSHOTS=docs/_static/arcade_lab npm --prefix fractal-gas-web run capture:arcade-docs

The command writes the PNGs beside the existing Arcade figures and updates docs/_static/arcade_lab/capture-manifest.json. The manifest is the lab notebook for the pictures: it records the capture name and note, viewport, selected game, algorithm, observation and level, the relevant run parameters, iteration or played-frame counts, and the final status. This record makes each screenshot repeatable and evidence-backed.

The capture uses the local plaintext ROM fixtures already described above. It reuses those existing files and leaves the ROM files unchanged. Before running it, start the cross-origin-isolated serve.py server on port 8091 and build the browser artifact so the served WebAssembly and JavaScript files exist.

Load ROMs locally or from the encrypted vault#

The page looks for a ROM in a deliberate order: a plaintext file next to the page, this browser’s IndexedDB cache, and then the encrypted blob in web/roms-enc/. A local checkout can read the plaintext file directly, while a hosted copy can keep plaintext ROMs out of its distribution.

For local development, the browser names are:

  • Mario: fractal-gas-web/web/test-rom.nes

  • Sonic: fractal-gas-web/web/sonic.rom

  • Atari: fractal-gas-web/web/roms/atari/<game>.bin

The NES submodule includes a test Mario ROM. If you are using that permitted test asset, copy it to the filename the page requests:

cd fractal-gas-web
cp third_party/nes-py/nes_py/tests/games/super-mario-bros-1.nes web/test-rom.nes

On a deployment, the committed encrypted files under web/roms-enc/ are the fallback. Enter the password in Unlock ROMs when the page asks. One password unlocks the vault; the browser decrypts the ROM with Web Crypto and remembers the successful password and decoded ROM in IndexedDB for later visits. The decrypted bytes are passed to the local worker that owns the emulator.

If you are preparing that vault from plaintext files, run the repository’s helper from fractal-gas-web:

FG_ROM_PASSWORD='choose-a-password' node tools/encrypt-rom.mjs --all

Sonic has one extra escape hatch. Select Sonic, then choose a file in Sonic ROM (.md) after the local and encrypted sources have been checked and the browser still needs a usable ROM. The input accepts .md, .bin, .gen, and .smd; the file is read in the browser and stored in that browser for future visits. The Arcade UI keeps the file in the browser. After the ROM is ready, choose Sonic’s Zone and Act. Changing either start-level selector initializes a new run.

Make a reproducible first Mario run#

We want the first experiment to be small enough to understand and specific enough to repeat. The important phrase is same ROM, same settings, same seed. The seed fixes the random choices made by the swarm; your processor may run at a different wall-clock speed from somebody else’s machine. The UI starts with Mario, Wave, Coords, World 1, Stage 1, and seed 7. Set the values explicitly so the experiment is written down and easy to recall.

  1. Open /web/ and wait for the status to say Ready - press Start. When the page shows Unlock ROMs, enter the vault password. Place web/test-rom.nes whenever the Mario ROM needs a local file.

  2. Select Mario, Wave, and Coords. Set World to 1 and Stage to 1.

  3. In Swarm, set Walkers (N) to 48, Seed to 7, and Elite walkers to 2. In Kinetics, set dt min to 6 and dt max to 30. Leave Distance coef and Reward coef at 1.0.

  4. In the Mario Coords controls, set Visit reward to Off, Visit pooling (px) to 5, Erase coef to 0.05, and Visit coef to 1.0. Wave starts with this setting Off; naming it here matters because the visit term changes the selection signal while the game remains the same.

  5. Press Start. Wave advances every walker, updates the screen of the leading walker, draws the swarm on Level map — swarm, and fills the cumulative-reward, virtual-reward, clone, alive, and frame-skip plots.

  6. Let a few iterations accumulate, then press Pause. Record the seed and settings with any observation you want to keep. The Env frames statistic reports the number of frames actually emulated; average frame skip provides a separate estimate.

  7. Press Reset, then Start again. Reset returns to the initial Mario state with the same active settings and seed, clears the readouts, plots, and map overlay, and leaves the run paused until you press Start.

Expect the walkers to explore multiple directions. This behavior shows the map working as intended: the swarm is exploring alternatives, and dead walkers are replaced by cloning on a later iteration. Read Cumulative reward as game progress and Virtual reward as the signal used to decide which timelines get copied; they answer different questions.

Super Mario Bros. level map with magenta alive-walker dots, gray dead-walker dots, and a gold ring around the leading walker.

Fig. 35 The Mario map shows the whole selected level with the current swarm overlaid. Alive walkers are magenta, dead walkers are gray, and the gold ring marks the best walker. It depicts many emulator states across the population and captures the evolving search, while a single playthrough represents a separate trajectory.#

Use Start, Pause, and Reset#

The three buttons control the run state; the selected settings define the experiment.

Start begins the worker loop, or resumes it after a pause. Pause stops new iterations while preserving the current swarm; for FMC and Jump Wave, it also preserves the current search or committed trajectory. Reset stops the loop and calls the emulator’s reset operation, then clears the browser’s run readouts, plots, map state, and displayed frame. It keeps the selected console, level, algorithm, and settings, so it is the button to use after an all-walkers-dead stop or a completed game.

Wave and Graph show the leading search walker. FMC and Jump Wave show the one game whose actions have been committed, while their plots still describe the planning population. That is why the screen title changes from Best walker to Played game when you select a planner.

Know which settings restart and which are live#

Here is the distinction that saves the most confusion. A restart-required setting changes the shape or identity of the state being copied: changing the number of walkers, for example, requires the UI to construct a new population. The UI reinitializes the run when such a control changes.

The live controls modify the next planning steps and preserve the past. In particular, a reward-term change applies to reward earned from then onward; reward already banked by a walker keeps its earlier weighting.

Restart required

Live while the run exists

Console and game: Mario, Atari game, Sonic, or Montezuma

Distance coef and Reward coef

Start level: Mario World/Stage or Sonic Zone/Act

Visit coef, Visit reward, Visit pooling, and Erase coef when those controls are visible

Algorithm: Wave, Graph, FMC, or Jump Wave

dt min and dt max

Observation: RAM, RGB, Gray, or Coords

Elite walkers for Wave, FMC, and Jump Wave

Walkers (N), or Graph’s Max walkers

Planner horizon; Jump Wave’s Stop at first bifurcation and Maximum search horizon

Seed

Mario, Sonic, and Montezuma reward-term sliders

For FMC and Jump Wave, changing a live planner or fitness setting clears any pending plan and searches again from the currently committed game. The current game state remains in place. For Wave and Graph, the existing swarm continues and uses the new live values on subsequent iterations.

The Graph has a special vocabulary: the Swarm number is shown as Leaves (start = min leaves), and Max walkers is its node cap. Graph also turns visit counting on by default in Coords mode for games with maps; Wave begins with visit counting set to Off by default. These changes affect behavior, so switching algorithms restarts the run and resets the interpretation of the population.