Movement and pathfinding: the offline measurements¶
This page is for contributors who need the evidence behind the movement and pathfinding rules. It
shows how the 2026 Clash Royale engine moves ground troops and plans their paths, measured on
recorded traces of the real client (Clash Royale 15.535.29). It is the primary source behind the
pathfinding.*, time.* and movement.* entries of data/calibration.json, and behind
pathfinder-spec.md, which turns these measurements into an implementation contract.
The traces carry one frame per 50 ms tick of a real battle. No number below comes from a community engine or from an argument. Where a claim is not measured (and several of the ones that get repeated loudest are not), it says so.
The model the engine selects today was measured on a later client version, 16.402, and is in
pathfinding.md. It supersedes the search and the contact law described here. The frame, the
units, the movement laws and the path representation are unchanged between the two.
1. Provenance¶
| Item | Value |
|---|---|
| Client | Clash Royale 15.535.29 |
| Corpus | recorded traces of client 15.535.29, one frame per tick, under data/oracle-native/ (not distributed) |
| Step | 0.05 s per tick, one recorded frame per tick |
The traces carry one frame per 50 ms tick and no wall-clock time. Every experiment starts a fresh battle and deploys after tick 100 (the game rejects a deploy before roughly tick 100).
2. The corpus¶
145 traces under data/oracle-native/, plus one orientation trace. Counting one unit per trace
undercounts. Enumerating every (side, generation_key) pair yields 150 units. The
difference matters, because the extra units are exactly the ones that break the tidy laws.
| Directory | Files | Units | What |
|---|---|---|---|
walk/ |
6 | 8 | One card each at own-tile (3.5, 8.5), empty board, 500 ticks. Skeletons holds three units, not one |
lane_sweep_Knight/ |
128 | 128 | A Knight at the centre of every second legal tile, 320 ticks |
building_Giant/ |
6 | 6 | Giant at (3.5, 3.5) with a friendly Cannon at (3.5+dx, 9.5), dx ∈ {−2,−1,0,+1,+2} plus control |
repath_Giant/ |
2 | 2 | Giant at (3.5, 5.5); friendly Cannon dropped at (3.5+dx, 11.5) after 60 ticks |
meet/ |
3 | 6 | Two mobile units. The only side-1 units and the only moving targets in the corpus |
orient/ |
1 | none | Tower layout and deploy-legality probe grids |
The deploy command is clamped to the deployer's own half, so the 128 lane-sweep files
contain only 64 distinct experiments: every cNN_rRR with RR >= 14 lands at
y = 14500, and nine files share the start (2499, 14500). Duplicates produce byte-identical
paths, so no conclusion changes. But any "128/128" or "140/140" figure is roughly twice
its true sample size. Figures below are stated over units or over first-paths, not files.
3. How it was measured¶
Four independent analyses cover timing and the speed law, node encoding and consumption,
occlusion geometry and cost, and the full A* fit. Each one was re-measured from scratch by an
independently written implementation. That rewrite covered the loaders, decoders, integer
arithmetic and Dijkstra as well, so that a shared bug could not hide in all four. The analysis is
read-only and reads this repo's data/oracle-native/ and data/raw/ (ROYALESIM_DATA_DIR
overrides).
Every number in sections 4-7 below comes from that final re-measurement unless it is attributed to one of the four analyses; section 10 lists what it covers.
4. Frame, time and movement¶
4.1 The frame¶
Native coordinates are integer arena units, 1000 per tile, x ∈ [0, 18000), y ∈ [0, 32000). This comes from the recorded trace itself, not inferred: the trace header puts the side-0 king at (9000, 3000) and the princess towers at (3500, 6500) and (14500, 6500), and the orientation trace's probe grids declare width 18, height 32, cell size 1000. Side 1 is the exact 180° rotation.
4.2 Tick length: 50 ms, one position write per tick¶
The traces carry one frame per 50 ms, which alone cannot rule out finer internal sub-steps. The discriminator is position truncation, and it is decisive. Modelling n independently truncated sub-steps per tick, re-aiming at the waypoint from each intermediate position and fitting the per-sub-step speed freely over 1..400:
| Sub-steps per tick | Surviving speeds |
|---|---|
| n = 1 | exactly one per card (60, 52, 54, 90, 90, 120) |
| n = 2..5 | none, for any card |
Independently: DeployTime / 50 equals first_move_tick − first_seen_tick for 149 of
150 units (the exception is a recording-window artefact: that trace's first recorded
frame is already tick 114). Tower-damage gaps are whole ticks at 50 ms for every card
with a HitSpeed in the corpus (Knight 1200 → 24, MiniPekka/HogRider 1600 → 32, Giant
1500 → 30, Golem 2500 → 50).
TICK_MS = 50. The position update runs once per 50 ms.
A note on the ledger's counter-argument. time.TICK_MS's disagreement.60_TPS_16.67ms
says PhoenixNoRespawn's DeployTime = 733 ms "is 44 ticks at 60Hz (733.33) but 14.66 at
20Hz". 733 / 16.667 = 43.98, not 44. The argument needs 733.33 and the shipped value is
733. Scanning every millisecond duration in csv_logic/characters/*.toml, only 7 values
are not multiples of 50, and one of them (Lightning HitSpeed = 460) is a whole number
of ticks at none of 50, 33.3 or 16.67 ms. "Shipped durations are whole ticks" is void
as an argument in either direction. Strike that disagreement entry rather than leave it
standing beside a measured value.
4.3 The Speed column is millitiles per tick¶
Fitting an integer S such that every moving tick satisfies
step = (trunc(S·dir_x/256), trunc(S·dir_y/256)), with S free over 1..5000 and no card
table, leaves exactly one survivor per card. Every unit of a given card fits the same S:
| card_id | Card | Units | S (units/tick) |
TOML Speed |
Step law holds |
|---|---|---|---|---|---|
| 26000000 | Knight | 131 | 60 | 60 | 26101 / 26575 = 98.22 % |
| 26000003 | Giant | 12 | 52 | 45 | 3238 / 3732 = 86.76 % |
| 26000009 | Golem | 1 | 54 | 45 | 270 / 270 = 100 % |
| 26000010 | Skeletons | 3 | 90 | 90 | 260 / 367 = 70.84 % |
| 26000018 | MiniPekka | 1 | 90 | 90 | 165 / 165 = 100 % |
| 26000021 | HogRider | 2 | 120 | 120 | 229 / 250 = 91.60 % |
So one Speed unit is one native unit per 50 ms tick. That is, tiles/s = Speed/50. At
18000 subtiles per tile the multiplier is exactly 18. The competing "Speed is
tiles/minute" reading is the one that gives 15, and it predicts a Knight at 1.00 tiles/s
against the measured 1.20. That is a flat 20 % error on every unit in the game.
The scale check is genuine, not circular: in steady state the heading is never
axis-aligned (the unit is steering back onto its column), so S is pinned by the exact
integer step. A direction-free cross-check agrees. The modal pure-y displacement on
ticks with dx = 0 is (0, 60), (0, 90), (0, 120), (0, 52), (0, 54) for the respective cards.
Scope. This is the unbuffed base speed at level 11. csv_logic is full of
SpeedMultiplier keys (rage, ice wizard, snowball, prince buff, hero forms) and nothing
here measures how they round. Separately, the traces show that the level multiplier
exists and does not touch Speed: measured tower damage per hit runs 2.556–2.559× the
level-11 TOML Damage for all six cards, while measured S equals TOML Speed exactly
for every non-stomp card.
4.4 The position update¶
x += trunc_toward_zero(S * movement_direction_x / 256) # per axis, independently
y += trunc_toward_zero(S * movement_direction_y / 256)
The sub-unit remainder is discarded every tick. There is no fractional accumulator: integrating this rule alone from the first moving tick reproduces the final position exactly for every isolated unit over 106–308 ticks, with zero drift.
Truncation toward zero, not floor, and not round. This is load-bearing and discriminating:
dir_x < 0 on every moving tick of every walk unit, and floor differs from truncation on
the x axis on every one of those ~1000 ticks. Floor matches 0 ticks; round matches 17–43
per card; ceil matches 201/240 (Knight). In Rust, i32 / i32 is already correct and
div_euclid is wrong.
4.5 The heading¶
d = centre(path_nodes[-1] at tick t-1) - position(t-1)
L = isqrt(d.x*d.x + d.y*d.y) # integer square root, floored
dir = (trunc(d.x*256 / L), trunc(d.y*256 / L)) # integer divide, toward zero
Computed from the pre-move position toward the node current at the start of the tick. Post-move as the origin scores 85–264 matches per card against 106–307 for pre-move.
The floored length is what matters, not the choice of integer-sqrt routine:
floor(math.sqrt(L2)) and math.isqrt(L2) score identically, while dividing by the
unfloored length matches 14–42 ticks per card and ceil(sqrt) 14–41. Over 1258 walk
ticks: floored-sqrt with truncating divide 1258/1258; nearest-integer sqrt 1179;
sqrt-after-scaling-by-256 191; ceil 173; a +1 sqrt 76.
The 256 is a normalisation, not a clamp. Because each axis truncates independently,
|dir| legitimately ranges over 254.678..256.236. The rounded magnitudes are {255, 256}
and 257 never appears. Do not renormalise.
Worked example, Knight tick 121: pos_120 = (3499, 8500), node 654 → cell (col 6,
row 18) → centre (3250, 9250), d = (−249, 750), isqrt(624501) = 790,
(−249·256)//790 = −80, (750·256)//790 = 243 → observed movement_direction = (−80, 243).
Heading is stateless: a pure function of position and current node, recomputed from scratch each tick. It snaps: 71.57° in a single tick on a spawn tick. There is no turn-rate limit that binds: no sample anywhere exceeds 71.57°, so a hypothetical cap above ~72°/tick would be invisible here. Implement it stateless; do not add inertia.
4.6 There is no ease-in¶
The apparent ramp over the first ~40 moving ticks (Knight |step| 58.82 → 60.00) is
entirely per-axis truncation of an off-axis heading while the unit steers onto its column.
The fitted S already reproduces the first moving tick exactly. No acceleration term,
no partial first step.
4.7 The Giant/Golem anomaly, and how much of it is actually solved¶
Giant and Golem both carry Speed = 45 yet measure 52 and 54. Both are among the five
shipped cards with StopMovementAfterMS, and both visibly freeze in the traces. The
proposed rule:
Giant: floor(45·740/640) = 52. Golem: floor(45·1200/1000) = 54. Both correct.
But this is a two-point fit and it is not uniquely identified. The Golem's exact value is 54.00000, so it discriminates no rounding mode at all. Only the Giant's 52.03125 separates floor/trunc (52) from ceil (53). Two algebraically distinct rivals agree on every card Supercell ships:
| Rival | Giant | Golem | GoblinGiant | RoyalGiant | IceGolemite |
|---|---|---|---|---|---|
floor(Speed·(Stop+Wait)/Stop) |
52 | 54 | 69 | 52 | 52 |
Speed + floor(Speed·Wait/Stop) |
52 | 54 | 69 | 52 | 52 |
Speed + Mass − 11 |
52 | 54 | not given | not given | not given |
The real evidence for the duty-cycle reading is not the arithmetic. It is that the same
Stop/Wait numbers independently predict the observed pause cadence. That coupling is
what should be cited. The rounding mode stays undetermined; IceGolemite (Speed 45,
470/80 → 52.66) is the one shipped card that would separate floor (52) from round (53),
and it has no trace.
4.8 The stomp pause schedule¶
With k = 0 on the unit's first moving tick and never reset, the unit is stationary iff
Both free choices are pinned, not asserted. Strict > beats >= (Giant 308/308 vs
304/308; Golem 306/306 vs 294/306; the Golem discriminates because Stop = 1000 lands
exactly on the 50 ms grid), and (k+1) beats k (Giant 268/308, Golem 282/306). Every
phase offset 0..T/50 was brute-forced; only offset 0 fits.
Observed: Giant 12 moving ticks, 2 stopped, then 13/2 repeating with a 1-tick stop every fifth cycle (74 ticks = 5 × 740 ms). Golem 20 moving, 3 stopped, then 21/3 on a 24-tick period. Holds 100 % on 7 of the 8 unobstructed Giant traces.
Two corrections to figures that were circulating: the Giant's duty cycle is 9 stop ticks
per 74, duty 0.8784, long-run mean 45.68 units/tick against an observed 45.79. So
the Giant overshoots Speed = 45 by about 1.5 %, in the same direction as the Golem
(47.25 model, 47.53 observed). The earlier "duty 0.8649 → 44.97, undershoots" figure is
wrong. The 50 ms grid samples the pause window coarsely; the engine must reproduce S
and the schedule, not the average.
The pause is not an absolute freeze. In meet/Giant_vs_Giant_seed15 the side-0 Giant
moves on 2 ticks the schedule marks as pauses (t281 and t296) while being jostled. The
schedule must gate the unit's own locomotion, not the position write, so that separation
and pushback still displace it during a freeze.
4.9 Where the movement laws stop¶
The old domain statement, "the step law fails iff avoidance_offset != 0", is false.
This is the most consequential correction in the whole pass.
Every step-law failure in the corpus is one of three regimes, and two of them set no flag at all:
Case counts are from the offline trace corpus of client 15.535.29 described at the top of this page.
| Regime | behavior_state |
Flags | Cases | Signature |
|---|---|---|---|---|
| Avoidance | 1 | avoidance_offset != 0 |
~940 | heading deflected, |step| within ±2.3 % of S |
| Crowd separation | 1 | none | 137 | |step| ≈ S, lateral residual 15–28 units ⟂ to movement_direction |
| Combat pushback | 2 | none | 36 | displacement 0.06–2.5 × S, can point opposite to movement_direction |
(one further case at behavior_state 4) |
4 | none | 1 | not given |
No field in the corpus records the contact impulse, so any push must be inferred from the residual (observed step minus predicted step).
Concrete witnesses. walk/Skeletons_x3.5_y8.5_seed2.jsonl.gz contains three skeletons, and
a loader that latches the first non-tower entity sees only one of them. Skeletons #1 and #2 fail the step law on 54/137
and 53/123 of their moving ticks and no integer S in 1..400 fits either. For example, #1 at
t193 steps (−23, 88) where the law predicts (4, 89), a pure lateral push of (−27, −1)
while |step| stays 87–92 for S = 90. In meet/Knight_vs_HogRider_seed15 t204–t221 the
Knight is pushed at up to 46 units/tick in the direction opposite to its own
movement_direction. In meet/Giant_vs_Giant_seed15 ticks 289–301, both Giants in
behavior_state 1 with every flag clear, the separation is 1465–1501 units, exactly
2 × CollisionRadius 750.
The honest domain is: the movement laws hold for a unit that is not in contact with another unit. That is a narrower and more useful statement than the flag test, and it hands the collision work a dataset that was not obviously there.
4.10 avoidance_offset is a bounded walk, not a decay¶
Corpus-wide, avoidance_offset changes by only ±10 and ±190. Onsets are always 0 → ±190
(23 negative, 13 positive; 36 runs total). But only 20 of 36 runs shrink monotonically:
runs are 19 to 67 ticks long (median 21) against the 19 a pure decay implies, and 16 runs
ramp back up by 10/tick before coming down. For example, lane_sweep_Knight/c02_r02 goes
−190, −180, … −20, −30, −40, … −70, −60, −50, … −10.
It is a ±10-per-tick walk bounded at ±190, not a decay. "Set to ±190, decremented by 10
per tick toward 0" must not be written into the ledger. The deflection it applies is also
not a simple rotation: the ratio of heading error to |avoidance_offset| ranges 0.327 to
0.387, non-linearly. What the offset does is unmeasured.
Avoidance also perturbs speed slightly, contradicting "the unit still moves at exactly S":
truncation toward zero can only shorten a step and |dir| <= 256.24, so |step| <= 60.06
for S = 60. The measured maximum on avoidance ticks is 61.39, and 255 of 437 lane-sweep
avoidance ticks exceed that ceiling. The magnitude histogram is {59: 60, 60: 276, 61: 101},
roughly ±2.3 %.
4.11 Deploy timing¶
A unit first appears on the tick after the accepted deploy command. behavior_state
is 4 (deploying) for DeployTime/50 − 1 ticks, flips to 1 on the tick before the first
move, and
149 of 150 units. LoadTime (600–1500 ms across these cards) does not gate movement; it
gates the first attack.
The spawn tick, not the command tick, is the anchor. The Golem's command is accepted at
tick 113 rather than 100 because it waits on elixir (the header's resource_before shows
elixir 8 against card_cost 8, versus elixir 7 / cost 3 for the Knight). It spawns at
114, targets, paths and moves at 174. A rule written against the deploy tick fits the
other five cards perfectly and is 14 ticks wrong on the Golem.
5. The path representation¶
5.1 Encoding¶
path_nodes are half-tile cell indices on a 36 × 64 grid of 500-unit cells:
The decode is pinned by an exact integer identity, not by a plausibility score.
path_segment_direction equals the 256-scaled direction from the unit to the last
element's cell centre on 3781 / 3781 segment-assignment ticks. Sweeping the offset,
a 10-unit shift in either axis collapses the match to 2–3 % (x) or 74 % (y); grid widths
18/32/34/35/37/38/64/72 all score ≤ 0.08 % while 36 scores 100 %. A brute-force sweep of
every width 8..128 in both row-major and column-major, scored by trajectory distance,
puts width-36 row-major at mean 120.6 units against 4613.3 for the runner-up.
(An earlier decoder score based on point-to-polyline distance is nearly blind to the row offset. Every walker moves almost due +y, so a 500-unit row shift costs almost nothing. Record the segment-direction identity as the evidence, not that score.)
Nodes are absolute arena cells, not per-side. Side-1 walkers decode in the same grid: mean trajectory distance 118.4 and 141.1 units, against 10238 and 11484 for a 180°-rotated decode.
5.2 Ordering and shape¶
The list is stored goal-first; the next waypoint is the last element, popped from the tail. Over ~486 000 adjacent pairs in the corpus, zero are not 8-neighbours. It is a contiguous cell chain with no smoothing or string-pulling.
Paths are weakly monotone toward the goal: no recorded path in the corpus contains both a forward and a backward row step. (The 6334 apparent "southward" steps are all side-1 walkers, whose goal is at low y.) They are not column-monotone: 1339 purely horizontal steps occur, e.g. (2,15) → (3,15) and (10,29) → (9,29).
The recorded path is not anchored at the unit's cell: the first recorded node sits
at Chebyshev distance 2 from the unit's cell in 113 of 128 lane-sweep traces and distance
1 in the other 15. That distance is a consequence of the consumption rule (5.3), not a
rule of its own. A "drop the first two cells" rule gets the Giant wrong, because both
repath_Giant first paths drop only one.
Longest list observed: 46 nodes. The trace schema caps at 115 and no experiment here comes close, so whether the engine truncates is untested.
5.3 Node consumption: a 1000-unit standoff, tested after the move¶
The often-repeated description of this mechanism is "the unit reaches the node". The unit does not. Measuring, for every consecutive tick pair with a structurally stable list, the distance from the post-move position to the tail node's cell centre:
| n | min | max | |
|---|---|---|---|
| Ordinary drops (list still non-empty) | 3731 | 637.2 | 1074.1 |
| Terminal drops (list becomes empty) | 120 | 774.1 | 1436.1 |
| Keeps | 27 824 | 1000.6 | not given |
No single Euclidean threshold exists. The drop maximum (1074.1) exceeds the keep minimum (1000.6). What is exact is one-sided:
| Predicate | Missed drops | False drops |
|---|---|---|
| post-move Euclid ≤ 1000 | 148 | 0 |
| post-move Euclid ≤ 1003 | 66 | 41 |
| post-move Euclid ≤ 1001 | 121 | 3 |
pre-move Euclid − S ≤ 1000 |
133 | 83 |
| post-move Chebyshev ≤ 1000 | 23 | 1988 |
| post-move Manhattan ≤ 1000 | 2525 | 0 |
| integer squared ≤ 10⁶ | 148 | 0 |
dist <= 1000 from the post-move position never fires early across 27 824 keep-ticks,
and fires late on 148 of 3731 drops (4.0 %), by at most 74 units. 60 of the 148 carry
avoidance_offset != 0; the 88 with no flag span 1000.0–1030.4. The threshold is
card-independent (best per-card fits: Knight 1003, Giant 1003, Golem 1001, HogRider 996,
Skeletons 993). It is one tile, not a function of CollisionRadius or Speed.
At most one node per tick. Zero of 3851 tail-drop ticks dropped more than one.
The rival "integer remaining-distance counter decremented by the speed" model is measurably worse, not better: it gets the wrong pop tick on 518 of 3550 segments with a systematic error histogram {−2: 148, −1: 102, +1: 102, +6: 91, +7: 15}, against 271 for live Euclid. The earlier reading that the overshoot favours the counter is backwards.
The goal node is never walked onto. It is dropped on the tick the unit enters
behavior_state 2, still 1046.7–1436.1 units from the goal cell centre. Across 116
terminal pops, every one has behavior_state != 2 on the previous tick and 2 on the pop
tick, and none was within the 1000 standoff. The census of (behavior_state, has_path,
has_target) has exactly four cells over 46 304 ticks: (1, T, T) walking, (2, F, T)
attacking with the path cleared, (4, F, F) deploying, (1, F, F) the single transition tick.
So the path clear and the state change are the same event.
Because the unit turns a full tile before each node, corners are cut: closest approach to a corner node averages 152.9 units and reaches 395.5. That falls out of the 1000-unit standoff plus per-tick re-aim; it needs no separate rule.
5.4 The two direction fields¶
movement_direction is the live bearing: norm256(centre(node_{t−1}) − pos_{t−1}), i.e.
the heading of the step that produced frame t. It matches on 29 717 / 29 996 live-path
ticks (99.07 %), against 70.54 % for aim-from-current-position, 52.71 % for the frozen
segment, and 0.56 % for "the quantised step actually taken".
An independent check that does not fit one derived quantity to another: the angle between
(pos_t − pos_{t−1}) and the movement_direction recorded at tick t is under 1° on
30 267 / 31 359 moving ticks (median 0.529°), against 27 154 for the value at t−1.
path_segment_direction is sticky: it changes only on ticks with
path_node_consumed == 1, and then equals norm256(next_node_centre − post-move position).
Zero exceptions on the walk traces. It is the segment heading frozen at acquisition and is
otherwise unused.
entity.x2 / y2 is exactly the previous tick's (x, y), 49 333 / 49 333, no
exceptions. That makes the heading law checkable inside a single frame, and gives a
trace-diff harness a free per-tick anchor.
5.5 path_node_consumed¶
Not a one-tick latch. The census over all 150 units:
has_path |
path_node_consumed |
Ticks |
|---|---|---|
| False | 1 | 17 624 |
| True | 1 | 4 031 |
| True | 0 | 27 828 |
| False | 0 | 0 |
It sits high for the whole deploy phase and the whole attacking phase: 4004 runs of 1s, 373 of them longer than one tick, the longest 257 ticks. The reading that fits every tick is:
path_node_consumed == 1iff there is no segment currently being walked; i.e. the path is empty, or the segment was (re)assigned this tick.
The circulating rule "a recompute that leaves the last node unchanged must NOT set it" is
inverted by the data: such recomputes split 95 with the flag set against 2 clear. Two of
the four supposed exceptions are path creations (repath_Giant/row11.5_dx± at tick 121,
previous list empty), where the rule cannot even apply. Every other creation in the
corpus carries 1.
6. The pathfinder¶
6.1 Grid¶
The 2026 tilemap (data/raw/cr-15.535.29/tilemaps/tilemap.csv) parses to 64 rows × 36
columns, histogram {0: 1386, 1: 309, 2: 309, 16: 104, 17: 36, 18: 36, 32: 112, 128: 8,
257: 1, 258: 1, 512: 2}. It is bit-identical to the 2018 data behind
data/derived/arena.json except 12 cells that only gain marker bits: 256 at the two
bridge centres (7,32) and (28,32), 512 at the arena centre (17,41) and (18,41), and 128 on
eight cells. Adding 256 to the road set changes nothing, so the 2026-only bits are not
pathfinding costs. Alignment is forced by geometry, not assumed. Two things fix it: the
bit-16 block spans cols 15–20 × rows 3–8, centred exactly on the side-0 king at
(9000, 3000), and water occupies rows 30–33 with non-water columns 5–8 and 27–30 at the
bridges.
The cost grid is invariant under 180° rotation and under independent vertical and horizontal flips (0 mismatching cells), so a row- or column-orientation error when parsing is harmless for pathfinding. It would matter, though, if the lane bit 1-vs-2 distinction ever becomes load-bearing.
6.2 Costs¶
Scored as "is the recorded path exactly cost-minimal between its own
endpoints", over all 150 first-paths including the meet/ traces and side-1 units,
with a Dijkstra written independently of the analysis scripts:
| Model | Suboptimal | Infeasible |
|---|---|---|
| road 5 / plain 8 / water blocked / bit-16 blocked / diag √2 | 0 | 0 |
| flat 8 everywhere (no road discount) | 96 | 0 |
| road 4 / plain 8 | 1 | 0 |
| road 6 / plain 8 | 96 | 0 |
| road 5 / plain 7 | 96 | 0 |
| road 5 / plain 9 | 1 | 0 |
| water traversable at cost 7 | 25 | 0 |
| water at cost 50 | 0 | 0 |
| bit-16 at cost 50 instead of blocked | 0 | 0 |
| no occluders at all | 12 | 0 |
"Road" is the tilemap lane bits 1 or 2. PATHFINDING_ROAD_COST and
PATHFINDING_MATCHINGROAD_COST are both 5 in the live globals, so nothing here can
distinguish a unit's own lane from the other one.
Water is effectively impassable to ground units. PATHFINDING_WATER_COST = 7 is in
the shipped globals but modelling it that way makes 25 first-paths strictly dearer than
the optimum. The recorded units refused water shortcuts it would have taken. Cost 50 and a hard
block are indistinguishable here, as are cost 50 and a hard block for bit-16 terrain and
for buildings: no recorded path ever needed to cross one.
6.3 The diagonal weight¶
A diagonal step costs √2 × the entered cell's cost. Sweeping the multiplier over all 150 first-paths:
| Multiplier | Suboptimal paths |
|---|---|
| 1.0 (diagonal = straight) | 3 |
| 1.2 | 2 |
| 1.4142 | 0 |
| 1.5 | 1 |
| 2.0 | 96 |
In exact rational arithmetic the zero-failure window is [1.38, 1.48], which contains √2.
(Float rounding at 1e-6 produces a spurious 116/128 failure for √2. Use Fraction if
you re-check this.) The witnesses that kill the uniform diagonal are
lane_sweep_Knight/c00_r06, c08_r04 and c10_r06, each of which pays one or two extra
steps to keep its diagonals scarce.
This matters beyond pedantry: with a uniform diagonal, road 5 / plain 8 and a flat cost
8 classify every path identically, so the road discount is unidentifiable. Fix the
diagonal and flat-8 fails 96 paths. The cost model is a joint fit. The road discount,
the √2 diagonal and the occlusion set are each load-bearing for the others, and no
per-axis sensitivity table is meaningful outside the winner.
6.4 Building occlusion¶
A building blocks every half-tile cell overlapping the axis-aligned, half-open box
[cx − R, cx + R) × [cy − R, cy + R) with R = its CollisionRadius:
Per-axis, never Euclidean. Scored over all 3776 recorded path lists, the per-axis square
gives 0 suboptimal / 0 infeasible, while circle-overlap at R gives 45 / 216,
circle-overlap at R + 250 gives 27 / 246, and cell-centre-within-Euclid gives 23 / 0.
Radius brackets, from the first-path optimality and blocked-but-used tests:
| Occluder | Shipped CollisionRadius |
Measured bracket | Evidence |
|---|---|---|---|
| Cannon | 600 | (500, 1000] | R ≤ 500 → 2 suboptimal; R ≥ 1005 → 18 blocked-but-used |
| Princess tower | 1000 | (500, 1000] | R ≤ 500 → 11 suboptimal; R ≥ 1100 → 155 blocked-but-used |
| King tower | 1400 | unmeasured | R = 0 scores identically to R = 1400 (0/0); only R ≥ 2000 fails |
The 500-unit cell quantum is why the brackets are wide: for a building on a tile centre,
every R in (500, 1000] blocks the same cells. R = CollisionRadius is therefore
consistent with the data and corroborated by three different shipped radii producing the
three observed footprints under one unfitted rule. It is not independently measured to
scale with CollisionRadius.
Half-open, not closed. A closed box blocks 57 cells the recorded paths use. The
discriminator is the tower, not the Cannon: for all five Cannon positions the half-open
and closed boxes are identical (none of x ± 600, y ± 600 is a multiple of 500), while
the towers' radii 1000 and 1400 are exact multiples of the cell size.
No mover radius, no clearance pad: the term is exactly zero. A pad of even 1
native unit blocks 155 cells the recorded paths use, because cx − R = 2500 lands
exactly on a cell boundary for the princess towers and the Giant's control path runs up
column 4 at rows 11–16. Cannon 600 + Giant 750 = 1350 would block the very column the
cannon_dx+0.0 detour takes. The Cannon alone brackets the pad to [0, 400]; including the
towers pins it at 0.
The deployer's own towers must occlude: without any occluders, 12 first-paths are strictly dearer than the optimum.
FRIENDLYONLY_OCCLUSIONS is not measured here. Adding the enemy towers as occluders
scores identically to friendly-only (0 suboptimal, 0 infeasible, 0 blocked-but-used),
because no interior cell of any first path lies inside an enemy tower box. The argument
that "goal cells sit inside the enemy box, so enemy towers cannot occlude" is answered
equally by the goal-cell exemption below. The globals say TRUE; the traces are silent.
The goal cell is exempt from occlusion. Four of 150 first-path goal cells lie inside a tower box: (6, 49) for MiniPekka and Skeletons, (7, 49) for Skeletons. Across all recorded lists 81 paths end inside an occlusion box, always as the final cell and never as an interior one. A planner that refuses occluded cells without exempting the goal finds no path at all for the short-reach cards.
6.5 The goal cell¶
The returned list ends at the first cell on the path whose centre is within
Range + CollisionRadius of the target's centre point, not its footprint, and not the
sum of both collision radii.
Verified as a two-sided condition (goal in reach and its predecessor out of reach) on
150 / 150 first paths, including the meet/ traces with moving targets and side-1
units. Rivals: "Range only" 1/140, "Range + both radii" 0/140, "to the footprint edge"
0/140.
The observed stop cells from the identical deploy tile (3.5, 8.5), all approaching the enemy left princess tower at (3500, 25500):
| Card | Range | CollisionRadius | Reach | Goal cell | Distance to target |
|---|---|---|---|---|---|
| Giant | 1200 | 750 | 1950 | (6, 47) | 1767.8 |
| Knight | 1200 | 500 | 1700 | (6, 48) | 1274.8 |
| Golem | 750 | 750 | 1500 | (6, 48) | 1274.8 |
| HogRider | 800 | 600 | 1400 | (6, 48) | 1274.8 |
| MiniPekka | 800 | 450 | 1250 | (6, 49) | 790.6 |
| Skeletons | 500 | 500 | 1000 | (6, 49) | 790.6 |
Two caveats that belong with the rule:
- The constant is pinned only to about ±75 units. A constant offset
kadded toRange + CollisionRadiusfits every one of the 150 first paths for anyk ∈ [−125.2, +24.8). Zero is inside that window; so are several other values. - The rule is necessary, not determinative. It is a condition on the recorded path's last node, not a predictor of which cell the search will stop at. Between 12 and 52 cells per sample satisfy it (median 32). The recorded goal cell is not the cheapest reachable in-reach cell in 114 of 140 samples, with no cost ties among them. Handing an A* the recorded goal cell raises exact-sequence reproduction from 22/140 to 43/140. The goal cell is an output of the expansion order, not an independent rule.
For a target that is below the unit, the sign flips: the standoff is on the approach
side. In meet/Knight_vs_HogRider ticks 250–273 the Knight's target sits below it and the
goal row is cell_centre_y + reach. An unconditional minus puts the goal on the far side
of the target.
The goal column is the unit's own column clamped into the target's collision box. The
half-width is only bracketed to (500, 1000] by the traces (750, 900 and 1000 score
identically at 98.96 % per-tick). 1000 is corroborated by csv_logic/buildings.csv
column 91, which gives CollisionRadius 1000 on every PrincessTower variant and 1400 on
KingTower. Read it per target; do not hard-code 1000.
6.6 Replan cadence: event-driven, no timer¶
Across 150 units and 31 859 path ticks there are 150 structural recomputes, 0.47 per 100 path ticks, evenly spread across families (walk 0.00, lane_sweep 0.46, building_Giant 0.57, repath_Giant 1.03, meet 0.69). Every one is explained by exactly two triggers, and the rule was tested as a prediction, not a post-hoc label:
Trigger 1: the goal cell moves. 137 / 137 predicted goal-column flips are followed by a replan, with 0 predicted flips that produce none and 0 replans without a predicted flip other than the two building drops. Lag histogram {1 tick: 128, 2 ticks: 9}, mean 1.07. The flips happen as the unit crosses x = 3000, 3500, 4000, 14000, 15000 and the column clamp moves. The goal row never moves in any of the 139 recomputes.
The trigger is not specific to the unit's own motion: in meet/Knight_vs_HogRider a Knight
chasing a moving HogRider recomputes at ticks 169, 172, 176, 180, 185, 189, gaps of
3,4,4,4,5. That looks periodic until you classify them, and all six are goal-cell moves
driven by the target's motion.
Trigger 2: a friendly building comes into existence. Lag 0 ticks from the entity,
+1 from the accepted command. In both repath_Giant traces the command is accepted at
tick 160, the Cannon entity first appears in a frame at tick 161, and the Giant's path is
already replanned in that same frame. An engine that waits a tick would be wrong.
The two repath_Giant ticks are the strongest occlusion witness in the corpus and are easy to
miss: a filter that looks for the node list growing skips tick 161
(35 → 35 nodes) and picks up ticks 389 and 290 instead. By that point the Giant has
walked past the Cannon and both paths are dead-straight single columns that any model
reproduces. At tick 161 the two traces diverge in exactly the right way: with the Cannon at
(3500, 11500) (box cols 5–8, rows 21–24) the route detours to column 4 for those rows. With
it at (2500, 11500) (box cols 3–6) the route swings out to column 7. Each path avoids its
own box and violates the other's.
The replan is whole-route, not a spliced local bypass: in the building_Giant traces
the detour appears in the first path the unit is ever recorded with, ten rows (5 tiles) ahead of
it. In repath_Giant/row11.5_dx+0.0 the replan changes rows 42–46, far beyond the
obstacle. Which side it takes falls out of the cost, not a handedness constant. The two
informative offsets go opposite ways (dx+0 left at 210.28 vs 217.36; dx−1 right at 202.43
vs 232.54), reproduced at a second y by the repath_Giant pair. Note that in both cases
the cheaper side is also the least-lateral-deviation side, so the traces do not separate
those two hypotheses. Only the negative claim (not a fixed handedness) is established.
Friendly troops trigger nothing. In meet/Knight_then_Giant_behind a Giant walking
directly behind a friendly Knight has 2 structural path changes in 316 path ticks. Troops
are handled by avoidance_offset, not by the path grid.
The caveat that cannot be removed. A recompute returning an identical list is invisible:
8 of 94 tail-surviving recomputes leave path_segment_direction frozen and
path_node_consumed clear, so a same-result replan leaves no fingerprint at all.
PATHFINDING_SAMEPATH_EPSILON = 3 may be suppressing near-identical results. "No periodic
replan" is a statement about observable path changes.
6.7 Timing of the first path¶
Target acquisition, the first path and the first step all land on that same tick: 149/150 units, with zero ticks anywhere in the corpus holding a non-empty path and a null target. The path is computed from the unit's pre-move position: the first surviving node is at Chebyshev distance 2 from the pre-move cell on 140/140 lane-sweep and walk units, but distance 1 from the post-move cell on 15 of them.
The path is not created once and then only shortened. In meet/Knight_vs_HogRider the
Knight's path is emptied and re-created four more times (ticks 250, 271, 273, 313) while it
holds a target throughout, as it drops in and out of the attacking state. The gate is
"never build a path while target is None", not "build the path once at acquisition".
6.8 What we still cannot reproduce: the node sequence¶
The cost model is right: the recorded path is exactly cost-minimal on 150 / 150 first-paths. But the exact node sequence is not reproduced. The best configurable A reaches 22 / 140 exact matches (43/140 when handed the recorded goal cell). Once the corpus is deduplicated to 76 distinct experiments the ceiling is 13 / 76*, of which 6 are the walk traces (one start, one straight column) and 6 are a single lane-sweep column.
The search space already explored: 5 heuristics × 5 queue tie-breaks × 8 neighbour orders
× relax-on-< vs <=, forward and backward, goal as a reach test on pop and as a fixed
cell with truncation (400 configurations). Plus 640 more with open lists ordered by cell
index ascending/descending, by g ascending/descending, and h-then-index (plausible shapes
for a REFRESH_OPENNODES linked or array open list rather than a binary heap). The ceiling
does not move.
Where the optimum is ambiguous the recorded path takes the orthogonal successor on 1568 of 1672
steps (93.8 %), counted over the offline trace corpus of client 15.535.29. But an ortho_first neighbour order still scores 22/140, so this is a
description of the output, not the rule. With the √2 diagonal, exact ties are rare, which
means the residual error is not a classic tie-break at all. Something structural is
missing: most likely the real open-list discipline, or a post-processing step.
A typical failure: c00_r08 agrees for 16 nodes, then the recorded path moves from column 6 to
column 5 at row 35 while the model stays on column 6. The switch row varies with the start
cell for the same goal (row 35, 36, 37, 44, 48 for starts c00, c02, c04, c08, c06), the
signature of an expansion-order artefact.
6.9 path_nodes is a post-tick snapshot¶
142 ticks in the corpus have a movement_direction that the heading law does not explain
from the recorded node list. All 142 carry path_node_consumed == 1, and 140 of them
are explained exactly by a cell 1–2 rows nearer in the same column:
| Explaining offset from the recorded tail | Count |
|---|---|
| (dcol 0, drow −1) | 85 |
| (dcol 0, drow −2) | 50 |
| (dcol −1, drow −1) | 3 |
| (dcol +1, drow −1) | 2 |
| unexplained | 2 |
The list also grew across 47 of those ticks. The mechanism, verified tick by tick on
lane_sweep_Knight/c08_r12 t226–t228: the unit repaths at the start of the tick and the new
path inserts one or two extra half-tile nodes below the old waypoint. The unit aims at the
nearest one and consumes it within the same tick, so that node appears in neither the
t−1 nor the t frame. At t227, dir = (−173, 188) = norm256((3750, 16750) − (3998,
16481)) exactly, and (3750, 16750) is cell 1195, absent from both lists.
Two consequences. First, the heading law is 100 %, not 99.54 %, and there is no
bridge or KS_POS_TO_TARGET exception to chase. These ticks cluster on the river rows
only because that is where the bridge approach forces a repath. Second, and more
importantly for anyone fitting a pathfinder: the recorded path_nodes is a post-tick
snapshot, a repath can insert nodes that never appear in any frame, and the effective
consumption count in a tick can exceed the one visible in the lists. Any pathfinder
fitted to the recorded node lists is missing those insertions. (The step law holds on all
142 of these ticks, so movement is untouched; the defect is purely "which node was
current".)
A large single-tick heading swing is sometimes attributed to proximity: "31.57° with the unit 113.4 units from the node centre". The measurement's own output says 1293.0 units for that sample, at which a 60-unit step can rotate the bearing by at most 2.66°. The proximity story is arithmetically impossible. The cause is the mid-tick repath above. Across all 128 lane-sweep traces, 0 of 127 heading changes greater than 10° with a genuinely unchanged target node are consistent with the heading law at both ticks.
7. What survived, what did not¶
Each analysis was re-measured from scratch by an independently written implementation (loaders, decoders, integer arithmetic and Dijkstra rewritten), so that a shared bug could not hide in all four. The verdicts below are from that re-measurement, over all 150 units.
Survived unchanged¶
| Claim | Strength |
|---|---|
TICK_MS = 50, one position write per tick |
Free-sub-speed fit: no integer speed survives at n = 2..5 |
SPEED_TO_SUBTILES_PER_TICK = 18 (Speed is millitiles/tick) |
Unique integer fit per card over 1..5000; direction-free cross-check agrees |
Position update: per-axis trunc(S·dir/256), remainder discarded |
Zero drift over 106–308 ticks; floor 0/1000, round 17–43 |
Heading: norm256(node_centre − pre-move pos) with floored integer length |
100 % once mid-tick repath insertions are accounted for |
| No ease-in, no acceleration, no partial first step | Fitted S reproduces the first moving tick exactly |
| Heading is stateless; no turn-rate limit binds | Stateless model reproduces 1242/1242 walk ticks |
| Half-tile 36 × 64 node encoding, goal-first, popped from the tail | 3781/3781 exact segment-direction identity |
path_segment_direction is sticky, frozen at assignment from the post-move position |
Zero exceptions on walk traces |
| Cost model road 5 / plain 8, water blocked, 8-connected | 0/150 suboptimal; flat-8 96, water@7 25 |
Occlusion = half-open AABB at CollisionRadius, per-axis |
0/3776; closed box 57 blocked-but-used |
Goal truncation at Range + CollisionRadius to the target centre |
150/150 including moving targets and side 1 |
| Replan is event-driven: goal cell moves, or a friendly building spawns | 137/137 predicted flips, 0 over-predictions |
| Whole-route replan, not a local splice | Detour in the first recorded path, 5 tiles ahead |
first_move = first_seen + DeployTime/TICK_MS |
149/150 units |
Refuted or materially corrected¶
| Claim as stated | Verdict |
|---|---|
"The step law fails iff avoidance_offset != 0" |
Refuted. 174 failures with every flag clear: 137 crowd separation (state 1), 36 combat pushback (state 2), 1 at state 4. Nothing in the corpus records the push |
"During avoidance the unit still moves at exactly S" |
Refuted. |step| reaches 61.39 for S = 60, above the 60.06 truncation ceiling; ±2.3 % |
"avoidance_offset is set to ±190 and decays by 10/tick" |
Refuted. A ±10 walk bounded at ±190; 16 of 36 runs ramp back up; runs reach 67 ticks |
| "The model reproduces every walk trace bit-exactly" | Refuted on coverage. 6 files, 7 units: two of the three Skeletons fit no integer S |
"path_node_consumed is a one-tick latch; an unchanged-tail recompute does not set it" |
Refuted. Runs up to 257 ticks; unchanged-tail recomputes split 95 set / 2 clear |
| "The diagonal cost is not settled by these traces" | Refuted. It is settled: √2, window [1.38, 1.48]; uniform diagonal fails 3 paths |
"Occlusion is inflated by the walker's own CollisionRadius" |
Refuted. A pad of 1 unit blocks 155 used cells |
| "Reaction to a new friendly building is 1 tick / 50 ms" | Corrected. 0 ticks from the entity existing; tick 160 is the command, 161 is the spawn and the replan |
"Path computed at deploy_tick + DeployTime/50" |
Corrected. spawn_tick + DeployTime/TICK_MS; the Golem's elixir wait makes the two differ by 14 ticks |
| "The first two path cells are always consumed immediately" | Corrected. A consequence of the 1000-unit standoff; both repath_Giant first paths drop only one |
| "Node consumption is an integer remaining-distance counter" | Refuted. Wrong pop tick on 518/3550 segments vs 271 for live Euclid |
| "The princess tower's 2×2 footprint is a new tower-specific rule" | Corrected. It is the ordinary rule at the shipped CollisionRadius 1000 |
| "The king tower's 3×3 footprint is measured" | Refuted. R_king = 0 scores identically to 1400; no trace goes near a king |
| "Enemy buildings demonstrably do not occlude" | Not established. Indistinguishable from friendly-only once the goal cell is exempt |
| "The goal rule alone explains every observed goal cell" | Overstated. Necessary, not determinative: the goal cell is an output of the expansion order |
| "The detour side is the cheaper side (5 offsets)" | Overstated. Only 2 of 5 offsets involve a detour at all, and in both the cost and least-deviation hypotheses agree |
| "Giant duty 0.8649 → 44.97 units/tick, undershoots Speed 45" | Wrong arithmetic. 9 stop ticks per 74, duty 0.8784, mean 45.68. It overshoots |
| "The stomp schedule is exact" | Scoped. Exact for an unobstructed path-follower; a jostled Giant moves on 2 scheduled pause ticks |
| "The stomp formula is solved (high confidence)" | Scoped. Two algebraically distinct rivals agree on every shipped card; rounding mode undetermined |
| "117 heading failures are a river/bridge exception" | Resolved. All 142 are mid-tick repath insertions; the heading law is 100 % |
A corpus pitfall worth keeping¶
The convenient loader latches the first non-tower entity of one side and follows only that one.
It hides 107 step-law failures inside the walk/Skeletons trace, makes every opposing-side unit
invisible, and skips the meet/ directory entirely. Those are the only traces with two mobile
units, a moving target, or a side-1 walker. Enumerate (side, generation_key) pairs instead. It
is fifteen lines.
8. Open questions¶
Open as of this corpus. Items 1 and 2 have since been settled on client 16.402 (the expansion
order is reproduced outright and the contact law is measured), and items 5, 6, 9 and 10 were
settled by the same captures. pathfinding.md carries all of them. The rest are open, and the
experiment each one needs is named.
- The tie-break / expansion order. 13/76 distinct experiments is the ceiling of a 1040-configuration search over the offline trace corpus of client 15.535.29. The 100 % below is the target of a proposed experiment, not a result. This is the single thing standing between the measured cost model and bit-exact path reproduction. Best next experiment: traces with a unique optimal path everywhere (pepper the lane with friendly Cannons so no ties exist) to confirm the cost model to 100 %, and traces with exactly one two-way tie to read the tie-break off directly.
- The collision / separation law. Two contact regimes break the movement law and
neither sets a flag, and no field records the push, so it must be fitted from the
residual. The data already exists and
nobody has used it:
walk/Skeletonsunits #1 and #2 (crowd separation, 107 clean samples) and all threemeet/traces (pushback, 36 samples plus 11 in state 1 at exactly 2 ×CollisionRadius). - What
avoidance_offsetactually does. The magnitude is roughly preserved, the offset walks ±10 within ±190, but the applied deflection fits no constant rotation scale (observed ratio 0.327–0.387, non-linear). What sets it to ±190, and what selects the sign, is unknown. - The residual 4 % of node pops that fire up to 74 units late. 60 of 148 carry avoidance; 88 carry nothing. Likely the same fixed-point arithmetic that produces the Giant/Golem 52/54 mapping. A live capture carrying the mover's state fields would settle it faster than another trace.
- Occluded cells: hard block or
PATHFINDING_BUILDING_COST = 50? Indistinguishable here because no recorded path ever needed to cross one. Same for bit-16 terrain and for water (50 vs impassable). Settle with a corridor where going through is cheaper than going around. FRIENDLYONLY_OCCLUSIONS. The globals say TRUE; the traces cannot tell. Needs a ground unit routed past an enemy building that is not its target.- The goal rule's free parameters. The reach constant is pinned only to ±75 units; the column clamp half-width only to (500, 1000]; the row rule rests on six data points, one per card, all from one start and one approach direction. The cheapest discriminating experiment is a Skeletons (Range 500, radius 500) in a lane 750 units off the tower centre, where the per-axis model says row 49 and a 2-D in-range model says row 50. A side-on or from-above approach would settle the axis question at the same time.
- Rule A vs rule H for occlusion. "Square of half-width
CollisionRadius" and "the building's own tile grown by half a tile" predict identical cells here, because every occluder in the corpus sits on a tile centre. Place a building at a half-tile x offset (x = 3000 or 4000), or one with a different radius (Tesla, Bomb Tower 600, Barbarian Hut), and re-run the six-offset sweep. - The stomp rounding mode. One
IceGolemitetrace (predicts 52 under floor, 53 under round) and oneGoblinGianttrace (predicts 69, testing the formula outside Speed 45). SpeedMultiplier. Completely unmeasured in this corpus. One rage trace would calibrate the buff path. Measured since on the 16.402 corpus (movement.BUFF_SPEED_RULE;pathfinding.md).- Does building removal trigger the same 0-tick replan? The Cannon's
LifeTimeis 30 000 ms and the traces end before expiry. PROJECTILE_SPEED_TO_SUBTILES_PER_TICK. No projectile flies in these traces. The troop side measuring Speed/50 is weak supporting evidence for 18 on projectiles too, given the 2023 frame test that measured ×1.2 for both a Musketeer projectile and a Giant. But it is not measured.- Path length cap. The schema allows 115; the longest route these deploys can produce is 46. No experiment in the corpus can reach the cap.
9. Using this in the engine¶
The implementation contract derived from these measurements (grid, costs, heuristic, occlusion,
goal, per-tick update order, node consumption, replan triggers and the verification gates) is in
pathfinder-spec.md. Section 10 of that file maps each rule onto the ledger key that carries
it.
The regression gates these measurements support, in increasing strictness:
- All 150 recorded first-paths are exactly cost-minimal on the engine's own grid.
- The movement laws reproduce every isolated unit bit-exactly from its first moving tick (walk: 239, 307, 305, 164, 121, 106 ticks; zero drift; identical final positions).
- The node-consumption predicate never fires early across the 27 824 keep-ticks.
- Goal truncation holds two-sided on all 150 first-paths.
- Exact node sequences: out of reach for this model (13/76), gated on the 16.402 arm instead
(
pathfinding.md).
10. Reproducing¶
These figures were measured over the offline corpus, which is not distributed, so the run itself is not reproducible from a clone. What it covered, section by section:
| Section | Content |
|---|---|
| A | Node consumption: pre-move vs post-move, threshold sweeps, rival predicates |
| B | Step and heading laws over all 150 units; the 142 heading anomalies and their explaining cells |
| B2 | Free per-unit speed fit with no card table |
| C | x2/y2, avoidance_offset deltas and runs, path_node_consumed census |
| D | First path / first target / first move timing |
| E | Diagonal weight sweep in exact rational arithmetic |
| F | Cost-axis discrimination at √2 |
| G, G2 | Occlusion radius brackets, box shape, mover pad, goal exemption, enemy occlusion |
| H | Goal truncation rule and its admissible constant window |
The rules these sections support are gated inside this repo by tests/oracle2026.rs and
tools/oracle_diff.py, which a reader can run against any trace in the recorded-trace format.