Skip to content

Troubleshooting

Errors covered Messages Discord

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.

..\.venv\Scripts\maturin develop --release

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)
publisher: True attached: False

For self-play, set the ROYALEVISER variable in the shell the training run starts from, then build ClashSelfPlayVecEnv(8). In PowerShell:

$env:ROYALEVISER = "127.0.0.1:9870"

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.

..\.venv\Scripts\python -m royaleviser --stream 127.0.0.1:9870

Two more reasons a viewer stays empty, in the order worth checking:

  • attached: False above 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:

from royalegym import ClashParallelEnv, MockEngine

env = ClashParallelEnv(engine=MockEngine())

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:

..\.venv\Scripts\python -c "from royalegym import RustEngine; print(len(RustEngine().cards()))"

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.

..\.venv\Scripts\maturin build --release

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:

Package 'royalesim' requires a different Python: 3.11.15 not in '>=3.12'

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:

py -3.12 -m venv .venv
.venv\Scripts\python --version

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:

.venv\Scripts\python -m pip install torch --index-url https://download.pytorch.org/whl/cpu

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:

DeployRefused: the engine refused 2 command(s) the mask allowed (cycle 1, slot 141)

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.

Join the Discord