Add "AI-and-Pathfinding"
parent
ef13428826
commit
e5309322c9
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue