Skip to content

README media

Every file the README embeds. Most of it is generated by a script and needs no screen, which is the part worth knowing. The exceptions are in the table: three tile-*.png screenshots and the hand-drawn family.svg are final and are not regenerated here. ../viewer-trace.png, the picture at the top of the README, is a copy of engine-trace.png.

The generator is RoyaleGym/docs/media/make_media.py, which is ../../../RoyaleGym/docs/media/make_media.py from here in a side-by-side clone. The link is the web URL rather than that relative path because a relative link with more .. than the page is deep escapes the repository, and GitHub does not clamp it: it rewrites the path under this repo's own tree and serves a 404. It lives in RoyaleGym because it writes into all four repos. From the folder that holds them:

.venv\Scripts\python RoyaleGym\docs\media\make_media.py engine-trace compare-ghost

The tile-compare and tile-paths-and-targets crops have a generator of their own here, because they are crops of this viewer's window on the recordings that ship with the tests:

.venv\Scripts\python RoyaleViser\docs\media\make_tiles.py

Underneath it is this repo's own capture(), which writes the window to a png, mp4 or gif with no window, no display and no clock. The same source and arguments give the same bytes, so regenerating is a clean diff. See docs/internals.md for the function.

File How it is made What it shows
engine-trace.png / .gif / .mp4 make_media.py engine-trace An engine trace opened at tick 900, with a unit pinned in the inspector.
compare-ghost.gif / .mp4 make_media.py compare-ghost The two recordings of the scripted battle, the second drawn on the first as hollow ghosts.
replay-scrubbed-4x.gif / .mp4 make_media.py replay-scrubbed The scripted battle that ships with the tests, played back.
live-training-env.png make_media.py live-training-env A real environment in another process, streaming over UDP to the viewer.
tile-compare.png make_tiles.py compare The compare panel at the end of the scripted battle, recorded from both seats: 395 ticks compared, 0 differ.
tile-paths-and-targets.png make_tiles.py paths-and-targets A recorded unit's route and another's target line, at tick 162 of the scripted battle.
tile-event-log.png, tile-inspector.png make_tiles.py event-log inspector (the engine-trace battle at tick 900) The event list, and every raw field of a pinned unit.
tile-live-stream.png, ../viewer-learning.png make_tiles.py live-stream learning (tests/run_stream.py) The viewer attached over UDP to a scripted battle and a scripted learner.
tile-synthetic-battle.png capture() on the scripted battle (tests/synthetic.py), tick 1015, crop="full" The whole window, rendered with no display.
family.svg hand-drawn the Royale repos and how they depend on each other; final

Two things the captions have to keep saying

The scripted battle is a script. tests/synthetic.py builds frames directly and no engine is involved. In its recording form the units move at six times their scripted speed. It is honest as a picture of the window and dishonest as a picture of how the engine plays, so replay-scrubbed-4x and compare-ghost are captioned as the scripted battle and never as a real match. The two recordings are also identical to each other, so a caption must not imply the ghost clip shows a disagreement it does not show.

The live shot is the one that is not reproducible, on purpose. Everything else here has its on-screen timing frozen, because a picture that changes every run is a diff nobody can review. live-training-env keeps the real frame rate and the real drop count, because the frame rate is the thing it is showing. The copy taken for the README admits 86 dropped frames of 116, which is what a busy machine looks like, and the README says so rather than cropping it out.

Some of these are not freely regenerable

Regenerating an image invalidates any prose that quotes what is inside it, and the generator cannot know which prose that is.

live-training-env.png is the live case. The README's caption cites its status line, "86 dropped frames out of 116", to make a point about what a busy machine looks like. Re-shooting it produces a different frame count and a different drop count, and the caption goes stale in the same breath. It happened once already: the picture was re-shot on a report that its learner string had changed, the string turned out to be conditional and had not changed, and the only effect was to break a caption that had been true.

So before regenerating any picture, grep the README for the numbers written on it. If the prose quotes them, the regeneration is a prose job as well as a rendering one.

Where each picture's battle comes from

Every picture in this repo is drawn from the engine, from a running environment, or from the scripted battle in tests/fixtures, which is committed. None is drawn from a recording of a real match: those recordings are private, so a picture made from one is a picture nobody else can make or check, and no caption here names one.

A picture made from a live engine records WHICH engine. royalegym's config() carries engine_binary_sha256, the hash of the compiled extension that produced the frames, beside build_digest, which is the data it was built from. The two move independently: a rebuild from a changed Rust tree leaves build_digest alone, so a picture stamped only with the data cannot tell you that the thing being pictured changed. Write the binary hash into the note beside any new picture taken from a running engine or a fresh trace, and take it in the process that produced the frames. The hash is read off the extension file on disk the first time it is asked for and kept for that process, so one fetched afterwards, from a second process, describes whatever is on disk by then rather than what drew the picture: the after-the-fact stamp again, one level down. Inside a single process they cannot diverge on Windows, where the loaded .pyd is refused for writing while it is held. The pictures already here predate the stamp and are not being given one after the fact; the ones drawn from tests/fixtures need none, because the fixtures are the committed input and the generator is in the repo.

Picture Its battle
engine-trace.*, tile-event-log.png, tile-inspector.png, ../viewer-trace.png an engine trace
live-training-env.png, tile-live-stream.png, ../viewer-learning.png a running environment, streaming over UDP
replay-scrubbed-4x.*, compare-ghost.*, tile-compare.png, tile-paths-and-targets.png, tile-synthetic-battle.png the scripted battle in tests/fixtures
family.svg hand-drawn, no battle

Two of these were hand-taken screenshots of a REAL match until 2026-09-22: tile-compare.png and tile-paths-and-targets.png. They were the only pictures here nobody else could reproduce, and the compare tile printed a recording's name on its own face. Both are now crops of the window on the committed recordings, made by make_tiles.py.

The other four hand-taken screenshots went the same way on 2026-10-01: tile-event-log, tile-inspector, tile-live-stream and ../viewer-learning.png are made by make_tiles.py too, on the current window, so no picture here is taken by hand any more. A generated picture updates when the window changes and can be diffed; what makes it publishable is still where its BATTLE came from, not that a script pressed the button.