Major rewrite: update API to match current codebase (auto-sync, tuple args, AStarPath/DijkstraMap objects, property-based perspective)
parent
991b6d95dd
commit
ebeb0216bb
2 changed files with 453 additions and 561 deletions
453
Grid-TCOD-Integration.-.md
Normal file
453
Grid-TCOD-Integration.-.md
Normal file
|
|
@ -0,0 +1,453 @@
|
|||
# Grid TCOD Integration
|
||||
|
||||
## Overview
|
||||
|
||||
McRogueFace integrates with libtcod for FOV (field of view), A* pathfinding, and Dijkstra maps. The integration automatically synchronizes each grid's walkability and transparency properties with an internal `TCODMap`.
|
||||
|
||||
**Parent Page:** [[Grid-System]]
|
||||
|
||||
**Related Pages:**
|
||||
- [[AI-and-Pathfinding]] - Using FOV and pathfinding for game AI
|
||||
- [[Grid-Rendering-Pipeline]] - How FOV affects rendering overlays
|
||||
- [[Entity-Management]] - Entity perspective and gridstate
|
||||
|
||||
**Key Files:**
|
||||
- `src/UIGrid.cpp` - TCODMap synchronization, FOV, pathfinding
|
||||
- `src/UIGrid.h` - TCODMap, TCODPath, TCODDijkstra members
|
||||
|
||||
---
|
||||
|
||||
## The World State Layer
|
||||
|
||||
### Cell Properties as World Physics
|
||||
|
||||
Each grid cell (GridPoint) has properties that drive TCOD algorithms:
|
||||
|
||||
```
|
||||
Visual Layer (ColorLayer/TileLayer) - What's displayed (colors, sprites)
|
||||
|
|
||||
World State Layer (GridPoint) - Physical properties (walkable, transparent)
|
||||
|
|
||||
Perspective Layer - Per-entity knowledge (FOV results)
|
||||
```
|
||||
|
||||
```python
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))
|
||||
|
||||
cell = grid.at(10, 10)
|
||||
cell.walkable = True # Affects pathfinding
|
||||
cell.transparent = True # Affects FOV
|
||||
cell.tilesprite = 0 # Visual tile index (legacy)
|
||||
```
|
||||
|
||||
**Automatic Synchronization:** When you set `cell.walkable` or `cell.transparent`, the internal TCODMap is automatically updated. There is no manual sync step required.
|
||||
|
||||
---
|
||||
|
||||
## Field of View (FOV)
|
||||
|
||||
### Computing FOV
|
||||
|
||||
FOV determines which cells are visible from a given position:
|
||||
|
||||
```python
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))
|
||||
|
||||
# Make all cells transparent
|
||||
for x in range(50):
|
||||
for y in range(50):
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Add some walls
|
||||
for x in range(20, 30):
|
||||
grid.at(x, 15).transparent = False
|
||||
grid.at(x, 15).walkable = False
|
||||
|
||||
# Compute FOV from position with radius
|
||||
grid.compute_fov((25, 25), radius=10)
|
||||
|
||||
# Query visibility of specific cells
|
||||
if grid.is_in_fov((25, 25)):
|
||||
print("Origin is visible")
|
||||
|
||||
if not grid.is_in_fov((25, 5)):
|
||||
print("Behind wall is not visible")
|
||||
```
|
||||
|
||||
**API:**
|
||||
- `grid.compute_fov((x, y), radius=N)` - Compute FOV from position
|
||||
- `grid.is_in_fov((x, y))` - Query if cell is currently visible
|
||||
|
||||
### FOV with Fog Overlay
|
||||
|
||||
Use a ColorLayer to visualize FOV:
|
||||
|
||||
```python
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[])
|
||||
|
||||
# Create fog overlay above entities
|
||||
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
|
||||
grid.add_layer(fog)
|
||||
fog.fill(mcrfpy.Color(0, 0, 0, 255)) # Start fully hidden
|
||||
|
||||
# After computing FOV, reveal visible cells
|
||||
def update_fog(grid, fog, pos, radius=10):
|
||||
grid.compute_fov(pos, radius=radius)
|
||||
w, h = grid.grid_size
|
||||
for x in range(w):
|
||||
for y in range(h):
|
||||
if grid.is_in_fov((x, y)):
|
||||
fog.set((x, y), mcrfpy.Color(0, 0, 0, 0)) # Visible
|
||||
else:
|
||||
fog.set((x, y), mcrfpy.Color(0, 0, 0, 192)) # Dim
|
||||
|
||||
update_fog(grid, fog, (25, 25))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## A* Pathfinding
|
||||
|
||||
### Finding Paths
|
||||
|
||||
Find the shortest path between two walkable cells:
|
||||
|
||||
```python
|
||||
grid = mcrfpy.Grid(grid_size=(30, 30), pos=(0, 0), size=(400, 400))
|
||||
for x in range(30):
|
||||
for y in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Find path - returns AStarPath object
|
||||
path = grid.find_path((5, 5), (25, 25))
|
||||
|
||||
if path is not None and len(path) > 0:
|
||||
# Walk the path (consumes next step)
|
||||
next_step = path.walk()
|
||||
print(f"Next step: ({next_step.x}, {next_step.y})")
|
||||
|
||||
# Peek at next step without consuming
|
||||
upcoming = path.peek()
|
||||
|
||||
# Check remaining steps
|
||||
print(f"Remaining: {path.remaining}")
|
||||
|
||||
# Check endpoints
|
||||
print(f"From: {path.origin}")
|
||||
print(f"To: {path.destination}")
|
||||
```
|
||||
|
||||
### AStarPath Object
|
||||
|
||||
| Property/Method | Description |
|
||||
|----------------|-------------|
|
||||
| `len(path)` | Total steps in path |
|
||||
| `path.walk()` | Get and consume next step (returns Vector) |
|
||||
| `path.peek()` | View next step without consuming |
|
||||
| `path.remaining` | Steps remaining |
|
||||
| `path.origin` | Start position (Vector) |
|
||||
| `path.destination` | End position (Vector) |
|
||||
|
||||
### Moving Entities Along Paths
|
||||
|
||||
```python
|
||||
player = mcrfpy.Entity(grid_pos=(5, 5), sprite_index=0)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Find path to target
|
||||
path = grid.find_path(
|
||||
(int(player.grid_x), int(player.grid_y)),
|
||||
(25, 25)
|
||||
)
|
||||
|
||||
if path and len(path) > 0:
|
||||
step = path.walk()
|
||||
player.grid_x = int(step.x)
|
||||
player.grid_y = int(step.y)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dijkstra Maps
|
||||
|
||||
### Computing Dijkstra Maps
|
||||
|
||||
Dijkstra maps compute distances from a goal to all reachable cells. Useful for multi-enemy AI where many entities path toward the same target:
|
||||
|
||||
```python
|
||||
grid = mcrfpy.Grid(grid_size=(30, 30), pos=(0, 0), size=(400, 400))
|
||||
for x in range(30):
|
||||
for y in range(30):
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Create Dijkstra map from goal position
|
||||
dm = grid.get_dijkstra_map((15, 15))
|
||||
|
||||
# Query distance from any cell to goal
|
||||
d = dm.distance((0, 0))
|
||||
print(f"Distance from (0,0) to goal: {d}")
|
||||
|
||||
# Get full path from any cell to goal
|
||||
path = dm.path_from((0, 0))
|
||||
print(f"Path length: {len(path)}")
|
||||
|
||||
# Get just the next step toward goal
|
||||
next_step = dm.step_from((0, 0))
|
||||
print(f"Next step: ({next_step.x}, {next_step.y})")
|
||||
```
|
||||
|
||||
### DijkstraMap Object
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `dm.distance((x, y))` | Distance from cell to goal |
|
||||
| `dm.path_from((x, y))` | Full path from cell to goal |
|
||||
| `dm.step_from((x, y))` | Next step from cell toward goal |
|
||||
|
||||
### Dijkstra vs A*
|
||||
|
||||
| Feature | A* (`find_path`) | Dijkstra (`get_dijkstra_map`) |
|
||||
|---------|-----------------|-------------------------------|
|
||||
| **Goals** | Single target | Single target, query from anywhere |
|
||||
| **Computation** | One path at a time | One map, unlimited queries |
|
||||
| **Use case** | Single entity, single target | Many entities, same target |
|
||||
| **Performance** | Fast per query | O(n) once, then O(1) per query |
|
||||
|
||||
**Rule of thumb:** 1-5 entities -> A* per entity. 10+ entities with same goal -> Dijkstra map.
|
||||
|
||||
---
|
||||
|
||||
## Entity Perspective System
|
||||
|
||||
### Setting Grid Perspective
|
||||
|
||||
```python
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))
|
||||
player = mcrfpy.Entity(grid_pos=(25, 25), sprite_index=0)
|
||||
grid.entities.append(player)
|
||||
|
||||
# Assign perspective (property, not method)
|
||||
grid.perspective = player
|
||||
|
||||
# Grid rendering now uses player's FOV for visibility
|
||||
grid.compute_fov((int(player.grid_x), int(player.grid_y)), radius=10)
|
||||
```
|
||||
|
||||
### FOV Update on Movement
|
||||
|
||||
```python
|
||||
scene = mcrfpy.Scene("game")
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[])
|
||||
|
||||
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
|
||||
grid.add_layer(fog)
|
||||
fog.fill(mcrfpy.Color(0, 0, 0, 255))
|
||||
|
||||
for x in range(50):
|
||||
for y in range(50):
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
player = mcrfpy.Entity(grid_pos=(25, 25), sprite_index=0)
|
||||
grid.entities.append(player)
|
||||
grid.perspective = player
|
||||
scene.children.append(grid)
|
||||
|
||||
def update_fov():
|
||||
"""Call after player moves"""
|
||||
px, py = int(player.grid_x), int(player.grid_y)
|
||||
grid.compute_fov((px, py), radius=10)
|
||||
w, h = grid.grid_size
|
||||
for x in range(w):
|
||||
for y in range(h):
|
||||
if grid.is_in_fov((x, y)):
|
||||
fog.set((x, y), mcrfpy.Color(0, 0, 0, 0))
|
||||
|
||||
def on_key(key, action):
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
dx, dy = 0, 0
|
||||
if key == mcrfpy.Key.W: dy = -1
|
||||
elif key == mcrfpy.Key.S: dy = 1
|
||||
elif key == mcrfpy.Key.A: dx = -1
|
||||
elif key == mcrfpy.Key.D: dx = 1
|
||||
|
||||
if dx or dy:
|
||||
nx = int(player.grid_x) + dx
|
||||
ny = int(player.grid_y) + dy
|
||||
if grid.at(nx, ny).walkable:
|
||||
player.grid_x = nx
|
||||
player.grid_y = ny
|
||||
update_fov()
|
||||
|
||||
scene.on_key = on_key
|
||||
update_fov() # Initial FOV
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Opening a Door
|
||||
|
||||
```python
|
||||
def open_door(grid, door_x, door_y):
|
||||
"""Open door - update world state (auto-syncs to TCOD)"""
|
||||
cell = grid.at(door_x, door_y)
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
cell.tilesprite = 2 # Open door sprite
|
||||
|
||||
# Recompute FOV if player nearby
|
||||
px, py = int(player.grid_x), int(player.grid_y)
|
||||
grid.compute_fov((px, py), radius=10)
|
||||
```
|
||||
|
||||
### Dynamic Obstacle
|
||||
|
||||
```python
|
||||
def boulder_falls(grid, x, y):
|
||||
"""Boulder blocks cell"""
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
cell.tilesprite = 3 # Boulder sprite
|
||||
# TCOD map auto-updated - paths through this cell now invalid
|
||||
```
|
||||
|
||||
### Chase AI with Dijkstra
|
||||
|
||||
```python
|
||||
def update_enemies(grid, player, enemies):
|
||||
"""Move all enemies toward player using Dijkstra map"""
|
||||
px, py = int(player.grid_x), int(player.grid_y)
|
||||
dm = grid.get_dijkstra_map((px, py))
|
||||
|
||||
for enemy in enemies:
|
||||
ex, ey = int(enemy.grid_x), int(enemy.grid_y)
|
||||
next_step = dm.step_from((ex, ey))
|
||||
if next_step is not None:
|
||||
enemy.grid_x = int(next_step.x)
|
||||
enemy.grid_y = int(next_step.y)
|
||||
```
|
||||
|
||||
### Spatial Queries
|
||||
|
||||
```python
|
||||
# Find entities near a position
|
||||
nearby = grid.entities_in_radius((int(enemy.grid_x), int(enemy.grid_y)), 5.0)
|
||||
for entity in nearby:
|
||||
print(f"Nearby: {entity.name}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### FOV Cost
|
||||
|
||||
FOV computation time scales with radius and grid size. Only compute when the entity moves:
|
||||
|
||||
```python
|
||||
last_pos = [None]
|
||||
|
||||
def update_fov_if_moved():
|
||||
px, py = int(player.grid_x), int(player.grid_y)
|
||||
if last_pos[0] != (px, py):
|
||||
grid.compute_fov((px, py), radius=10)
|
||||
last_pos[0] = (px, py)
|
||||
```
|
||||
|
||||
### Pathfinding Cost
|
||||
|
||||
- Limit search distance for distant targets
|
||||
- Use Dijkstra maps for many entities with same goal
|
||||
- Cache paths and recompute only when grid changes
|
||||
|
||||
### Cell Property Changes
|
||||
|
||||
Setting `walkable` or `transparent` auto-syncs to TCOD. For bulk changes, set all properties first, then compute FOV/paths:
|
||||
|
||||
```python
|
||||
# Set many cells, then compute once
|
||||
for x in range(100):
|
||||
for y in range(100):
|
||||
grid.at(x, y).walkable = compute_walkable(x, y)
|
||||
|
||||
# Single FOV computation after all changes
|
||||
grid.compute_fov((px, py), radius=10)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Pathfinding Returns None
|
||||
|
||||
**Causes:**
|
||||
1. Target is unreachable (blocked by walls)
|
||||
2. Start or end position is non-walkable
|
||||
|
||||
**Debug:**
|
||||
```python
|
||||
path = grid.find_path((x1, y1), (x2, y2))
|
||||
if path is None or len(path) == 0:
|
||||
print(f"Start walkable: {grid.at(x1, y1).walkable}")
|
||||
print(f"End walkable: {grid.at(x2, y2).walkable}")
|
||||
```
|
||||
|
||||
### Issue: FOV Doesn't Match Expected
|
||||
|
||||
**Cause:** Cell `transparent` property not set correctly.
|
||||
|
||||
**Fix:** Ensure walls have `transparent = False`:
|
||||
```python
|
||||
cell = grid.at(x, y)
|
||||
cell.walkable = False
|
||||
cell.transparent = False # Must set both for walls
|
||||
```
|
||||
|
||||
### Issue: Entity Can See Through Glass
|
||||
|
||||
Glass cells should block movement but allow sight:
|
||||
```python
|
||||
glass = grid.at(x, y)
|
||||
glass.walkable = False # Can't walk through
|
||||
glass.transparent = True # CAN see through
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Quick Reference
|
||||
|
||||
**FOV:**
|
||||
- `grid.compute_fov((x, y), radius=N)` - Compute FOV from position
|
||||
- `grid.is_in_fov((x, y))` - Check if cell is visible
|
||||
|
||||
**A* Pathfinding:**
|
||||
- `grid.find_path((x1, y1), (x2, y2))` - Returns AStarPath object
|
||||
|
||||
**Dijkstra Maps:**
|
||||
- `grid.get_dijkstra_map((x, y))` - Returns DijkstraMap object
|
||||
- `dm.distance((x, y))` - Distance to goal
|
||||
- `dm.path_from((x, y))` - Full path to goal
|
||||
- `dm.step_from((x, y))` - Next step toward goal
|
||||
|
||||
**Spatial Queries:**
|
||||
- `grid.entities_in_radius((x, y), radius)` - Find nearby entities
|
||||
|
||||
**Perspective:**
|
||||
- `grid.perspective = entity` - Set FOV perspective entity
|
||||
|
||||
**Cell Properties:**
|
||||
- `cell.walkable` - Bool, affects pathfinding
|
||||
- `cell.transparent` - Bool, affects FOV
|
||||
|
||||
---
|
||||
|
||||
**Navigation:**
|
||||
- [[Grid-System]] - Parent page
|
||||
- [[AI-and-Pathfinding]] - Using FOV and pathfinding for game AI
|
||||
- [[Grid-Rendering-Pipeline]] - FOV overlay rendering
|
||||
- [[Entity-Management]] - Entity gridstate and perspective
|
||||
|
|
@ -1,561 +0,0 @@
|
|||
# Grid TCOD Integration
|
||||
|
||||
## Overview
|
||||
|
||||
McRogueFace integrates with libtcod (The Chron of Doryen) for FOV (field of view), pathfinding, and Dijkstra maps. The integration maintains a synchronized `TCODMap` that mirrors each grid's walkability and transparency properties.
|
||||
|
||||
**Parent Page:** [[Grid-System]]
|
||||
|
||||
**Related Pages:**
|
||||
- [[AI-and-Pathfinding]] - Using FOV and pathfinding for game AI
|
||||
- [[Grid-Rendering-Pipeline]] - How FOV affects rendering overlays
|
||||
- [[Entity-Management]] - Entity perspective and gridstate
|
||||
|
||||
**Key Files:**
|
||||
- `src/UIGrid.cpp::syncTCODMap()` - Synchronization (lines 343-361)
|
||||
- `src/UIGrid.cpp::computeFOV()` - FOV computation (line 363)
|
||||
- `src/UIGrid.h` - TCODMap, TCODPath, TCODDijkstra members
|
||||
|
||||
**Related Issues:**
|
||||
- [#64](../issues/64) - TCOD updates (last TCOD sync)
|
||||
- [#124](../issues/124) - Grid Point Animation
|
||||
- [#123](../issues/123) - Subgrid system integration with TCOD
|
||||
|
||||
---
|
||||
|
||||
## The World State Layer
|
||||
|
||||
### TCODMap as World Physics
|
||||
|
||||
In the three-layer grid architecture, **TCODMap represents world state**:
|
||||
|
||||
```
|
||||
Visual Layer (UIGridPoint) - What's displayed (colors, sprites)
|
||||
↓
|
||||
World State Layer (TCODMap) - Physical properties (walkable, transparent)
|
||||
↓
|
||||
Perspective Layer (UIGridPointState) - Per-entity knowledge (discovered, visible)
|
||||
```
|
||||
|
||||
Every grid has a `TCODMap` that must be kept synchronized with cell properties.
|
||||
|
||||
---
|
||||
|
||||
## TCODMap Synchronization
|
||||
|
||||
### Initialization
|
||||
|
||||
When a grid is created, its TCODMap is initialized:
|
||||
|
||||
```cpp
|
||||
// UIGrid constructor
|
||||
tcod_map = new TCODMap(gx, gy);
|
||||
tcod_dijkstra = new TCODDijkstra(tcod_map);
|
||||
tcod_path = new TCODPath(tcod_map);
|
||||
|
||||
// Sync initial state
|
||||
syncTCODMap();
|
||||
```
|
||||
|
||||
### Synchronization Methods
|
||||
|
||||
#### syncTCODMap() - Full Sync
|
||||
|
||||
Synchronizes entire grid:
|
||||
|
||||
```cpp
|
||||
void UIGrid::syncTCODMap() {
|
||||
if (!tcod_map) return;
|
||||
|
||||
for (int y = 0; y < grid_y; y++) {
|
||||
for (int x = 0; x < grid_x; x++) {
|
||||
const UIGridPoint& point = at(x, y);
|
||||
tcod_map->setProperties(x, y, point.transparent, point.walkable);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:** Initializing grid or making bulk changes to many cells.
|
||||
|
||||
**Performance:** O(grid_x * grid_y) - expensive for large grids.
|
||||
|
||||
#### syncTCODMapCell() - Single Cell Sync
|
||||
|
||||
Synchronizes one cell:
|
||||
|
||||
```cpp
|
||||
void UIGrid::syncTCODMapCell(int x, int y) {
|
||||
if (!tcod_map || x < 0 || x >= grid_x || y < 0 || y >= grid_y) return;
|
||||
|
||||
const UIGridPoint& point = at(x, y);
|
||||
tcod_map->setProperties(x, y, point.transparent, point.walkable);
|
||||
}
|
||||
```
|
||||
|
||||
**Use when:** Changing a single cell's properties (e.g., opening a door, destroying a wall).
|
||||
|
||||
**Performance:** O(1) - efficient for incremental updates.
|
||||
|
||||
### Python API
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))
|
||||
|
||||
# Modify cell properties
|
||||
cell = grid.at((10, 10))
|
||||
cell.walkable = False # Block pathfinding
|
||||
cell.transparent = False # Block FOV
|
||||
|
||||
# Sync to TCOD (required!)
|
||||
grid.sync_tcod_map() # Full sync
|
||||
|
||||
# Or sync single cell
|
||||
grid.sync_tcod_cell(10, 10)
|
||||
```
|
||||
|
||||
**Important:** Changing `cell.walkable` or `cell.transparent` does NOT automatically update TCODMap. You **must** call `sync_tcod_map()` or `sync_tcod_cell()` afterward.
|
||||
|
||||
---
|
||||
|
||||
## Field of View (FOV)
|
||||
|
||||
### Computing FOV
|
||||
|
||||
FOV determines which cells are visible from a given position:
|
||||
|
||||
```python
|
||||
# Compute FOV from position (25, 25) with radius 10
|
||||
visible_cells = grid.compute_fov(
|
||||
x=25,
|
||||
y=25,
|
||||
radius=10, # 0 = unlimited
|
||||
light_walls=True, # Walls at FOV edge are visible
|
||||
algorithm=mcrfpy.FOV_BASIC # or FOV_DIAMOND, FOV_SHADOW, etc.
|
||||
)
|
||||
|
||||
# Returns list of (x, y, visible, discovered) tuples
|
||||
for x, y, visible, discovered in visible_cells:
|
||||
print(f"Cell ({x}, {y}) is visible")
|
||||
```
|
||||
|
||||
### FOV Algorithms
|
||||
|
||||
libtcod provides several FOV algorithms:
|
||||
|
||||
| Algorithm | Description | Performance | Use Case |
|
||||
|-----------|-------------|-------------|----------|
|
||||
| `FOV_BASIC` | Simple raycasting | Fast | General purpose |
|
||||
| `FOV_DIAMOND` | Diamond-shaped FOV | Fast | Square grids |
|
||||
| `FOV_SHADOW` | Shadow casting | Medium | Realistic lighting |
|
||||
| `FOV_PERMISSIVE` | Permissive FOV | Slow | Maximum visibility |
|
||||
| `FOV_RESTRICTIVE` | Restrictive FOV | Medium | Minimal visibility |
|
||||
|
||||
**Default:** `FOV_BASIC` provides good balance of speed and realism.
|
||||
|
||||
### Checking FOV
|
||||
|
||||
After computing FOV, check if specific cells are visible:
|
||||
|
||||
```python
|
||||
# Compute FOV first
|
||||
grid.compute_fov(player.x, player.y, radius=10)
|
||||
|
||||
# Check if cell is visible
|
||||
if grid.is_in_fov(enemy_x, enemy_y):
|
||||
print("Player can see enemy!")
|
||||
enemy.draw_with_highlight()
|
||||
```
|
||||
|
||||
**Thread Safety:** FOV computation is protected by a mutex, allowing safe concurrent access.
|
||||
|
||||
---
|
||||
|
||||
## Pathfinding
|
||||
|
||||
### A* Pathfinding
|
||||
|
||||
Find shortest path between two points:
|
||||
|
||||
```python
|
||||
# Find path from (5, 5) to (45, 45)
|
||||
path = grid.find_path(
|
||||
x1=5, y1=5,
|
||||
x2=45, y2=45,
|
||||
diagonal_cost=1.41 # sqrt(2) for diagonal movement
|
||||
)
|
||||
|
||||
# path is list of (x, y) tuples
|
||||
if path:
|
||||
for x, y in path:
|
||||
grid.at((x, y)).color = (255, 0, 0, 255) # Highlight path
|
||||
|
||||
# Move entity along path
|
||||
entity.path = path
|
||||
else:
|
||||
print("No path found!")
|
||||
```
|
||||
|
||||
### Diagonal Movement Cost
|
||||
|
||||
The `diagonal_cost` parameter affects pathfinding behavior:
|
||||
|
||||
- **1.0** - Diagonal movement is same cost as cardinal (unrealistic, creates zigzag paths)
|
||||
- **1.41** (√2) - Diagonal movement costs more (realistic, smoother paths)
|
||||
- **2.0** - Diagonal movement very expensive (prefers cardinal directions)
|
||||
- **Large value** - Effectively disables diagonal movement
|
||||
|
||||
```python
|
||||
# Pathfinding that prefers cardinal directions
|
||||
path = grid.find_path(10, 10, 20, 20, diagonal_cost=2.0)
|
||||
|
||||
# Pathfinding that allows free diagonal movement
|
||||
path = grid.find_path(10, 10, 20, 20, diagonal_cost=1.0)
|
||||
```
|
||||
|
||||
### Pathfinding Limitations
|
||||
|
||||
- **Static paths:** Path is computed once; doesn't update if grid changes
|
||||
- **No A* customization:** Cannot provide custom cost functions yet
|
||||
- **Blocking:** Pathfinding is synchronous (blocks Python execution)
|
||||
|
||||
**Workaround for dynamic obstacles:**
|
||||
```python
|
||||
# Recompute path periodically
|
||||
def update_enemy_path(ms):
|
||||
# Check if path is still valid
|
||||
for x, y in enemy.path:
|
||||
if not grid.at((x, y)).walkable:
|
||||
# Path blocked, recompute
|
||||
enemy.path = grid.find_path(enemy.x, enemy.y,
|
||||
player.x, player.y)
|
||||
break
|
||||
|
||||
mcrfpy.setTimer("path_update", update_enemy_path, 500) # Every 0.5s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dijkstra Maps
|
||||
|
||||
### Computing Dijkstra Maps
|
||||
|
||||
Dijkstra maps compute distance from goal(s) to all cells, useful for multi-enemy AI:
|
||||
|
||||
```python
|
||||
# Compute Dijkstra map with player as goal
|
||||
grid.compute_dijkstra(
|
||||
root_x=player.x,
|
||||
root_y=player.y,
|
||||
diagonal_cost=1.41
|
||||
)
|
||||
|
||||
# Each enemy can now path toward player
|
||||
for enemy in enemies:
|
||||
# Get path to nearest goal (player)
|
||||
path = grid.get_dijkstra_path(
|
||||
from_x=enemy.x,
|
||||
from_y=enemy.y,
|
||||
max_length=1 # Just get next step
|
||||
)
|
||||
|
||||
if path:
|
||||
next_x, next_y = path[0]
|
||||
enemy.move_to(next_x, next_y)
|
||||
```
|
||||
|
||||
### Multiple Goals
|
||||
|
||||
Dijkstra maps support multiple goal cells:
|
||||
|
||||
```python
|
||||
# Find distance to ANY exit
|
||||
exit_positions = [(5, 5), (45, 5), (5, 45), (45, 45)]
|
||||
|
||||
grid.compute_dijkstra_multi(exit_positions, diagonal_cost=1.41)
|
||||
|
||||
# Each entity can now path to nearest exit
|
||||
path = grid.get_dijkstra_path(entity.x, entity.y, max_length=0) # 0 = full path
|
||||
```
|
||||
|
||||
### Dijkstra vs A*
|
||||
|
||||
| Feature | A* (find_path) | Dijkstra Maps |
|
||||
|---------|---------------|---------------|
|
||||
| **Goals** | Single target | One or many targets |
|
||||
| **Computation** | Once per path | Once for all entities |
|
||||
| **Use case** | Single entity, single target | Many entities, same target |
|
||||
| **Performance** | O(log n) per entity | O(n) once, then O(1) per entity |
|
||||
|
||||
**Rule of thumb:**
|
||||
- 1-5 entities → Use A* per entity
|
||||
- 10+ entities with same goal → Use Dijkstra map
|
||||
|
||||
---
|
||||
|
||||
## Entity Perspective System
|
||||
|
||||
### Gridstate and Discovered/Visible
|
||||
|
||||
Each entity can have a `gridstate` vector tracking what it has seen:
|
||||
|
||||
```cpp
|
||||
// UIEntity member
|
||||
std::vector<UIGridPointState> gridstate;
|
||||
|
||||
struct UIGridPointState {
|
||||
bool discovered; // Has entity ever seen this cell?
|
||||
bool visible; // Can entity currently see this cell?
|
||||
};
|
||||
```
|
||||
|
||||
### Setting Entity Perspective
|
||||
|
||||
```python
|
||||
# Enable perspective for player entity
|
||||
grid.set_perspective(player)
|
||||
|
||||
# This does two things:
|
||||
# 1. Sets grid.perspective_enabled = True
|
||||
# 2. Stores weak_ptr to player entity
|
||||
|
||||
# Now grid rendering will use player's gridstate for FOV overlay
|
||||
```
|
||||
|
||||
See [[Grid-Rendering-Pipeline]] Stage 4 for overlay rendering details.
|
||||
|
||||
### Updating Entity Gridstate
|
||||
|
||||
After computing FOV, update entity's gridstate:
|
||||
|
||||
```python
|
||||
def update_player_fov():
|
||||
"""Update player FOV and gridstate"""
|
||||
# Compute FOV
|
||||
visible_cells = grid.compute_fov(player.x, player.y, radius=10)
|
||||
|
||||
# Update gridstate
|
||||
for x, y, visible, discovered in visible_cells:
|
||||
idx = y * grid.grid_size[0] + x
|
||||
player.gridstate[idx].visible = visible
|
||||
player.gridstate[idx].discovered = discovered
|
||||
|
||||
# Call every time player moves
|
||||
mcrfpy.setTimer("player_fov", update_player_fov, 100)
|
||||
```
|
||||
|
||||
**Note:** This is a manual process currently. Issue #64 may add automatic gridstate updates.
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Opening a Door
|
||||
|
||||
```python
|
||||
def open_door(door_x, door_y):
|
||||
"""Open door at position, update world state"""
|
||||
cell = grid.at((door_x, door_y))
|
||||
|
||||
# Update visual
|
||||
cell.tilesprite = OPEN_DOOR_SPRITE
|
||||
cell.color = (200, 200, 200, 255)
|
||||
|
||||
# Update world state
|
||||
cell.walkable = True
|
||||
cell.transparent = True
|
||||
|
||||
# Sync to TCOD (required!)
|
||||
grid.sync_tcod_cell(door_x, door_y)
|
||||
|
||||
# Recompute FOV if player nearby
|
||||
if distance(door_x, door_y, player.x, player.y) < 15:
|
||||
update_player_fov()
|
||||
```
|
||||
|
||||
### Dynamic Obstacle
|
||||
|
||||
```python
|
||||
def boulder_falls(x, y):
|
||||
"""Boulder falls, blocking cell"""
|
||||
cell = grid.at((x, y))
|
||||
|
||||
# Visual update
|
||||
cell.tilesprite = BOULDER_SPRITE
|
||||
|
||||
# Block movement and sight
|
||||
cell.walkable = False
|
||||
cell.transparent = False
|
||||
|
||||
# Sync to TCOD
|
||||
grid.sync_tcod_cell(x, y)
|
||||
|
||||
# Invalidate any paths going through this cell
|
||||
for entity in entities:
|
||||
if entity.path and (x, y) in entity.path:
|
||||
entity.path = None # Force recompute
|
||||
```
|
||||
|
||||
### Chase AI with Dijkstra
|
||||
|
||||
```python
|
||||
class ChaseAI:
|
||||
"""AI that chases player using Dijkstra maps"""
|
||||
def __init__(self, grid, player):
|
||||
self.grid = grid
|
||||
self.player = player
|
||||
self.dijkstra_dirty = True
|
||||
|
||||
def update(self):
|
||||
# Recompute Dijkstra map if player moved
|
||||
if self.dijkstra_dirty:
|
||||
self.grid.compute_dijkstra(self.player.x, self.player.y)
|
||||
self.dijkstra_dirty = False
|
||||
|
||||
# Move all enemies toward player
|
||||
for enemy in enemies:
|
||||
path = self.grid.get_dijkstra_path(enemy.x, enemy.y, max_length=1)
|
||||
if path:
|
||||
next_x, next_y = path[0]
|
||||
enemy.move_to(next_x, next_y)
|
||||
|
||||
def on_player_move(self):
|
||||
self.dijkstra_dirty = True
|
||||
|
||||
ai = ChaseAI(grid, player)
|
||||
mcrfpy.setTimer("ai", lambda ms: ai.update(), 200) # Update 5x per second
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### FOV Computation Cost
|
||||
|
||||
| Grid Size | Radius | Time (FOV_BASIC) |
|
||||
|-----------|--------|------------------|
|
||||
| 50x50 | 10 | ~0.5ms |
|
||||
| 100x100 | 15 | ~1.5ms |
|
||||
| 200x200 | 20 | ~4ms |
|
||||
|
||||
**Optimization:**
|
||||
- Only compute FOV when entity moves
|
||||
- Use smaller radius when possible
|
||||
- Cache results for stationary entities
|
||||
|
||||
### Pathfinding Cost
|
||||
|
||||
| Grid Size | Path Length | Time (A*) |
|
||||
|-----------|-------------|-----------|
|
||||
| 50x50 | 20 cells | ~0.3ms |
|
||||
| 100x100 | 50 cells | ~1.2ms |
|
||||
| 200x200 | 100 cells | ~5ms |
|
||||
|
||||
**Optimization:**
|
||||
- Limit pathfinding distance for distant targets
|
||||
- Use Dijkstra maps for many entities with same goal
|
||||
- Cache paths and only recompute when grid changes
|
||||
|
||||
### Sync Cost
|
||||
|
||||
- **syncTCODMap()**: O(grid_x * grid_y) - use sparingly
|
||||
- **syncTCODMapCell()**: O(1) - use freely
|
||||
|
||||
**Best Practice:**
|
||||
```python
|
||||
# BAD: Full sync after every cell change
|
||||
for x in range(100):
|
||||
for y in range(100):
|
||||
grid.at((x, y)).walkable = compute_walkable(x, y)
|
||||
grid.sync_tcod_map() # O(n²) per cell = O(n⁴) total!
|
||||
|
||||
# GOOD: Bulk changes then single sync
|
||||
for x in range(100):
|
||||
for y in range(100):
|
||||
grid.at((x, y)).walkable = compute_walkable(x, y)
|
||||
grid.sync_tcod_map() # O(n²) once
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Issue: Pathfinding Returns Empty Path
|
||||
|
||||
**Causes:**
|
||||
1. Target is unreachable (blocked by walls)
|
||||
2. TCODMap not synchronized after cell changes
|
||||
3. Start or end position is non-walkable
|
||||
|
||||
**Debug:**
|
||||
```python
|
||||
path = grid.find_path(x1, y1, x2, y2)
|
||||
if not path:
|
||||
# Check walkability
|
||||
print(f"Start walkable: {grid.at((x1, y1)).walkable}")
|
||||
print(f"End walkable: {grid.at((x2, y2)).walkable}")
|
||||
|
||||
# Try computing FOV to see what's reachable
|
||||
visible = grid.compute_fov(x1, y1, radius=50)
|
||||
if (x2, y2) not in [(x, y) for x, y, _, _ in visible]:
|
||||
print("Target not reachable from start!")
|
||||
```
|
||||
|
||||
### Issue: FOV Doesn't Match Visual
|
||||
|
||||
**Cause:** TCODMap `transparent` property not synced with cell visual.
|
||||
|
||||
**Fix:**
|
||||
```python
|
||||
# After changing cell visual
|
||||
cell = grid.at((x, y))
|
||||
cell.tilesprite = WALL_SPRITE
|
||||
cell.transparent = False # Important!
|
||||
grid.sync_tcod_cell(x, y)
|
||||
```
|
||||
|
||||
### Issue: Entity Can't See Through Glass
|
||||
|
||||
**Cause:** Glass cells have `transparent = False`.
|
||||
|
||||
**Fix:**
|
||||
```python
|
||||
# Glass cell setup
|
||||
glass_cell = grid.at((x, y))
|
||||
glass_cell.walkable = False # Can't walk through
|
||||
glass_cell.transparent = True # CAN see through
|
||||
grid.sync_tcod_cell(x, y)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Reference
|
||||
|
||||
See [`docs/api_reference_dynamic.html`](../src/branch/master/docs/api_reference_dynamic.html) for complete TCOD API.
|
||||
|
||||
**FOV Methods:**
|
||||
- `grid.compute_fov(x, y, radius=0, light_walls=True, algorithm=FOV_BASIC)` → List[(x, y, visible, discovered)]
|
||||
- `grid.is_in_fov(x, y)` → bool
|
||||
|
||||
**Pathfinding Methods:**
|
||||
- `grid.find_path(x1, y1, x2, y2, diagonal_cost=1.41)` → List[(x, y)]
|
||||
- `grid.compute_dijkstra(root_x, root_y, diagonal_cost=1.41)` → None
|
||||
- `grid.get_dijkstra_path(from_x, from_y, max_length=0)` → List[(x, y)]
|
||||
|
||||
**Sync Methods:**
|
||||
- `grid.sync_tcod_map()` → None (sync entire grid)
|
||||
- `grid.sync_tcod_cell(x, y)` → None (sync single cell)
|
||||
|
||||
**Cell Properties:**
|
||||
- `cell.walkable` - Boolean, affects pathfinding
|
||||
- `cell.transparent` - Boolean, affects FOV
|
||||
|
||||
---
|
||||
|
||||
**Navigation:**
|
||||
- [[Grid-System]] - Parent page
|
||||
- [[AI-and-Pathfinding]] - Using FOV and pathfinding for game AI
|
||||
- [[Grid-Rendering-Pipeline]] - FOV overlay rendering
|
||||
- [[Entity-Management]] - Entity gridstate and perspective
|
||||
Loading…
Add table
Add a link
Reference in a new issue