Goal-directed pathing diagnostic — 9 September 2026
The AI reads a forward movement-distance field as though it were distance to a goal. This is incorrect when entering terrain costs more than leaving it, or climbing and descending have different costs or legality. The board routine itself implements its documented forward semantics correctly. This finding does not establish the cause of a particular matchup loss or show that correcting the heuristic improves balance.
Reproduced discrepancy
A read-only probe used a 3-column, 1-row board, with ground at offset (0,0) and the goal at (1,0). Board.reachable(start, 10, new Set(), jump) supplied the actual safe route cost. The AI's current estimate is Board.distanceField([goal], jump).get(start). There were no occupants or alternate routes affecting these examples.
| Route | Actual safe cost | Current goal-seeded field at start |
|---|---|---|
| Ground → level 1, ordinary legs | 2 | 1 |
| Level 1 → ground, ordinary legs | 1 | 2 |
| Ground → level 2, Jump legs | 1 | Absent |
| Level 2 → ground, Jump legs | No safe route | 1 |
| Open ground → rubble, ordinary legs | 2 | 1 |
For the level-2 example, the field cannot expand from the roof to ground because that descent is a fall. The reverse journey is a legal Jump climb. The same reversal also misprices ordinary level-1 climbs and destination terrain. Jump legs do not remove the two-level descent restriction.
engine/board.ts documents distanceField as distance from its sources. choosePosition in engine/ai.ts seeds that routine with enemy positions and unheld zone centers, then subtracts the resulting values as remaining approach cost from each candidate position. Missing field entries currently fall back to geometric hex distance. The probe's “Absent” therefore does not mean the AI rejects that position outright; it means the purported walking-distance estimate is unavailable and the fallback takes over.
The reported Foundry example—armored versus melee, seed 300000, first side A, both aggression 0.75—motivated this check. Its end-corridor movement and lack of melee damage remain symptoms with potentially several causes. No corrected-policy game was run for this diagnostic, and the active screen's engine was not changed.
Optional candidate after the frozen screen
Add a separate goal-directed field or an explicit direction option with existing forward behavior as the default. Reverse Dijkstra starts at the goals and considers each adjacent predecessor v of the popped cell u. Its edge weight and legality must be those of the original move v → u, including the destination terrain at u, upward climb surcharge/limit, low-wall exclusion and rejection of falls. It must not simply negate elevation while retaining the wrong destination terrain.
Expose the new field only through an explicit study policy/candidate. Preserve the existing forward API and default behavior, and include direction in the field-cache key. Do not modify the existing fallback, goal selection or tactical scoring in the first candidate: those are separate hypotheses. Record the changed source fingerprint and rerun a matched baseline under the new executable before comparing the candidate. Any session exposure needs an explicit capability distinction; old saves must retain their recorded policy.
Acceptance checks before measuring the candidate:
- Reproduce all five cases above with correct goal-directed estimates.
- On a small board containing mixed terrain, heights, roofs and blocked cells, compare each goal-directed value with a safe
reachablesearch from that starting hex, for ordinary and Jump legs. Missing entries must also agree. - Verify multiple goals choose the cheapest reachable goal, goals cost zero, and neither field uses occupancy or falls unless explicitly specified.
- Preserve existing forward-field tests and exact default event/future-RNG goldens. Confirm cache separation and destroyed Jump-leg handling.
- Measure the candidate on matched seeds and the same concrete rosters/maps/policies. Report movement cycles, engagement, contribution, scoring and outcomes. A corrected cost model is not by itself evidence of improved game balance or stronger human strategy.
This is a diagnostic and proposed experiment, not a live-rule change.
Implemented optional candidate
After the original 30-condition screen completed and its source hashes were verified, Rules.goalDirectedPathing: true was added as an opt-in bot policy. Board.distanceToGoals traverses reversed edges; GameState.distanceToGoals caches those fields under a separate direction key. The AI selects this method for its existing enemy/zone goals only when the flag is explicitly true. Goal selection, tactical scoring, geometric fallback, existing forward fields and defaults are unchanged.
Session exports using the flag had the distinct local-prototype-4-goal-pathing capability, including configurations combined with command reserve or defensive Enraged. Import rejected an old capability with the new flag or a goal-pathing capability without a true flag. That protected recorded policy; it did not claim that an old reader supported the candidate.
Superseded 2026-09-16. The prototype keeps no save compatibility. The engine string is now local-prototype-5, and there are no capability markers. A save carries every rule key. Restore refuses a save that is missing a key, and it refuses a save that no longer replays. See the simulator design.
Validation: the new field tests first failed before implementation, then all 15 focused goal-pathing tests passed. The broader command below passed 83 tests across six files, including existing forward fields, AI behavior, historical full-event/future-RNG goldens and session capabilities. Typecheck passed. Exhaustive small-board tests compare every passable start against independent safe forward reachability, for both leg types and multiple goals. Correct field costs alone do not establish a better policy or balance; matched study comparisons remain separate evidence.
pnpm exec vitest run engine/goal-pathing.test.ts engine/board.test.ts engine/ai.test.ts engine/defensive-lifecycle.test.ts engine/command-reserve.test.ts engine/session.test.ts
pnpm typecheck
Separate preview follow-up
The exported plannedMove preview helper currently passes printed Jump capability to reachable even when Legs are destroyed. Production choosePosition already disables Jump in its distance-field selection for destroyed Legs. Keep this preview discrepancy separate from the goal-direction candidate: correcting its reachability is a follow-up UI/test-helper fix, not part of the field-direction experiment or its default behavior.