diff --git a/Grid-TCOD-Integration.-.md b/Grid-TCOD-Integration.-.md new file mode 100644 index 0000000..91e3fc2 --- /dev/null +++ b/Grid-TCOD-Integration.-.md @@ -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 \ No newline at end of file diff --git a/Grid-TCOD-Integration.md b/Grid-TCOD-Integration.md deleted file mode 100644 index f0fed29..0000000 --- a/Grid-TCOD-Integration.md +++ /dev/null @@ -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 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 \ No newline at end of file