mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:50:18 +00:00
webgpu-worker-flawed
4 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6bd95d4884 |
Pathfinding - optimize naval invasions (#2932)
# Pathfinding pt. 4 https://pf-pt-4.openfront.dev/ ## Description: Hello again! Pathfinding. It's fast, but inaccurate. This PR makes it more accurate and actually faster. Sadly it is _faster_ because of a blunder in previous PR (using BucketQueue where MinHeap would be better), not because of a new tech. More importantly, it is more accurate. And that's what people apparently want. ## What changed? Most of the functional changes relate to `SpatialQuery` module. This is the thingy that answers "we know the target, which tile of my territory is the best to launch an invasion". To make it compute a path from South America to the deep inland China river, it has to work on a coerced map, one with a very small resolution, so small in fact, that every 4096 map tiles gets compressed to just one pixel. I hope you see where this is going. Previously we selected a random coastal tile within this big pixel (honestly it wasn't random at all, but could very well be for the illustrative purposes). Now, we try to be a bit more deliberate. Since we already know the rough location of the probably best tile, we can exclude all other tiles from the computation. Imagine a player's territory spans both Americas on global map - that's a lot of shores. But since we already know the best tile is somewhere close to Miami, the problem space was greatly reduced, no need to consider all other shores. But pathing to the target in China from Miami is still crazy expensive. This is where second trick comes to play - instead of pathing all the way to China, we select a _waypoint_ in the rough direction of China, about 100 to 200 tiles away. This way we fairly cheaply select best tile to launch an invasion towards this abstract point. And chances are, this point is far enough, the newly computed path is very close to being optimal. When you throw a dart from far away, the difference between scoring 10 and missing is very small. This is why aiming in the general direction of the board - as opposed to the ceiling - is usually good enough. ## Okay, but what about the crazy paths when I send invasion to the opposed bank of a river?! Well, pathing from America to China is cool, but most players wouldn't notice the difference on such long paths, what about the short ones? We now try more accurate pathing first and defer to hierarchy only if it fails. This produces much better paths for short invasions. While the fix described above ensures the accuracy is improved also on medium-to-long routes. ## Playground Yes. https://github.com/user-attachments/assets/9cf9586f-c99a-416d-b856-8cf0a21c35ed ## CodeRabbit Grab a 🥕. Remember `tests/pathfinding/playground` is mostly generated code and go easy on it. It's enough for it to work and do it's job of visualizing the paths. No need for throughout review of these files. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: moleole |
||
|
|
85def73bd9 |
Pathfinding Refinement (#2878)
# Pathfinding pt. 3 ## Description: This PR introduces final change to the pathfinding - path refinement. It optimizes Line of Sight refinement by searching with for the best tile with a binary search instead of linearly. And then spends the recovered budget on better refinement of the first and last 50 tiles of the journey - the place where user is most likely to look at. Additionally this PR re-introduces magnitude check and makes the ships prefer sailing close to the coast, but not too close. ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## What? | Before | After | | :--- | :--- | | <img width="1097" height="1117" alt="image" src="https://github.com/user-attachments/assets/4a0b300d-10ef-4151-b6dc-33acfb49f992" /> | <img width="1093" height="1119" alt="image" src="https://github.com/user-attachments/assets/cf81c515-c145-40f4-91e5-a4353986907b" /> | | <img width="1096" height="1129" alt="image" src="https://github.com/user-attachments/assets/21b46bce-f961-4259-88f6-fe4a66180270" /> | <img width="1098" height="1126" alt="image" src="https://github.com/user-attachments/assets/d92587d1-e6b6-4353-b4a4-1efe71bca43d" /> | ## Performance There is actually a severe performance impact of these changes. The path initial path takes almost 2x as long to generate - this is because pre processing can only do so much if the initial path is ugly. Luckily in real gameplay we only need to do this calculation once per edge, so the actual observed performance impact should be much smaller. Cache FTW. | | No Cache | Cache | | :--- | :--- | :--- | | Before | 277.04ms | 208.58ms | | After | 498.34ms | 264.27ms | ## DebugSpan Small utility, it allows any code to be easily instrumented for performance. The idea is the same as with [OTEL Spans](https://opentelemetry.io/docs/concepts/signals/traces/). Produce a span, create sub-spans, measure whatever you need. Works only when `globalThis.__DEBUG_SPAN_ENABLED__ === true`, otherwise no-op. Cool stuff, try it out: ```ts // Convenient wrapper, small performance impact return DebugSpan.wrap('add', () => a + b) // Synchronous API, basically free DebugSpan.start('work') work() DebugSpan.end() // Create sub spans DebugSpan.wrap('complex', () => { const aPlusB = DebugSpan.wrap('add', () => a + b) DebugSpan.set('additionResult', () => aPlusB) // Store data return aPlusB * c }) // Access spans, data and timing const span = DebugSpan.getLast() const compelxSpan = DebugSpan.getLast('complex') console.log(complexSpan.duration, complexSpan.data['additionResult']) ``` These are virtually free and can be enabled on-demand **in production** and available in the devtools. Under the hood devtools integration is just a wrapper around [Performance API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API). For clarity data keys not prefixed by `$` are omitted from the integration. Every key prefixed with `$` must be fully JSON serializable. <img width="977" height="799" alt="image" src="https://github.com/user-attachments/assets/b4d43506-1639-4f78-a611-30e61de12a07" /> |
||
|
|
0e3ced3bfa |
Pathfinding Refactor pt. 2 (#2866)
## Playtest https://pf-pt-2.openfront.dev/ ## Pathfinding Refactor pt. 2 <img width="1536" height="1024" alt="image" src="https://github.com/user-attachments/assets/9477958e-54b7-4c83-b317-ba789e809e9e" /> This is a follow-up to a previous PR introducing pathfinding changes. This time, it introduces a complete refactor of `pathfinding` directory and breakdown into composable pieces. ### Unified PathFinder interface `PathFinder<T>` and `SteppingPathFinder<T>` are introduced to unify **all** pathfinding across the application. First one exposes complete path, while stepping variant allows the callee to iterate over the path by calling `.next`. All pathfinders share this one common interface, which makes them easy to use in any scenario - `PathFinding.Water(game).search(from, to)`. `SteppingPathFinder<T>` extends `PathFinder<T>` with an ability to iterate over the path. It handles caching, storing current index and invalidation. This allows the units to not care about the inner workings of the pathfinder and just call `pf.next(current, target)` and receive instructions on what to do next. ### Common entry point All pathfinders are now exposed from common `PathFinding` entrypoint: - `PathFinding.Water` - `PathFinding.Rail` - `PathFinding.Stations` - `PathFinding.Rail` Additional entry point is introduced for pathfinders which need to work both in the worker, but also on the frontend, which lacks `Game` interface. Currently only `UniversalPathFinding.Parabola` is available. ### Spatial Query New module has been introduced close to `pathfinding` - `SpatialQuery`. It aims to resolve any questions game may have about finding tiles meeting criteria. Currently `SpatialQuery.closestShore(player, target)` and `SpatialQuery.closestShoreByWater(player, target)` are available - they help answering questions about naval invasion: "What is the best landing location from user's click?" and "Which our tile should be used to launch the transport ship?". Under the hood they use very similar mechanics to pathfinding, so it felt right to put them close by. ### Modular architecture Pathfinders now support transformers: `MiniMapTransformer`, `ShoreCoercingTransformer`, `ComponentCheckTransformer`, `SmoothingTransformer`. Transformers functions like a middleware in the pathfinding chain. They wrap around the pathfinder and provide additional functionality. This allows the pathfinder to focus on actually finding the path instead of doing unrelated things. Example chain for simple (A*) water pathfinding: ```ts static WaterSimple(game: Game): SteppingPathFinder<TileRef> { const miniMap = game.miniMap(); const pf = new AStarWater(miniMap); return PathFinderBuilder.create(pf) .wrap((pf) => new ShoreCoercingTransformer(pf, miniMap)) .wrap((pf) => new MiniMapTransformer(pf, game.map(), miniMap)) .buildWithStepper(tileStepperConfig(game)); } ``` The Pathfinder - here `AStarWater` - does not care about the conversion between minimap and main map tiles. It also does not care if the source or destination is a land tile. The transformers take care of that. The pathfinder gets a set of valid coordinates and produces the path - that's it. Modular approach makes working on a particular set of utilities much easier - for example map upscaling is handled consistently across all pathfinders. Additionally, the pathfinders are not tied to the particular map resolution used. Pass them a different map and they will work the same. ### Algorithms Algorithms used are neatly organized inside `src/core/pathfinding/algorithms`. They are prefixed with the algorithm name and suffixed with the use case. File without suffix exposes generic version ready to traverse any graph with adapters. Specialized versions either use an adapter or inline logic when performance is critical - using adapters leads to 20-30% performance loss. The directory includes `A*` and `BFS` but also other useful utils, such as `AbstractGraph` used to generate... an abstract graph on top of the tile map and `ConnectedComponents` helping to identify whether two tiles are connected by a path without actually computing the path. ### Playground The playground have been updated with new algorithms, including tweaked very greedy `A*`. <img width="2175" height="1424" alt="image" src="https://github.com/user-attachments/assets/1f833651-0024-4299-bf86-882f5368358c" /> ### Tests Yeah, there are some, a little too many if I say so myself. But there are no useless tests. I had to ensure refactored code works somehow reliably. This PR comes with trust me bro guarantee, but I would appreciate someone confirming **naval invasions, nukes (esp. MIRV) and warships**. ### Discord `moleole` GL & HF |
||
|
|
b090f2f624 |
HPA* Pathfinding (#2815)
## Pathfinding with HPA*
Hi! The primary objective of this PR is to replace per-tile A* with
hierarchical pathfinding - HPA*. In practice, this means we create an
abstract graph on top of the actual map with far fewer points and use it
to decide on general path structure. Only then we go back to tile-level
and build path between selected waypoints. This speeds up long distance
pathfinding by over 1000x in some cases. To make the review easier, it
comes with a benchmark and visual playground.
## PREPROCESSING
H part of HPA* means "hierarchical" and requires preprocessing.
This PR includes pre-processing as part inside `new Game()` constructor.
It takes about 135ms for `giantworldmap` on my machine, which increases
the effective initialization from ~95ms to ~230ms. This time could be
reduced in different ways, which are **out of scope** for this PR.
After confirming the initialization time is bearable on low-end devices,
I argue merging this PR as-is is acceptable tradeoff. It creates small
lag at the beginning of a round but pays for itself in the first minute
of the match.
## Nerdy details
**Architecture**
- HPA*-style hierarchical pathfinding
- 32×32 sectors on minimap with gateway nodes on borders
- Gateway graph built via BFS during preprocessing
- Water component optimization skips unreachable gateway pairs
- A* on gateway graph → local A* within sectors → Bresenham path
smoothing
- Minimap upscaling identical to currently used in MiniAStar
**Key Optimizations**
- Typed arrays instead of high-level primitives
- Stamp-based visited tracking (no need to recreate buffers, O(1)
clearing)
- Optional - enabled by default - caching of tile paths between gateways
- Line of sight smoothing for the final path
## Review Focus
Play with included tools, benchmark and visualization. Pathfinding
should be safe to merge as a black box - you do not need to understand
the details. Outcomes can be tested empirically in-game. Visualize (and
share!) edge cases with included playground. Confirm the 100x speedup is
real with benchmark.
If you plan to dive into the code, I suggest the following order:
- Pathfinding abstraction in `src/core/pathfinding/`
- Pathfinding tests in `tests/core/pathfinding/`
- NavMesh in `src/core/pathfinding/navmesh/` + integration with
`Game.ts`
- Benchmark in `tests/pathfinding/benchmark/`
Do not look at playground's code, it has been created with a clanker.
The design is 100% mine and I spent way too long polishing it, but I
haven't even once edited the code manually. There is probably no
abstraction whatsoever, just do not look at the code, let it play.
## Core Changes
#### Pathfinding (`src/core/pathfinding/navmesh/`)
- HPA* + refinement -> three phased pathfinding: A* over the graph ->
naive path -> refinement
- comes with A* and BFS optimized for for specific needs
#### Pre-Processing (`src/core/pathfinding/navmesh/`)
- identify water bodies to avoid pathfinding between disconnected nodes
- create high-level graph of gateways on top of tile map
#### Abstraction (`src/core/pathfinding/`)
- common `PathFinder` interface that can return full path and also act
as state machine (`.next()`)
- adapters for both new and legacy algorithm with fallback to legacy if
navigation mesh not available
#### Benchmark (`tests/pathfinding/benchmark/`)
- `npx tsx tests/pathfinding/benchmark/run.ts` - no guesswork, numbers
- `npx tsx tests/pathfinding/benchmark/run.ts --synthetic` - 1000s of
synthetic paths
- `npx tsc tests/pathfinding/benchmark/generate.ts` - generate more as
needed, test new maps
- includes ONE synthetic scenario to avoid PR bloat, generate more
locally / later
#### Playground (`tests/pathfinding/playground/`)
- `npx tsx tests/pathfinding/playground/server.ts` - visualize paths
with both new and legacy algorithm
## Benchmarks
### Compared with legacy in default - hand picked - scenario:
```
Initialization: 95.95ms -> 227.29ms
Pathfinding: 3038.43ms -> 6.45ms
Distance: 26972 -> 26810 tiles
```
### 42,000 synthetic routes across all maps
```
Running 42 synthetic scenarios with hpa.cached adapter...
✅ synthetic/achiran | Init: 93.42ms | Path: 139.07ms | Dist: 1481630 tiles | Routes: 1000/1000
✅ synthetic/africa | Init: 87.14ms | Path: 155.08ms | Dist: 1829414 tiles | Routes: 1000/1000
✅ synthetic/asia | Init: 57.60ms | Path: 112.55ms | Dist:
|