Troubleshooting¶
Eleven things go wrong. The first seven come in roughly the order a new install meets them. Problems 8 to 11 turned up when the install was followed on a 4-CPU Linux machine with no GPU and no display, on 2026-09-27. Each one below gives the message you actually see, what it means, and what to type next.
Every message on this page was copied from a real run, except where a line says otherwise.
The commands are written for Windows PowerShell, which is the default shell on Windows 10 and 11.
On macOS and Linux use .venv/bin/python in place of .venv\Scripts\python, forward slashes,
cp for Copy-Item, and export X=Y where a line sets a variable.
The two commands that answer most questions
Run these from your RoyaleGym folder. The first says whether the Rust engine is built and
importable. The second prints a short hash of the data the engine was built with.
..\.venv\Scripts\python -c "import royalegym; print(royalegym.core_available())"
..\.venv\Scripts\python -c "from royalegym import RustEngine; print(RustEngine.build_digest())"
True from the first one means the engine is there. False means skip to problem 5. If the
second one raises instead of printing a hash, that is problem 1.
1. The engine is older than the data files¶
What you see. A RuntimeError, the moment you construct RustEngine():
RuntimeError: royalesim was built against different data; rebuild with `maturin develop --release`:
calibration time.TICK_MS: built 50, now 51
The last line names what differs. There is one line per difference. You may instead see a line
like arena.json differs from the one compiled in (...).
What it means. The engine compiles the constants file and the arena into itself when you
build it. So the build carries a copy. If you edit data/calibration.json, or regenerate
data/derived/arena.json, the copy inside the engine and the file on disk stop agreeing.
RoyaleGym refuses to start rather than run a battle whose rules are half old and half new.
There is a second error that looks like this one, about the tiles a tower forbids:
RuntimeError: the Rust engine and the action mask disagree on troop territory; rebuild with
`maturin develop --release` after regenerating cards.json:
That one has a different cause. The engine and RoyaleGym are reading two different
cards.json files. The engine reads the one in the RoyaleSim folder it was built in. RoyaleGym
reads the one under ROYALESIM_DATA_DIR, or under ../RoyaleSim/data when that is not set. Make
them the same file: point ROYALESIM_DATA_DIR at the data folder of the checkout the engine was
built in, or build the engine in the checkout whose data you want.
What to do. For the first error, build again. From your RoyaleSim folder:
Measured from a fresh clone
The release build has now been run from a fresh clone: 163 s and about 1 GB of memory on a 4-CPU Linux machine on 2026-09-27.
The READMEs say to give it a few minutes and some free memory. A debug build from fresh clones took 2 minutes 38 seconds on 2026-09-22.
Order matters here and it catches people out. Generate the data first, build second, because
the build copies the arena file into the engine. The card table is different. Every time you
create an engine, it reads data/derived/cards.json from the RoyaleSim folder it was built
in. Putting a different table at that path there changes the cards with no rebuild. Pointing
ROYALESIM_DATA_DIR somewhere else does not change which card table the engine reads.
2. The data files were never generated¶
What you see. A FileNotFoundError naming cards.json. The message spells out the fix:
FileNotFoundError: ...\data\derived\cards.json is absent. It is generated, and the generated files are not in the repository. In the sibling RoyaleSim checkout run:
python tools/extract_cards.py --vintage 2018
python tools/extract_cards.py --vintage 2018 --out data/derived/cards.json
The README's Install section has every step in order, and the data has to be extracted BEFORE the engine is built. (data dir: ROYALESIM_DATA_DIR or ...\RoyaleSim\data)
The two ... are the full folder path on your machine. Everything else is the real message, from
RoyaleGym ecbb80b on 2026-09-27. If arena.json is missing too, you see that first. With
MockEngine it is the same message naming arena.json, with python tools/extract_arena.py as
its command. With RustEngine it is a plain No such file or directory naming arena.json.
What it means. A clone carries only cards-15.535.json in data/derived/; the lines below
generate the rest. A fresh clone has no cards.json, no arena.json and no globals.json.
Generating them is a mandatory install step, not an optional one.
Note the third line. The error message above is RoyaleGym's own, and it still tells you to
extract the 2018 table over cards.json. That was the recipe before RoyaleSim committed its
derived 15.535 table; the current install copies instead. Follow the block below rather than the
quoted message.
What to do. Run all four generator commands, from your RoyaleSim folder. On macOS and Linux
the copy line is cp data/derived/cards-15.535.json data/derived/cards.json.
..\.venv\Scripts\python tools\extract_arena.py
..\.venv\Scripts\python tools\extract_cards.py --vintage 2018
Copy-Item data\derived\cards-15.535.json data\derived\cards.json
..\.venv\Scripts\python tools\extract_globals.py
Then build the engine, as in problem 1. An earlier version of these commands, which put the 2018 table where the copy line now puts the 15.535 one, was run on fresh clones of the four repos on 2026-09-22 with a debug build, and RoyaleGym's pytest suite gave 385 passed, 0 skipped. With the commands as they are now, six comparisons between the two engines skip on purpose. Problem 6 explains why.
Keep --vintage 2018 on the extract_cards.py line, and keep the copy line
The extract line writes data/derived/cards-2018.json, which a Rust test loads by that
name. Without the flag the extractor asks for card data that is not shipped. The
Copy-Item line puts the committed 15.535 table at data/derived/cards.json, which is
the file the engine loads. You want both.
3. extract_cards.py fails with no --vintage¶
What you see. Running the extractor with no flag, on a clone:
missing .../data/raw/cr-15.535.29/csv_logic: decode the 15.535.29 assets first (tools/decode_sc_assets.py)
The ... is your folder path.
What it means. With no flag the extractor builds the newer of the two card tables, and that
one needs a client asset pack that is not redistributed. A public clone does not have it. The
2018 tables ARE tracked in the repo, 23 files of them (counted on 2026-09-22), so
--vintage 2018 is self-contained and works everywhere.
What to do. Add --vintage 2018, as in problem 2.
What you give up. Nothing you need for writing a bot. The copy line in problem 2 puts the
committed 15.535 table at cards.json, so the engine, the examples and both of RoyaleSim's suites
run on it, tests/levels.rs and tests/jump16402.rs included. What needs the asset pack itself
is rebuilding that table, and two RoyaleSim tools: tools/check_data.py with no --vintage (use
--vintage 2018 on a clone) and tools/mechanic_register.py. On a clone they stop at once and
ask for the pack.
4. The viewer shows nothing¶
What you see. RoyaleViser opens, and stays empty. Your training loop or your env script is clearly running.
What it means. Almost always, nothing is publishing. The correction that catches most people:
A plain ClashParallelEnv does not read ROYALEVISER
Setting the ROYALEVISER environment variable and constructing ClashParallelEnv on its own
does nothing at all. Only ClashSelfPlayVecEnv reads that variable. A single env publishes
only when you hand it a publisher.
This was checked. With ROYALEVISER=127.0.0.1:9870 set,
ClashParallelEnv(engine=RustEngine()).viser is None, while
ViserPublisher.from_env() returns a publisher.
What to do. For one env, hand it a publisher yourself:
from royalegym import ClashParallelEnv, RustEngine
from royalegym.viser import ViserPublisher
env = ClashParallelEnv(engine=RustEngine(), viser=ViserPublisher()) # 127.0.0.1:9870
print("publisher:", env.viser is not None, "attached:", env.viser.attached)
For self-play, set the ROYALEVISER variable in the shell the training run starts from, then
build ClashSelfPlayVecEnv(8). In PowerShell:
In cmd it is set ROYALEVISER=127.0.0.1:9870. On macOS and Linux it is
export ROYALEVISER=127.0.0.1:9870. A set line typed into PowerShell or bash sets nothing, and
the viewer stays empty. ClashSelfPlayVecEnv(8) binds one publisher and gives it to game 0.
That is deliberate. A viewer watches one battle and holds one port, so eight envs each grabbing
that port is an address-already-in-use error.
Then start the viewer in another terminal:
Checked without a window
On 2026-09-24 this command was run with the display switched off, on the machine that wrote this page rather than a clean install. It attached: first-bot's streaming program, started after it, sent it 3,528 frames. Nobody has yet watched the window itself from a clean install.
Two more reasons a viewer stays empty, in the order worth checking:
attached: Falseabove is normal before the viewer starts. The publisher sends nothing until a viewer says hello, and stops again about three seconds after it goes quiet. So start the viewer, then look again.- With self-play, only game 0 is published. Games 1 to 7 are invisible on purpose.
5. ImportError when you construct RustEngine()¶
What you see.
ImportError: royalesim is not built (No module named 'royalesim'); run `maturin develop --release` in the sibling RoyaleSim checkout (../RoyaleSim) with the workspace venv active. The data has to be extracted BEFORE that build: arena.json is compiled in, and cards.json is read each time an engine is constructed; the "Install" section of the RoyaleGym README.md has both steps in order.
That is the message at RoyaleGym ecbb80b, 2026-09-27.
What it means. Exactly what it says, with one wrong turning in it. The Rust engine was never
built into the venv you are running, or you are running a different Python from the one you built
into. Note that import royalegym itself still works. The package is designed to import without
the engine.
Read the middle clause, not just the command
The part people skip is that the data has to be generated before the build. The build
copies arena.json into the engine, so on a clone with no generated data it stops. With
the arena generated but not the cards, the build works and the engine fails later, when it
looks for cards.json. Either way, run the four commands in problem 2 first, then build.
What to do. Either build it, which is the maturin develop --release line in problem 1, or
carry on without it for now:
MockEngine is a plain Python stand-in. It runs the whole API, so you can write and debug a
reward function against it. It is not a second simulator: spells resolve instantly, there are no
stuns or knockbacks, and cards run at their base level. Anything you conclude about game
behaviour from MockEngine is about MockEngine.
If you are unsure which of the two you have, run the core_available() line at the top of this
page.
6. Tests that skip instead of failing¶
What you see. Green output with an s in it, and no failures:
..\.venv\Scripts\python -m pytest -q tests/test_rust_engine.py -k test_mock_and_rust_agree_on_setup_state -rs
sssss [100%]
=========================== short test summary info ===========================
SKIPPED [5] tests\test_rust_engine.py:704: the two engines are reading different card tables, so this comparison would measure the DATA and not the engines. A SKIP IS NOT A PASS -- to run it, the engine's cards.json has to be the 2018 table: it reads data/derived/cards.json in the RoyaleSim checkout it was built in (C:\...\RoyaleSim\data\derived\cards.json), each time one is constructed, and ROYALESIM_DATA_DIR does not move it. Regenerate that file with `python tools/extract_cards.py --vintage 2018 --out data/derived/cards.json` in that checkout; no rebuild is needed. cards.json vintage '15.535.29 client (2026, LIVE build family)' vs MockEngine's 'retroroyale-2018'. Differences (rust/mock) -- count: Goblins 4/3
5 skipped, 96 deselected in 1.81s
That run was RoyaleGym ecbb80b on 2026-09-27. The line number and the deselected count move as
tests are added.
What it means. A skip is not a pass. The check above compares the two engines against each other. It can only say something about the ENGINES when both are reading the same card table. On a machine where they are not, it would be measuring the difference between two card tables instead, so it steps aside and says why, naming both tables.
Add -rs to any pytest run to see the reason for every skip. Without it you get a letter.
What the two engines actually disagree about
MockEngine is an independent Python reading of the same card data, not a copy of the Rust
engine, which is what makes comparing them worth anything. On the tracked 2018 table they
agree across the measured set.
The one difference this check reports when the two engines read different card tables, which is every install that follows the recipe, is the unit count of Goblins: 4 in the 15.535 table, 3 in the 2018 one. That is a difference between the two card tables rather than between the two engines, which is exactly why the comparison steps aside instead of failing.
It is worth knowing before you meet it. If you compare a Goblins battle across the two engines, the unit counts will not line up, and nothing is broken. The example deck used elsewhere in these docs is drawn entirely from the 18 cards whose behaviour is checked against recordings, and Goblins is not one of them.
What to do. Nothing. The Install recipe puts the 15.535 table at cards.json, so the
compiled engine reads that table while MockEngine reads the 2018 one. Six comparisons like
this one skip on every normal clone, and that is correct. They still run in RoyaleSim's
cross-repo job, with the 2018 table on both sides. Before the recipe switched tables, fresh
clones on 2026-09-22 ran them and passed: RoyaleGym's pytest suite gave 385 passed, 0 skipped, on a
debug engine build.
If you see skips you did not expect, read the reason before you trust the green. Skips also happen when the engine is not built at all, which is problem 5. A few tests skip when Node.js is not on your PATH or RoyaleViser is not installed. And some skip when a test needs recordings of real matches, which are private and not distributed.
7. One venv, two checkouts, and the card count changes under you¶
What you see. Numbers that move for no reason between runs. The clearest symptom is the size of the card catalogue changing:
Run that, work in a second checkout for an hour, run it again, and get a different number.
What it means. maturin develop installs the engine INTO the venv it is run from. If two
checkouts share one venv, building in the second one swaps the engine out from under the first.
Everything still imports. Everything still runs. It is just a different engine now, built from
different data, and nothing announces it.
This is the worst failure on the page, because it does not look like a failure.
What to do. Keep one venv per checkout, or, for the second checkout, build a wheel and put it in a throwaway venv instead of installing into the shared one:
UNVERIFIED
Nobody has run this from a clean install yet. The wheel build is the recommended workaround, not something this project has timed.
The build hash at the top of this page is the cheap check, but it only covers the calibration and arena built into the engine. It does not cover the card table, and it cannot see the engine's Rust code at all: two engines built from different code and the same data print the same hash. So run three checks at the start of a run and again at the end, each as its own command: the hash, the card count above, and this one, which tells two compiled engines apart:
..\.venv\Scripts\python -c "from royalegym.rust_engine import engine_binary_digest; print(engine_binary_digest())"
If any of them changed, something changed underneath you.
8. The engine compiles, then the install refuses your Python¶
What you see. maturin develop --release compiles the engine for several minutes, then stops
at the install step with a line like this one, from a 4-CPU Linux machine on 2026-09-27:
What it means. The venv was made with a Python older than 3.12. python -m venv .venv uses
whichever Python python is, and on that machine it was 3.11. Every package in the stack needs
3.12 or newer.
What to do. Delete the .venv folder and make it again with Python 3.12, from the Royale
folder. Check the version before you build:
On macOS and Linux the first line is python3.12 -m venv .venv. The second line must print 3.12
or newer. Then run the install again from the step after the venv: the pip line, the build, and
the packages.
9. The torch install is gigabytes on a machine with no NVIDIA GPU¶
What you see. On Linux, pip install -e "RoyaleLearn[torch]" downloads a CUDA build of
torch and about 20 NVIDIA packages. On a 4-CPU Linux machine with no GPU on 2026-09-27 the venv
grew to 5.5 GB, and torch.cuda.is_available() printed False.
What it means. The default torch download on Linux is the CUDA build, whether or not the machine has a GPU.
What to do. No NVIDIA GPU? Install CPU torch first, then pip install -e "RoyaleLearn[torch]"
as before. From the Royale folder:
On macOS and Linux that is
.venv/bin/python -m pip install torch --index-url https://download.pytorch.org/whl/cpu. The
default torch download pulls about 5 GB of CUDA wheels (measured on a 4-CPU Linux machine on
2026-09-27).
10. The viewer opens no window on a machine with no display¶
What you see. python -m royaleviser runs and no window opens. Nothing says so. On Linux you
may see a line about XDG_RUNTIME_DIR and several ALSA error lines. Without --seconds it keeps
running until you stop it.
What it means. There is no screen to show the window on, and the viewer gives no warning.
What to do. Tell SDL, which draws the window, to use no display and no sound, and save a PNG
with --shot. On Linux, from the Royale folder:
export SDL_VIDEODRIVER=dummy
export SDL_AUDIODRIVER=dummy
.venv/bin/python -m royaleviser RoyaleViser/tests/fixtures/frames-synthetic-A.jsonl.gz --seconds 8 --shot shot.png
It prints royaleviser: saved shot.png and a line of draw times. SDL_AUDIODRIVER=dummy is only
there to silence the ALSA lines.
11. Training stops with DeployRefused¶
What you see. train or bench stops after its first collection. On a 4-CPU Linux machine on
2026-09-27 the message was:
The count, the cycle and the slot can differ on your run.
What it means. The action mask allowed a play, and the engine then refused it. On
2026-09-27 the cause was one card, Heal: the mask offered it on the seat's own princess towers
and on buildings. The default setup deals random decks from the whole catalogue, so Heal turned
up. RoyaleGym b0948de fixed it, and the test suite now compares the mask with the engine for
every card.
What to do. Update RoyaleGym and RoyaleSim, then rebuild the engine. If it still stops, the mask and the engine disagree about something the tests do not cover. Ask in the Discord and paste the whole message.
Still stuck¶
Ask in the project's Discord. Paste the command you ran, the whole error, and the output of the two lines at the top of this page. Those two say more about a broken install than a paragraph of description.
- Install for the whole setup from an empty folder.
- How accurate is the engine if the engine runs but a battle does not look right.