Skip to content

1. Getting Started

This chapter takes you from a fresh clone to a completed search and a replayed solution. The whole walkthrough uses the in-repo TestEmulator, so you need no game ROM and no emulator submodule — it works on any machine.

Prerequisites

JaffarPlus is a C++20 project built with Meson and Ninja. You will need:

Dependency Purpose Notes
A C++20 compiler Building GCC or Clang. The binaries are compiled with -march=native.
Meson + Ninja Build system pip install meson ninja works.
libnuma (+ headers) NUMA-aware memory allocation The jaffar binary links against it directly (dependency('numa')). On Debian/Ubuntu: apt install libnuma-dev.
NCurses Console UI (optional) On by default; disable with -DuseNCurses=false.
Emulator-specific libs Per core Some cores (e.g. QuickerSDLPoP) need SDL2. The TestEmulator needs nothing extra.

A real game search additionally needs the game's data (a ROM, disk image, or game files) and usually an initial state file — these are not shipped with the repository. The TestEmulator used in this chapter needs neither.

Getting the source

git clone https://github.com/SergioMartin86/jaffarPlus.git
cd jaffarPlus

JaffarPlus uses git submodules under extern/ — the shared jaffarCommon library plus one submodule per emulator core. You only need the submodules for the core you intend to build:

# Always needed:
git submodule update --init extern/jaffarCommon

# Needed only for the core you build, e.g. for Prince of Persia:
git submodule update --init extern/quickerSDLPoP

The TestEmulator is in-repo and needs no submodule beyond jaffarCommon.

Building

JaffarPlus is configured per build directory. A build directory is tied to a single emulator core (and, optionally, a single game), so you typically keep one build directory per core you work with.

meson setup build -Demulator=TestEmulator
ninja -C build

This produces two executables in build/:

  • jaffar — the search engine. Takes a .jaffar configuration file and searches for a solution.
  • jaffar-player — replays and inspects solutions; can render frames and capture screenshots.

Choosing an emulator core

The -Demulator= option selects which emulation core is compiled in. The available choices are:

QuickNES  QuickerNES  QuickerNESArkanoid  QuickerGPGX  QuickerSDLPoP  QuickerSnes9x
QuickerStella  Atari2600Hawk  QuickerSMBC  QuickerNEORAW  QuickerRAWGL  QuickerArkBot
QuickerMGBA  QuickerGambatte  QuickerDSDA  PipeBot  TestEmulator

Each core builds the games written for it. For example:

meson setup build-nes  -Demulator=QuickerNES     # all NES games
meson setup build-pop  -Demulator=QuickerSDLPoP  # Prince of Persia

To switch an existing build directory to a different core, reconfigure it:

meson configure build -Demulator=QuickerNES
ninja -C build

Building a single game

A core can have many games, and rebuilding all of them when you only care about one is wasteful. Use -Dgame= to compile just one, identified by its header file stem:

# Compile only the 'sprilo' game of the QuickerNES core
meson setup build-sprilo -Demulator=QuickerNES -Dgame=sprilo
ninja -C build-sprilo

-Dgame= matches the game's source file name (case-insensitive); leaving it empty (the default) builds every game of the selected core. Game and emulator registration is fully automatic — see Adding a Game — so there is nothing to "wire up" by hand.

The repository ships a self-contained puzzle, docs/examples/gridwalker.jaffar, that runs on the TestEmulator. The "game" is a cursor on a 5×5 grid that can move up, down, left or right; the goal is to reach the bottom-right corner (4,4) from the top-left (0,0). The optimal solution is exactly 8 moves, which makes it a perfect first run.

./build/jaffar docs/examples/gridwalker.jaffar

The engine explores the reachable grid positions and, because best-first search returns a shortest path, finds an optimal 8-move solution. By default it stops on the first win ("End On First Win State": true) and writes the solution to /tmp/jaffar.gridwalker.best.sol (set by "Best Solution Path").

Reading the output

A run prints a banner describing the loaded game and emulator, then a periodic status report, and finally an exit line. The most important fields:

  • Step — the current search depth (how many inputs deep the frontier is).
  • State count / database usage — how many distinct states are being held, and how full the state database is.
  • Best reward / best solution length — the reward of the best state found so far and how many inputs it took to reach it.
  • Exit Reason — one of:
  • Solution found. — a win state was reached.
  • Engine ran out of states. — the frontier emptied before a win (no solution reachable under the given inputs/rules, or the state database was too small).
  • Maximum step count reached. — hit the "Max Steps" limit first.

Validating a configuration without running

Before committing to a long search, check that a configuration parses and builds the full engine (driver, emulator, game, rules, inputs) with --dryRun:

./build/jaffar docs/examples/gridwalker.jaffar --dryRun

A dry run loads and validates the entire configuration but does not initialize NUMA, load trace files, or run the search — so it is fast and host-independent. On success it prints Finished dry run successfully.; on a malformed or incomplete configuration it reports the offending key. This is the same check CI runs against every documented example (see the accuracy note).

Tip — unrecognized keys are rejected. The parser reads keys by name and fails on any key it does not recognize, in every section, reporting [Error] Unrecognized key(s) in <section>: '...'. A misspelled or stale key is a hard error, not a silent no-op — so --dryRun catches typos immediately. If a key is rejected, check its spelling and nesting against the Configuration Reference.

Replaying a solution

A .sol file is a sequence of inputs. Replay it with jaffar-player:

./build/jaffar-player docs/examples/gridwalker.jaffar /tmp/jaffar.gridwalker.best.sol \
    --reproduce --unattended --exitOnEnd
  • --reproduce plays from the start of the solution.
  • --unattended skips the interactive prompt (good for scripts/CI).
  • --exitOnEnd quits when the last input is consumed.

For cores with a renderer you can watch the playback in a window (omit --disableRender), step frame-by-frame interactively, or capture frames to disk with --screenshotDir. See the Tooling Reference for every player flag and the video-rendering helper.

Where to go next