Improve path finding - #184
Conversation
|
I do not understand at all why we have these extra cost fields. Why not simply switching the existing gradient to u16 and multiply their number per "swim class" (compared to boolean CAN_SWIM)? So I'll ask the AI why we have duplication rather than updating our existing gradients. |
|
In general, while it is totally fine to do AI-based development for Glob2, we should really be a bit mindful for duplication and slop. Even frontier models (gpt 6 astra and fable 5.1) still have a very tunnel vision and would create a huge mess in the code if not kept in check and nudge towards simplicity/DRY. |
Pathfinding gradients become Uint16 fields built by a bucket-queue Dijkstra: a cardinal step costs 10, a diagonal 14, and water is charged at the unit's walk/swim ratio, bucketed into swim classes. Every gradient (resources, forbidden, guard and clear areas, buildings) is kept per swim class and built the first time a unit of that class asks for it. Units step to the neighbour whose value minus the step cost is highest, which also removes the 5x5 minigrad and the 32x32 local building grids: a building has one full-map gradient per class, rebuilt when the map changes nearby, when a unit is stuck, and periodically for clearing flags. The point-to-point A* uses the same costs. The Uint8 chamfer stays for the Castor and Warrush helper maps; Castor reads the corn gradient through a conversion to its own 8-bit scale. Fable 5.1 helped authoring this commit.
Walking, swimming and flying units advanced delta by their full speed on diagonal moves, so a diagonal covered 41 % more distance per tick than a cardinal move and units were faster zigzagging than walking straight. The weighted gradients price a diagonal at 14/10 of a cardinal step, so the timing has to match or units would be routed onto slower paths. Fable 5.1 helped authoring this commit.
26e5b1f to
cddad36
Compare
|
Restructured along those lines. The existing gradients are now the Uint16 fields themselves, indexed by swim class instead of the canSwim bool, and Map::propagateGradient (a bucket-queue Dijkstra) replaces the chamfer for them in place. There are no parallel cost fields, no per-team switch and no stats any more. The 5x5 minigrad and the 32x32 local building grids went with it, since a weighted full-map gradient makes them redundant: net -490 lines. The Uint8 chamfer stays only because Castor and Warrush run it on their own helper maps. The round-trip aware hiring is out of this PR; it can come separately if wanted. Description rewritten with the measurements against master (economy, ticks per second at matched unit counts, memory). |
|
I support the proposed gameplay direction, including swimming-aware weighted routing and the diagonal timing correction. I am happy for this to merge once we have tightened up the computational performance. We are currently benchmarking and profiling this against the existing pathfinder with my recent gradient optimizations, including a comparison with diagonal timing matched. That should help identify which costs remain and where further optimization is worthwhile. This is conditional support: the performance work and verification are still in progress, so please keep that as an outstanding condition before merging. |
Reuse wrapped neighbors, terrain costs and destination buckets while preserving traversal order. Add a randomized heap-solver oracle across all swimming classes and thin toroidal grids. Three paired release benchmarks show 22.5–27.0% less core CPU across four fixtures. Clean binaries match replay bytes and per-tick checksums for 60,000 ticks each; 167 unit tests and targeted ASan/UBSan checks pass.
|
Added a focused performance optimization to the weighted propagation loop. It reuses wrapped neighboring coordinates, the two terrain costs, and their queue buckets for each popped cell. Neighbor visitation remains NW, N, NE, E, SE, S, SW, W, preserving queue insertion order. No changes to weights, swimming classes, scheduling or diagonal timing. Measured against this PR's original head
Whole-process elapsed time fell by about 17–20%. Separate function-timing runs showed about 35–39% less time in Dijkstra propagation, with identical call counts. Core timing excludes AI order generation, order execution, loading and replay output; function profiles include all calls, so their totals are not interchangeable. No new persistent memory or production profiling code. Validation: 167 unit tests pass. The new test compares 700 randomized fields against an independent heap-based shortest-path solver, covering all seven classes, water, obstacles, mixed seed costs, absent sources, toroidal wrapping and thin/rectangular maps. ASan+UBSan checks pass with the changed propagation and test translation units instrumented (remaining linked objects were not instrumented). Clean release binaries produce byte-identical replays and complete per-tick checksum sidecars across all four fixtures, 60,000 ticks per binary. This also compares favorably with my optimized existing gradients on these fixtures, but the weighted/old routing versions produce different populations; the before/after table above is the stronger comparison because the simulation results match exactly. These measurements do not cover 512x512 or late-game worst cases. For #193's rebase: its direction-dependent lane penalties must remain per edge; the two shared terrain costs alone do not include those penalties. |
|
Pushed a second behavior-preserving optimization, On the AI-heavy 256×256 BigArena fixture, three alternating paired 15,000-tick release runs against
The core timer excludes most AI order generation; the core regression is reported explicitly, so this is not a universal speedup. Separate diagnostic profiling showed helper propagation falling from 3.337 s to 2.496 s with the same 3,642 calls. Numbi-only workloads do not use these helpers. Validation: clean before/after releases produced identical replays and every per-tick checksum on four fixtures, totaling 60,000 ticks per binary. Both passed the independent helper oracle covering 3,000 random fields plus thin/toroidal maps, obstacles, cutoff, inert inputs and mixed seeds. The harness is added to Linux CI. The targeted ASan/UBSan harness aborted in the local Homebrew SDL2 initializer before the tests, so that check is incomplete. Also tested queue-ring sizing, direct candidate-gradient comparisons, early downhill rejection and compact terrain-cost buffers. None showed enough repeatable benefit to retain. This follows the earlier 22–27% core-CPU reduction; further straightforward changes are showing diminishing returns. Measurements are Apple M3 / Apple Clang release builds, with final pairs isolated from a competing build that disturbed screening. |
|
Final code-quality follow-up is pushed as
Validation: 170 unit tests passed; actual Final Linux/Windows CI is running; merge will wait for those checks. |
Draft. Replaces the chamfer gradient descent with weighted distance fields, in place: the existing gradients become
Uint16and are kept per swim class instead of percanSwimbool.What changes
resourcesGradient,forbiddenGradient,guardAreasGradient,clearAreasGradientandBuilding::globalGradientareUint16fields indexed[team][swimClass]. A cell holdsGRADIENT_AT_GOAL - cost, cost in tenths of a land step (10 cardinal, 14 diagonal, water at the unit's swim-class rate), so "higher is closer",0obstacle and1unreachable keep their meaning (MapInternal.h).Map::propagateGradient(Dial's bucket-queue Dijkstra,MapGradientField.cpp) replaces the chamfer for pathfinding; theUint8chamfer stays only for the Castor and Warrush helper maps.Unit::swimClass()buckets walk/swim speed into 7 classes (0 = cannot swim); a gradient is allocated the first time a unit of that class asks for it, so unused classes cost nothing. The round-robin scheduler updates only the gradients in use.Map::directionByGradientpicks the neighbour with the highest value minus step cost; when blocked it may sidestep to an equal cell (deterministicsyncRand). This replaces the 5x5 minigrad and the 32x32 local building grids: a building has one full-map gradient per class, rebuilt when the map changes nearby (dirtyBuildingGradients), when a unit is stuck, and every 125 ticks for clearing flags (Map::buildingGradient).clearingFlagStep,localResourcesandupdateLocalResourcesgo away with it.deltaat speed/sqrt(2), so diagonals are no longer 41 % faster than cardinal moves (second commit; the octile costs assume it).pathfindPointToPointuses the same step costs and a total order on equal keys.Net: 43 files, +912 / -1401.
MapGradientLocal.cppandMapMinigrad.cppare deleted,MapGradientField.cppandtest/GradientTest.cppadded.Testing
Headless (
-test-games-nox), release build, Nicowar on every team. "old" is master's pathfinder and timing, "old + sqrt2" is master's pathfinder with only the diagonal timing fix, "new" is this branch. 10 game minutes (15000 ticks), 5 seeds each, mean units per team at the end:The diagonal timing fix alone costs master's units about 15 % of their economy on Mazury (they were moving 41 % faster on diagonals); the new pathfinder is within noise of "old + sqrt2" on both small maps and about even on the big one.
Ticks per second at matched unit counts (512-tick windows, same seeds, runs one after the other on an idle machine):
The 10-minute 512x512 game takes 128 s wall on master and 75 s here. One Dijkstra costs about what one chamfer pass costs, but the chamfer needs several passes on a big map and there is no 32x32 grid to rebuild any more; on the tiny map the per-class gradients cost a little at low unit counts.
Peak RSS on Mazury 4x4 (512x512, 8 Nicowars, 10 game minutes): old 140 MB, new 179 MB (one
Uint16field per building and swim class in use, instead of oneUint8field per building and swim variant plus the local grids).Determinism: same seed twice gives identical per-team timelines and end-of-game lines on 128x128 and 512x512; replays differ only in the known pointer-looking bytes of the Echo AI save in the header. 166 unit tests pass (8 new in
GradientTest); the CI harnesses (building footprint, selection, terrain, savegame safety) pass;server=1builds. A mixed Castor/Warrush/Nicowar/Numbi game (war flags, clearing flags, guard areas) runs clean.Known limits
Uint16costs cap a path at 6553 land tiles; hunger ends a trip after ~400 steps, so it never binds.