diff --git a/AI-and-Pathfinding.-.md b/AI-and-Pathfinding.-.md new file mode 100644 index 0000000..468d643 --- /dev/null +++ b/AI-and-Pathfinding.-.md @@ -0,0 +1,466 @@ +# AI and Pathfinding + +Field of view (FOV), pathfinding, and AI systems for creating intelligent game entities. + +## Quick Reference + +**Systems:** [[Grid-System]], [[Entity-Management]] + +**Key Features:** +- A* pathfinding +- Dijkstra maps +- Field of view (FOV) +- Per-entity perspective/knowledge + +**TCOD Integration:** `src/UIGrid.cpp` libtcod bindings + +--- + +## Field of View (FOV) + +### Basic FOV + +```python +import mcrfpy + +# Setup grid with transparency +grid = mcrfpy.Grid(50, 50, 16, 16) + +# Set tile transparency (can entity see through this tile?) +for x in range(50): + for y in range(50): + # Walls are opaque, floor is transparent + is_wall = grid.at((x, y)).tilesprite == WALL_TILE + grid.transparent((x, y), not is_wall) + +# Compute FOV from entity position +player = mcrfpy.Entity(25, 25, 0) +grid.compute_fov(player.x, player.y, radius=10) + +# FOV is now computed - use grid.perspective for rendering +``` + +**Implementation:** `src/UIGrid.cpp::compute_fov()` - Uses libtcod's FOV algorithm + +### Per-Entity Perspective + +Each entity can have its own knowledge of the map: + +```python +# Set which entity's perspective to render +grid.perspective = player + +# This affects rendering: +# - Unexplored tiles: Black (never seen) +# - Explored tiles: Dark (seen before, not visible now) +# - Visible tiles: Normal (currently in FOV) +``` + +**Three rendering states:** +1. **Unexplored** - Never seen by this entity +2. **Explored** - Seen before, not currently visible +3. **Visible** - Currently in field of view + +**Implementation:** +- `src/UIGrid.cpp::perspective` property +- `src/UIGridPointState.h` - Per-entity tile knowledge + +### FOV Example: Fog of War + +```python +import mcrfpy + +# Create game +mcrfpy.createScene("game") +grid = mcrfpy.Grid(50, 50, 16, 16) +grid.texture = mcrfpy.createTexture("tiles.png") + +# Create player +player = mcrfpy.Entity(25, 25, 0) +grid.entities.append(player) + +# Set grid perspective to player +grid.perspective = player + +# Update FOV when player moves +def on_player_move(dx, dy): + new_x = player.x + dx + new_y = player.y + dy + + if grid.walkable((new_x, new_y)): + player.pos = (new_x, new_y) + + # Recompute FOV from new position + grid.compute_fov(player.x, player.y, radius=10) + +# Input handling +def on_keypress(key, pressed): + if pressed: + if key == mcrfpy.Key.Up: + on_player_move(0, -1) + elif key == mcrfpy.Key.Down: + on_player_move(0, 1) + # ... etc +``` + +--- + +## Pathfinding + +### A* Pathfinding + +Finds shortest path from entity to target: + +```python +# Entity pathfinding +player = mcrfpy.Entity(10, 10, 0) +grid.entities.append(player) + +# Find path to target position +path = player.path_to(30, 25) + +# path is list of (x, y) tuples +if path: + print(f"Path length: {len(path)}") + for x, y in path: + print(f" Step: ({x}, {y})") + + # Move along path + next_step = path[0] + player.pos = next_step +``` + +**Caching:** Paths are automatically cached in entity for performance + +**Implementation:** +- `src/UIEntity.cpp::path_to()` - A* pathfinding +- Uses libtcod's A* implementation +- Respects grid walkability + +### Dijkstra Maps + +Multi-target pathfinding for AI: + +```python +# Create Dijkstra map with multiple goal points +goals = [(10, 10), (40, 40), (25, 5)] +grid.compute_dijkstra(goals) + +# Get distance from any position to nearest goal +distance = grid.get_dijkstra_distance(entity.x, entity.y) + +# Get path from position to nearest goal +path = grid.get_dijkstra_path(entity.x, entity.y, max_length=20) +``` + +**Use cases:** +- **Chase AI:** Dijkstra map with player as goal +- **Flee AI:** Inverted Dijkstra map (high values = safe) +- **Multi-target:** Pathfind to any of several objectives + +**Implementation:** `src/UIGrid.cpp` - Dijkstra map functions + +### Pathfinding Example: Smart Enemy AI + +```python +import mcrfpy + +class SmartEnemy: + def __init__(self, x, y): + self.entity = mcrfpy.Entity(x, y, 1) + self.state = "idle" + self.aggro_range = 15 + self.last_player_pos = None + + def update(self, player, grid): + # Calculate distance to player + dx = player.x - self.entity.x + dy = player.y - self.entity.y + distance = (dx*dx + dy*dy) ** 0.5 + + if distance < self.aggro_range: + # Can we see the player? + if self.can_see(player, grid): + # Use pathfinding to chase + self.chase(player) + self.last_player_pos = (player.x, player.y) + elif self.last_player_pos: + # Move to last known position + self.investigate(self.last_player_pos) + else: + # Lost player, go back to idle + self.state = "idle" + else: + self.state = "idle" + + def can_see(self, player, grid): + # Check if entity can see player using FOV + grid.compute_fov(self.entity.x, self.entity.y, radius=self.aggro_range) + # In real implementation, check if player's tile is visible + # For now, simple line-of-sight check + return True # Simplified + + def chase(self, player): + # Use A* pathfinding + path = self.entity.path_to(player.x, player.y) + if path and len(path) > 0: + next_step = path[0] + self.entity.pos = next_step + + def investigate(self, target_pos): + # Move to last known position + if (self.entity.x, self.entity.y) == target_pos: + self.last_player_pos = None + self.state = "idle" + else: + path = self.entity.path_to(*target_pos) + if path and len(path) > 0: + next_step = path[0] + self.entity.pos = next_step +``` + +--- + +## AI Patterns + +### Pattern 1: Chase AI (Dijkstra) + +Enemies chase player using Dijkstra map: + +```python +def update_enemies(grid, player, enemies): + # Compute Dijkstra map with player as goal + grid.compute_dijkstra([(player.x, player.y)]) + + for enemy in enemies: + # Get path toward player + path = grid.get_dijkstra_path(enemy.x, enemy.y, max_length=1) + if path and len(path) > 0: + next_x, next_y = path[0] + if grid.walkable((next_x, next_y)): + enemy.pos = (next_x, next_y) +``` + +**Advantage:** Computes paths for all enemies at once - more efficient than individual A* + +### Pattern 2: Flee AI (Inverted Dijkstra) + +Enemies flee from danger: + +```python +def flee_from_player(grid, player, scared_npcs): + # Compute danger map (player = maximum danger) + grid.compute_dijkstra([(player.x, player.y)]) + + for npc in scared_npcs: + # Find tile furthest from player + best_pos = None + best_distance = -1 + + for dx in [-1, 0, 1]: + for dy in [-1, 0, 1]: + if dx == 0 and dy == 0: + continue + + check_x = npc.x + dx + check_y = npc.y + dy + + if grid.walkable((check_x, check_y)): + distance = grid.get_dijkstra_distance(check_x, check_y) + if distance > best_distance: + best_distance = distance + best_pos = (check_x, check_y) + + if best_pos: + npc.pos = best_pos +``` + +### Pattern 3: Guard AI (Patrol Routes) + +Entities patrol between waypoints: + +```python +class Guard: + def __init__(self, x, y, waypoints): + self.entity = mcrfpy.Entity(x, y, 2) + self.waypoints = waypoints + self.current_waypoint = 0 + self.wait_time = 0 + + def update(self, grid): + if self.wait_time > 0: + self.wait_time -= 1 + return + + # Get current target waypoint + target = self.waypoints[self.current_waypoint] + + # Are we there yet? + if (self.entity.x, self.entity.y) == target: + # Wait at waypoint + self.wait_time = 30 # Wait 30 ticks + # Move to next waypoint + self.current_waypoint = (self.current_waypoint + 1) % len(self.waypoints) + else: + # Pathfind to waypoint + path = self.entity.path_to(*target) + if path and len(path) > 0: + next_step = path[0] + self.entity.pos = next_step + +# Create guard with patrol route +guard = Guard(10, 10, [(10, 10), (30, 10), (30, 30), (10, 30)]) +``` + +### Pattern 4: State Machine AI + +Complex behaviors using states: + +```python +class ComplexAI: + def __init__(self, x, y): + self.entity = mcrfpy.Entity(x, y, 3) + self.state = "patrol" + self.health = 100 + self.aggro_range = 10 + self.flee_threshold = 30 + + def update(self, player, grid): + if self.state == "patrol": + self.do_patrol(grid) + # Check for player + if self.distance_to(player) < self.aggro_range: + self.state = "chase" + + elif self.state == "chase": + self.do_chase(player) + # Check health + if self.health < self.flee_threshold: + self.state = "flee" + + elif self.state == "flee": + self.do_flee(player, grid) + # Check if safe + if self.distance_to(player) > self.aggro_range * 2: + self.state = "patrol" + + def distance_to(self, other_entity): + dx = self.entity.x - other_entity.x + dy = self.entity.y - other_entity.y + return (dx*dx + dy*dy) ** 0.5 + + def do_patrol(self, grid): + # Patrol logic + pass + + def do_chase(self, player): + # Chase with pathfinding + path = self.entity.path_to(player.x, player.y) + if path and len(path) > 0: + self.entity.pos = path[0] + + def do_flee(self, player, grid): + # Flee using inverted Dijkstra + # (Implementation similar to flee AI above) + pass +``` + +--- + +## Performance Considerations + +### FOV Performance + +**Cost:** O(cells in radius) +- 10 tile radius: ~314 cells to check +- 20 tile radius: ~1256 cells to check + +**Optimization:** Only recompute when needed +```python +# Don't recompute every frame! +def on_player_move(): + player.pos = new_pos + grid.compute_fov(player.x, player.y, radius=10) # Only on move +``` + +### Pathfinding Performance + +**A* Cost:** O(n log n) where n = path length +- Short paths (< 20 tiles): < 1ms +- Long paths (> 100 tiles): Can be 5-10ms + +**Optimization:** Use path caching +```python +class CachedPathfinder: + def __init__(self, entity): + self.entity = entity + self.cached_path = None + self.cached_target = None + + def path_to(self, target_x, target_y): + # Check if we can reuse cached path + if self.cached_target == (target_x, target_y) and self.cached_path: + # Remove first step if we've reached it + if self.cached_path and self.cached_path[0] == (self.entity.x, self.entity.y): + self.cached_path = self.cached_path[1:] + return self.cached_path + + # Compute new path + self.cached_path = self.entity.path_to(target_x, target_y) + self.cached_target = (target_x, target_y) + return self.cached_path +``` + +### Dijkstra Performance + +**Cost:** O(n) where n = number of cells +- 50x50 grid: 2500 cells +- 100x100 grid: 10000 cells + +**Best practice:** Compute once, query many times +```python +# Compute Dijkstra map once +grid.compute_dijkstra([(player.x, player.y)]) + +# Use for all enemies (amortized O(1) per enemy) +for enemy in enemies: + path = grid.get_dijkstra_path(enemy.x, enemy.y, max_length=1) + # Move enemy... +``` + +--- + +## Integration with Other Systems + +### With Grid System + +See [[Grid-System]] for: +- Setting walkability: `grid.walkable((x, y), True)` +- Setting transparency: `grid.transparent((x, y), True)` +- Grid perspective rendering + +### With Entity Management + +See [[Entity-Management]] for: +- Creating entities +- Moving entities along paths +- Entity lifecycle + +### With Animation + +See [[Animation-System]] for: +- Smoothly animating entity movement +- Visual feedback for AI state changes + +--- + +## Related Documentation + +- [[Grid-System]] - Grid fundamentals, TCOD integration +- [[Entity-Management]] - Entity creation and movement +- `src/UIGrid.cpp` - FOV and pathfinding implementation +- Tutorial Part 6+ - AI and pathfinding examples + +**Open Issues:** +- [#64](../../issues/64) - Grid-Entity-GridPointState TCOD Updates +- [#115](../../issues/115) - SpatialHash (improves AI spatial queries) \ No newline at end of file