Clash Royale Simulator — system-level invariant verification for two engine subsystems
This is a deterministic multi-agent combat simulator written in Rust — 20 ticks per second, integer-only arithmetic, fully reproducible bit-for-bit. Built as practice for distributed systems and multi-agent simulation research, this document focuses on two system-level invariants I stress-tested with 100× reproducibility: simultaneous hit resolution (proving that symmetric agents trade damage on the exact same tick — the distributed consensus problem of "who goes first") and N-body collision convergence to stable separation (proving that N overlapping agents reach equilibrium spacing — the swarm formation problem). These mirror exactly the challenges of concurrent event ordering in distributed systems and multi-robot formation control in cloud-native edge deployments.
When two identical agents face each other at equal distance, both enter attack windup on the same tick. The question: does the engine resolve both hits on the same tick (mutual trade), or does iteration order cause one to die before dealing its hit? This is the distributed conflict resolution problem — identical to two nodes transmitting simultaneously.
| Parameter | JSON field | Value | Engine units |
|---|---|---|---|
| Hitpoints | hitpoints_per_level[10] | 1766 | HP |
| Damage | damage_per_level[10] | 202 | HP per hit |
| Hit speed | hit_speed | 1200 ms | 24 ticks (full cycle) |
| Load time (windup) | load_time | 700 ms | 14 ticks |
| Backswing | hit_speed − load_time | 500 ms | 10 ticks |
| Movement speed | speed | 60 → 30 u/tick | after speed_to_units_per_tick() |
| Attack range | range | 1200 | internal units |
| Collision radius | collision_radius | 500 | internal units |
Each troop has a 3-state attack animation model, driven by load_time and hit_speed from the JSON data. Damage is only applied at the Windup→Backswing transition. If the target becomes invalid during Windup, the attack is cancelled and the troop returns to Idle without dealing damage.
load_time and hit_speed.Place two Knights at equal distance from each other, both deployed simultaneously. They walk toward each other at identical speed (30 u/tick), enter range on the same tick, start Windup on the same tick, and the 14-tick timer expires on the same tick. The question is whether the engine processes A's damage before B's — which would kill B before B's hit resolves.
The engine's tick loop (step 7 in engine.rs) processes combat in two phases. First, all entities evaluate their attack state machines — advancing timers, detecting Windup completion, computing damage amounts. Then, accumulated damage is applied to targets. This means entity A's hit doesn't kill entity B before B's own hit resolves in the same tick.
This is the synchronous update pattern — identical to how distributed simulations handle concurrent state mutations. All agents observe the previous tick's state, compute their actions, and commit simultaneously.
The test spawns the same scenario 100 times and SHA256-hashes the complete entity state (position, HP, buffs, attack phase, target) after 300 ticks. All 100 hashes are identical, proving zero non-determinism in the combat resolution path.
The same property holds for ranged units. Two Musketeers (range = 6000, projectile speed = 1000) fire projectiles on the same tick. Both projectiles travel independently and deal damage on impact. We verify both took damage after 150 ticks, consistent 100×.
When N agents spawn at the exact same position, the collision resolution system must separate them to stable equilibrium positions. This is the swarm formation control problem — identical to drones initializing at a shared depot and fanning out to surveillance positions.
Each tick, the collision system computes pairwise overlap between all non-flying entities within 1.2 × (r_i + r_j) distance. For Knights (collision_radius = 500), the interaction radius is:
The push force is proportional to the penetration depth:
At exact overlap (dist = 0), the symmetry-break rule fires: entity with even ID pushes +X, odd pushes −X. This seeds the initial separation.
The stable separation distance converges to approximately 2 × collision_radius. This is because the push force reaches zero when entities are exactly at the interaction boundary. For Knights:
Measured: 998 iu (test 1830c). The 2-unit discrepancy comes from integer truncation in the push vector computation — consistent with the integer-only arithmetic invariant.
The collision system operates on two independent layers: ground (z = 0) and air (z > 0). Ground entities only collide with ground entities; flying entities only collide with flying entities. A ground Knight and a flying Mega Minion at the exact same (x, y) do not push each other.
| Entity | flying_height | Layer | Collides with |
|---|---|---|---|
| Knight | 0 | Ground | Ground troops + buildings |
| Mega Minion | 1500 | Air | Air troops only |
| Cannon (building) | 0 | Ground | Ground troops |
The collision check in combat.rs enforces this with an explicit layer guard:
// combat.rs — tick_collisions(), layer guard
// Flying/ground layer check: flying only collides with flying,
// ground only with ground (and buildings which are always ground).
if a.is_flying != b.is_flying && !b.is_building {
continue;
}
// Flying troops don't collide with ground buildings
if a.is_flying && b.is_building {
continue;
}
| Constraint | How satisfied |
|---|---|
| Deterministic conflict resolution | Deferred damage application: all hits computed before any applied. Verified 100× with SHA256 hash. |
| No first-mover advantage | Entity iteration order doesn't affect outcomes. Symmetric agents produce symmetric results. |
| Collision convergence | N=5 converges in ~10 ticks, N=10 in ~16 ticks. Equilibrium at 2 × collision_radius. |
| Layer isolation | Explicit is_flying guard. Zero cross-layer push forces. |
| Integer-only arithmetic | Push vectors use i64 intermediate products, truncated to i32. No floating-point drift. |
| All parameters from JSON | collision_radius, mass, flying_height, load_time, hit_speed — zero hardcoded heuristics. |
RESULTS: 96 passed, 0 failed out of 96 assertions
Simultaneous hit resolution:
✓ Mirror Knights: both trade same tick (diff=0), consistent 100×
✓ Mirror Musketeers: both projectiles deal damage, consistent 100×
✓ Simultaneous princess tower destruction: consistent 50×
✓ Melee vs ranged first-hit: Musketeer always hits first, consistent 100×
Collision convergence:
✓ 5 Knights at same point: all 5 separated, no overlaps
✓ Min pairwise distance = 998 iu (≈ 2 × collision_radius)
✓ 10 Knights at same point: ≥5 unique positions, all alive
✓ Flying + Ground same position: no collision (layer isolation)
✓ Building blocks troop pathing (no clip-through)
System-level determinism:
✓ 4v4 combat: 100× identical SHA256 hashes
✓ Complex mixed scenario: 50× identical
✓ Full 9600-tick match: 10× identical
✓ Entity ID monotonicity: 200 spawns strictly increasing
✓ Entity pool cleanup: 200-entity combat, dead removed correctly