poe2-bot/docs/pathfinding.md
2026-03-07 09:53:57 -05:00

153 lines
5.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Nexus.Pathfinding — Navigation & Exploration
## Overview
Two classes: **NavigationController** (state machine — decides *where* to go) and **PathFinder** (A* algorithm — decides *how* to get there).
---
## NavigationController
### Modes
| Mode | Trigger | Behavior |
|------|---------|----------|
| Idle | `Stop()` | No movement |
| NavigatingToPosition | `NavigateTo(pos)` | Path to fixed world coordinates |
| NavigatingToEntity | `NavigateToEntity(id)` | Chase a moving entity (re-targets each tick) |
| Exploring | `Explore()` | BFS frontier exploration of unmapped terrain |
### Update Loop (called every tick)
```
1. Area change detection → clear path, explored grid, stuck history
2. EnsureExploredGrid() → allocate/resize to match terrain (preserves old data on expansion)
3. MarkExplored(playerPos) → mark cells near player as visited (radius 150 grid cells)
4. ResolveGoal() → get target position based on mode
5. If no goal and Exploring → PickExploreTarget() via BFS
6. Reach detection → within WaypointReachedDistance (80u), clear goal or stop
7. Stuck detection → if < 30u movement in 60 ticks (~1s), repath or pick new target
8. Pathfinding → A* from player to goal (with explored grid bias in explore mode)
9. Waypoint advancement → advance index as player reaches each waypoint
10. Output → DesiredDirection (normalized vector to next waypoint)
```
### Explored Grid
Parallel bool array matching terrain dimensions. Tracks which cells the player has visited.
- **Mark radius**: 150 grid cells (~1630 world units) — circular region around player
- **Preservation**: On terrain expansion, overlapping explored data is copied to new grid
- **Offset-aware**: Uses same OffsetX/OffsetY as terrain for absolute grid coordinates
### BFS Exploration (PickExploreTarget)
When Exploring mode needs a new goal:
1. **BFS frontier search** (up to 100,000 iterations)
- 8-directional BFS outward from player
- Finds nearest unexplored walkable cell
- Returns that cell as world coordinates
2. **Random distant target** (if BFS finds nothing)
- 20 attempts at random directions, 1500-3500 world units away
- Pushes player toward terrain edges where expansion triggers
3. **Edge fallback** (if random fails)
- Heads toward nearest terrain boundary (10 cells from edge)
- Guarantees continued exploration with infinite terrain
4. **Exploration complete** (only if all fallbacks fail)
- Sets `IsExplorationComplete = true`
- Prevents expensive re-BFS every tick
- Reset on area change
### Stuck Detection
- **Window**: Last 60 positions (~1 second at 60Hz)
- **Threshold**: Must move at least 30 world units in window
- **Grace period**: 120 ticks (2 seconds) after picking new explore target
- **On stuck while exploring**: Mark failed goal as explored, pick new target, set grace period
- **On stuck otherwise**: Repath
### Path Failure Handling
- **Explored bias fallback**: If A* with explored grid bias fails, retry without bias (bias can make distant targets unreachable)
- **Cooldown**: 3 seconds before retrying after path failure (prevents CPU burn on impossible paths)
---
## PathFinder — A* Implementation
### Signature
```csharp
public static List<Vector2>? FindPath(
WalkabilitySnapshot terrain, Vector2 start, Vector2 goal, float worldToGrid,
bool[]? exploredGrid, int exploredWidth, int exploredHeight,
int exploredOffsetX, int exploredOffsetY)
```
Returns world-coordinate waypoints or null if unreachable.
### Movement Model
- **8-directional grid**: Cardinal + diagonal
- **Costs**: Cardinal = 1.0, Diagonal = √2 ≈ 1.414
- **Explored penalty**: ×1.5 multiplier for explored cells (biases paths through unexplored territory)
### Heuristic
```
h = max(dx, dy) + 0.414 * min(dx, dy)
```
Diagonal/Chebyshev-based. Admissible and consistent.
### Algorithm
1. **Snap to walkable**: If start/goal in wall, BFS search for nearest walkable cell (radius up to 20)
2. **A* search** (budget: 200,000 iterations):
- Priority queue ordered by f = g + h
- 8 neighbors per expansion
- **Corner-cut check**: Diagonals require at least one adjacent cardinal cell walkable
- **Explored grid bias**: Multiply step cost by 1.5 for explored cells
- Track `bestNode` (closest reachable) for fallback
3. **Path reconstruction**: Backtrack via cameFrom map
4. **Simplification**: Remove collinear waypoints, keep only turning points
5. **Fallback**: If goal unreachable but bestNode is meaningfully closer (within 80% of starting heuristic), path to closest reachable cell
### Data Structures
| Structure | Type | Purpose |
|-----------|------|---------|
| Open set | PriorityQueue<(int,int), float> | Nodes to expand, ordered by f-score |
| Closed set | HashSet<(int,int)> | Already expanded nodes |
| gScore | Dictionary<(int,int), float> | Best known cost to each node |
| cameFrom | Dictionary<(int,int), (int,int)> | Backtracking map |
---
## Integration
```
AreaProgressionSystem
│ .Explore() / .NavigateTo() / .NavigateToEntity()
NavigationController
│ .Update(GameState) → computes path, sets DesiredDirection
│ calls PathFinder.FindPath() for A* routing
NavigationSystem
│ reads DesiredDirection → submits MoveAction
ActionQueue → BotEngine → MovementKeyTracker → WASD keys
```
### Coordinate Systems
| Space | Example | Conversion |
|-------|---------|------------|
| World | (1517, 4491) | Raw game units |
| Grid | (139, 413) | world × WorldToGrid (23/250) |
| Local grid | (139-ox, 413-oy) | grid - terrain offset |
| Screen | project via CameraMatrix | WorldToScreen.Project() |