A deterministic Clash Royale battle simulator in Java. Its battle core plays a battle in 50 ms steps on the game's own tables of one data version, and is held tick by tick to battles recorded in the game. A LibGDX debug visualizer plays battles by hand and replays recorded ones. A reinforcement learning environment steps a vector of battles behind flat buffers a Python trainer reads without a copy (docs/environment.md).
- Tick-exact: held tick for tick, with no tolerance, to battles recorded in the game on every data version it supports. CI replays all of them on each change and fails if any outcome moves; the outcome of each is listed in
conformance/reference-expectations/<version>.json(conformance). The battles of the current reference client, 16.635.1, are not recorded yet (compatibility) - Fast: about 104,000 steps a second on one thread on real Ladder replays, about 5,000 times the game's own 20 steps a second, headless (docs/performance.md)
Porting crforge's logic to another language, by hand or by asking an AI assistant to rewrite it? Please put a comment at each ported function or module that names the crforge file and the commit you read, for example:
// Ported from crforge core/src/main/java/org/crforge/core/pathfinding/target/TargetingVisit.java @ <commit> (https://github.com/voonhous/crforge)It is a request, not a license condition (crforge is Apache 2.0), and it is mostly for your benefit. crforge is held tick by tick to battles recorded in the game, and its rules get corrected whenever a recording shows them wrong.
With the file and commit in your code, git log <commit>..origin/main -- <file> lists every change to that logic since you read it (main is the development branch). The repository URL in the comment lets us find your port and tell you when we fix something it carries.
To hear about fixes, watch the repository's releases (the release notes list behaviour corrections per mechanic), or open an issue to ask about one.
Clash Royale battles turn on small margins: a troop that retargets one step later, a projectile that lands one tick after a tower dies, a spell that catches a unit at the edge of its radius. A simulator that only approximates the game (hand-tuned stats, floating point time, mechanics guessed from videos) drifts from the real battle within seconds, and the drift compounds. An agent trained on such a simulator learns the simulator's quirks rather than the game, and results from different simulators cannot be compared.
The game itself is no substitute: it cannot be run headless, stepped, forked or replayed at the scale machine learning needs. A simulator that gives the same battle as the game, step for step, from the same inputs, can stand in for it: train and evaluate agents offline, label recorded replays, and run experiments anyone can reproduce.
The full match of the animation above: the replay viewer plays the simulated battle (left) beside the game's own replay of the same battle (right).
crforge_demo3_blurred.mp4
crforge aims to be that simulator for AI and ML research on Clash Royale: an open, deterministic and accurate 1v1 battle engine, with evolutions, heroes and champions in scope. Accuracy is measured, not assumed: every recorded reference battle is either matched tick for tick, or its first divergence or refusal is recorded. On top of the battle core, the plan is a reinforcement learning environment and tooling to learn from recorded replays.
- The game's own data: units, projectiles, buffs, area effects, actions and expressions are read from the game's tables, in their own units and under their own column names; a column or class the battle core does not model is refused, never guessed
- Integer time: every duration is whole milliseconds, stepped by exactly 50 per tick
- Held to recorded battles: the
conformancemodule plays each recorded battle's scenario and compares every tick with the game's own trace, with no tolerance - Fidelity ledger: every battle core class says which of its parts are settled and which are not (
./gradlew :core:fidelityReport) - Replays: the visualizer plays a replay file, or a crawl of them, on the data version it was recorded on
Requirements: Java 17
# Build
export JAVA_HOME=$(/usr/libexec/java_home -v 17)
./gradlew build
# Run tests
./gradlew test
# Run debug visualizer
./gradlew :desktop:runmacOS: The visualizer needs
-XstartOnFirstThread. The Gradle task handles this; add it to VM options if running from an IDE.
The debug visualizer and the tests run the battle core, which needs the game tables. This repository does not ship them. The build makes them on your machine from the game's own files once you say where those come from, with the Gradle property crforge.assetSource (in ~/.gradle/gradle.properties, or -Pcrforge.assetSource=...):
cdnfetches them from the game's asset CDN (an opt-in, never a default)- a
file:URI names a local copy
That is the only way the tables are found (Building the game tables).
The debug visualizer prints the tables folder, data version and content sha at startup, and its key map. See Debug Visualizer for the full list, including the routing overlays (G, N).
./gradlew :desktop:run -Pcrforge.assetSource=<source>This opens the debug screen on a Ladder 1v1 battle on the standard arena, with towers and cards at level 11. You play both sides from the hand panels beside the arena: blue (side 0) at the bottom, red (side 1) at the top. The two decks are fixed, in org.crforge.desktop.battle.BattleDecks.
To play a card:
- Select it: click it in its hand, or press
1-4for blue's hand and5-8for red's. A number key only selects the card. - Move the mouse over the arena. The hovered tile shows where the card would be placed: each unit's outline and attack range, a spell's circle, or a red tile where the card cannot be placed.
- Left click to play it. Right click deselects the card.
A play runs 20 ticks (one second) after the click, as a player's play does in the game. A play the game would not accept (not enough elixir, a card already waiting to be played, no tile to place it on) is refused on the spot with a message.
| Key | Action |
|---|---|
SPACE |
Pause / resume |
. |
Pause and advance exactly one tick |
+ / - |
Speed up / slow down (0.25x to 8x) |
R |
Start a new battle |
I |
Switch between Deploy and Inspect; in Inspect, click a unit to pin its details |
O, D, A, H |
Range circles, damage numbers, area damage, HP numbers |
V |
Switch to the next data version built (a new battle on it) |
T |
Hide / show the diagnostics sidebar |
The full key list, including the routing overlays, is in Controls.
To play against a trained model instead of playing both sides, pass it as an ONNX file with --args="--model <file.onnx>" (Playing against a model).
./gradlew :desktop:run -Pcrforge.assetSource=<source> --args="--replay <file>"This opens the replay viewer on a replay file instead of a Ladder battle. You can also drop a replay file on the window of the debug screen or of the viewer. The viewer reads:
- a replay file (
.json): one battle and the plays of both sides - a file of many replays, one a line (
.jsonl, or compressed as.jsonl.zstor.jsonl.gz): the viewer lists them above the arena, and[and]open the previous and next
To try the viewer, open the example replay this repository ships, examples/replays/ladder.json, from the repository root:
./gradlew :desktop:run -Pcrforge.assetSource=<source> --args="--replay $PWD/examples/replays/ladder.json"It is a whole battle the battle core played between the debug screen's two decks with a fixed script, not a battle recorded in the game (examples). The repository ships no recorded replays. The battle core can write any battle it plays as a replay file (org.crforge.core.battle.replay.ScenarioWriter), and the environment writes one for a battle that faults (fault bundles).
A replay plays only on the tables of the data version it was recorded on. When the replay names its data version, the viewer switches to that version if you have built its tables, and refuses the replay with the reason if you have not. Build a version's tables with -Pcrforge.dataVersion=<version>. A replay the battle core cannot play in full is refused with every reason, never played in part.
| Key | Action |
|---|---|
SPACE |
Pause / resume |
. / , |
Pause and step one tick forward / back |
+ / - |
Speed up / slow down |
R |
Restart the replay from tick 0 |
F |
Flip the view: the game's view of side 1 (the default, as the game shows a replay) or of side 0 |
[ / ], L |
Previous / next replay of a file of many, hide or show the list |
| Left click | Inspect a unit |
The plays are the replay's own: cards cannot be selected or played in the viewer. More in Replays.
| Module | Description |
|---|---|
core |
The battle core: a headless battle simulator on the game's tables |
desktop |
LibGDX debug visualizer for watching and interacting with matches |
conformance |
Checks the battle core against recorded reference battles (README) |
env |
The reinforcement learning environment: a vector of battles behind flat buffers (docs) |
tables |
The table decoder: builds the game tables of a data version from the game's files |
python/ |
The crforge-env Python package: the environment's vector and wrappers for Python trainers (README) |
core has no GUI dependencies. conformance and env depend on core only, desktop on core and env (and ONNX Runtime, for a model opponent), and tables on nothing in the project. python/ is not a Gradle module: it runs the env module's installed jars in the JVM. The recorded battles live in a separate game data repository, at the commit crforge-data.lock names.
| Document | Description |
|---|---|
| Architecture | Modules, packages, the 50 ms step and the debug visualizer |
| The Battle Core | The step, the entity tick, what is covered and what is assumed |
| Troop Pathfinding | Routing grid, cell costs, routes and movement rules |
| Game Tables and Reference Battles | The tables the battle core reads and the battles it is held to |
| The Environment | The RL environment: its schema, mask, faults, benchmark and Python package |
See docs/architecture.md for the full documentation index.
This project uses Google Java Format enforced via Spotless. Formatting is checked on build:
./gradlew spotlessApplyThis project was inspired by scholarlygaming's Clash Royale engine, the first somewhat complete open-source Clash Royale simulation engine.
This is an independent fan project created for educational and research purposes. It is not affiliated with, endorsed by, or associated with Supercell. "Clash Royale", "Supercell", and related names and imagery are trademarks of Supercell Oy. See NOTICE for full attribution.
Licensed under the Apache License 2.0.
