diff --git a/AI-and-Pathfinding.-.md b/AI-and-Pathfinding.md similarity index 96% rename from AI-and-Pathfinding.-.md rename to AI-and-Pathfinding.md index 468d643..159b846 100644 --- a/AI-and-Pathfinding.-.md +++ b/AI-and-Pathfinding.md @@ -1,466 +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 +# 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