Skip to content

README media, and the generator for all four repos

The generator lives here: make_media.py. It writes into RoyaleSim, RoyaleGym, RoyaleLearn and RoyaleViser, because the four repos are cloned side by side into one folder and every install recipe already assumes that. A sibling that is not there is skipped and says so.

.venv\Scripts\python RoyaleGym\docs\media\make_media.py --list
.venv\Scripts\python RoyaleGym\docs\media\make_media.py whole-battle determinism
.venv\Scripts\python RoyaleGym\docs\media\make_media.py                # everything

Run it from the folder that holds the repos. It needs the engine built, royalegym and royaleviser installed, and Pillow. The videos also need RoyaleViser's media extra, which supplies ffmpeg; without it the videos are skipped and the stills are still written.

It is reproducible. The battle is played on a fixed seed with a named deck, and RoyaleViser's capture() freezes the two numbers on screen that move with the wall clock, so running it twice gives the same bytes. Two shots are deliberate exceptions, because time is what they show: throughput times the engine on the machine that runs it, and live-training-env shows a real frame rate.

The deck is named on purpose. An environment with no state mutator deals a random 8-card deck out of the card catalogue, and the catalogue's size depends on which card table the checkout built. Two machines then play different battles from the same seed. Naming eight cards makes the battle a function of this file. The cards are looked up by name and never by number, because a card id is a position in the catalogue and the positions move between tables.

Each video is a .gif and an .mp4 of the same thing. The README points at the gif, because GitHub plays a gif inside an <img> tag and will not play an mp4 there.

What it makes, and where each one goes

Shot Repo What it shows
whole-battle RoyaleGym One whole battle end to end in the viewer, both players choosing at random among their legal moves.
battle-in-viewer RoyaleGym The same battle at its busiest tick in the whole viewer window: both hands, the status block and the inspector.
two-apis RoyaleGym One battle through PettingZoo, which drives both seats, and through Gymnasium, which drives one. The two runs are compared after every step, and a run with the seats swapped shows the comparison can fail.
legality-mask RoyaleGym Where each card in hand may go on the opening step, one board per card. Every move is checked against the engine's check_deploy, and again with the mask shifted one row to show the check can fail.
self-play-batch RoyaleGym A self-play batch: N battles become 2N rows, and the two rows of a battle hold the same towers with own and enemy swapped.
five-pieces RoyaleGym The swappable pieces of ClashParallelEnv, read off its constructor, with the default and the shipped choices for each.
start-anywhere RoyaleGym Four starting boards, one per state mutator, drawn from what reset() left behind.
record-and-verify RoyaleGym A battle recorded, saved, loaded and re-run on a fresh engine with every tick hash matching, beside a copy with one deploy moved a tile, where they stop matching.
replay-page RoyaleGym A whole battle as one HTML page, decoded back out of the written file, and a count of the outside references it makes, which is zero.
hidden-information RoyaleGym A to-scale map of one seat's observation vector: its own hand, what it has worked out about the enemy, and the enemy hand Reveal(enemy_hand=True) adds.
battle-page RoyaleSim The busiest stretch of the same battle, found by sweeping the trace for the tick with the most units alive. One frame per engine tick.
cards-and-spells RoyaleSim A spell landing on a crowd, found the same way: the tick with a spell in the air and the most units on the board.
deploy-legality RoyaleSim The arena coloured by what check_deploy answers, before and after an enemy princess tower falls.
determinism RoyaleSim One seed run twice and resumed once from a snapshot, with the per-tick hashes side by side.
throughput RoyaleSim A real transcript of tools/throughput.py, the median of five runs, with the spread of all five.
snapshots RoyaleSim One saved position loaded into several engines and played on differently.
ledger RoyaleSim All 148 engine constants graded by how well each one is known.
action-mask RoyaleLearn The legality mask of one real decision, drawn as one board per hand card.
engine-trace RoyaleViser A trace opened at tick 900, with a unit pinned in the inspector.
compare-ghost RoyaleViser The second recording of the scripted battle ghosted onto the first.
replay-scrubbed RoyaleViser The scripted battle that ships with the tests, played back.
live-training-env RoyaleViser A real environment streaming over UDP to the viewer in another process. This is the shot whose on-screen timing is real, because the frame rate is the thing being shown.
site-hero the docs site A smaller copy of the whole battle, for docs/site. MkDocs copies only what is under its own docs folder, so the site cannot reach this one.

What it does not make

A placeholder is a generated SVG whose alt text begins "Image placeholder:" or "Video placeholder:" and says what the real picture must show. Seven are left across the four repos, in two groups.

Two in RoyaleSim need a recording of a real battle drawn beside the engine (measured-routes, contact-law). Nothing here draws one yet. They are also the two tiles carrying the project's strongest claims, so they are worth doing properly rather than approximating.

Five in RoyaleLearn need real runs of the trainer (training-run, rollout-workers, frozen-pool-ladder, checkpoints, metrics-sink). RoyaleLearn's trainer exists and trains. What is missing is a run long enough to photograph. A rating curve, a ladder of frozen opponents, a resumed run lying on the original's curve and a metrics dashboard all need a run of many iterations, and so far runs have been a few iterations long. rollout-workers needs something slightly different: steps per second measured at several worker counts on an idle machine, and nothing sweeps that yet. There is no honest way to fake any of them.

make_media.py --list prints all seven with the reason for each.

Some pictures in the READMEs are not made here, and nothing else in the four repos remakes them: the family.svg diagrams, which are drawn by hand, and a few screenshots of the viewer (engine-battle-viewer.png in RoyaleSim, self-play-env.png in RoyaleLearn and the tile-*.png stills in RoyaleViser). battle-in-viewer.png here was one of them until 2026-09-28; it is now the battle-in-viewer shot, the Try-it battle captured in the whole window.

The drawn figures

Fourteen of the shots are drawn rather than captured, one to a module, _shot_*.py. Each one exposes draw(out_path) and computes everything on it at the moment it runs, from the engine or from the repos' own files.

They are drawn at about 1000 pixels wide and have to survive being shrunk to 360, which is roughly what a three-column README table gives them on a desktop. That is the whole of their design constraint: nothing smaller than 34 pixel type, one headline, and very few elements. An earlier set was drawn at 1280 with 18 pixel labels and was unreadable in the place it was going to be seen. If you add one, render it, shrink it to 360 wide, and look at it before you call it done.