Update AI and Pathfinding
parent
f05289abc2
commit
3be2fec45c
1 changed files with 385 additions and 385 deletions
|
|
@ -1,386 +1,386 @@
|
|||
# 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 (`grid.find_path()` returns `AStarPath` object)
|
||||
- Dijkstra maps (`grid.get_dijkstra_map()` returns `DijkstraMap` object)
|
||||
- Field of view (`grid.compute_fov()` / `grid.is_in_fov()`)
|
||||
- Per-entity perspective rendering
|
||||
|
||||
**TCOD Integration:** `src/UIGrid.cpp` libtcod bindings
|
||||
|
||||
---
|
||||
|
||||
## Field of View (FOV)
|
||||
|
||||
### Basic FOV
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Setup grid with transparency
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
|
||||
|
||||
# Set tile properties (walkable + transparent)
|
||||
for x in range(50):
|
||||
for y in range(50):
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Add walls (opaque and unwalkable)
|
||||
grid.at(10, 10).transparent = False
|
||||
grid.at(10, 10).walkable = False
|
||||
|
||||
# Compute FOV from position with radius
|
||||
grid.compute_fov((25, 25), radius=10)
|
||||
|
||||
# Query visibility
|
||||
if grid.is_in_fov((25, 25)):
|
||||
print("Origin is visible")
|
||||
if not grid.is_in_fov((0, 0)):
|
||||
print("Far corner not visible")
|
||||
```
|
||||
|
||||
### Per-Entity Perspective
|
||||
|
||||
Each entity can have its own view of the map:
|
||||
|
||||
```python
|
||||
player = mcrfpy.Entity(grid_pos=(25, 25), sprite_index=0)
|
||||
grid.entities.append(player)
|
||||
|
||||
# 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)
|
||||
|
||||
# Clear perspective (show everything)
|
||||
grid.perspective = None
|
||||
```
|
||||
|
||||
### FOV + Fog of War Pattern
|
||||
|
||||
Use a ColorLayer to implement fog of war:
|
||||
|
||||
```python
|
||||
# Create grid with fog layer
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400), layers=[])
|
||||
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
|
||||
grid.add_layer(fog)
|
||||
|
||||
# Start fully fogged
|
||||
fog.fill(mcrfpy.Color(0, 0, 0, 255))
|
||||
|
||||
# Setup transparency
|
||||
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)
|
||||
|
||||
def update_fov():
|
||||
"""Recompute FOV and update fog layer."""
|
||||
px, py = int(player.grid_x), int(player.grid_y)
|
||||
grid.compute_fov((px, py), radius=10)
|
||||
|
||||
for x in range(50):
|
||||
for y in range(50):
|
||||
if grid.is_in_fov((x, y)):
|
||||
fog.set((x, y), mcrfpy.Color(0, 0, 0, 0)) # Visible
|
||||
# Explored but not visible: keep at partial opacity
|
||||
# (track explored state separately if needed)
|
||||
|
||||
# Initial FOV
|
||||
update_fov()
|
||||
|
||||
# Update on movement
|
||||
def on_player_move(dx, dy):
|
||||
new_x = int(player.grid_x + dx)
|
||||
new_y = int(player.grid_y + dy)
|
||||
point = grid.at(new_x, new_y)
|
||||
if point and point.walkable:
|
||||
player.grid_x = new_x
|
||||
player.grid_y = new_y
|
||||
update_fov()
|
||||
```
|
||||
|
||||
### Movement + FOV Input Handling
|
||||
|
||||
```python
|
||||
scene = mcrfpy.Scene("game")
|
||||
scene.children.append(grid)
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
move_map = {
|
||||
mcrfpy.Key.W: (0, -1),
|
||||
mcrfpy.Key.A: (-1, 0),
|
||||
mcrfpy.Key.S: (0, 1),
|
||||
mcrfpy.Key.D: (1, 0),
|
||||
}
|
||||
|
||||
def handle_key(key, action):
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
if key in move_map:
|
||||
dx, dy = move_map[key]
|
||||
on_player_move(dx, dy)
|
||||
|
||||
scene.on_key = handle_key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pathfinding
|
||||
|
||||
### A* Pathfinding
|
||||
|
||||
`grid.find_path()` returns an `AStarPath` object:
|
||||
|
||||
```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 between two points
|
||||
path = grid.find_path((10, 10), (20, 20))
|
||||
|
||||
if path and len(path) > 0:
|
||||
print(f"Path has {path.remaining} steps")
|
||||
print(f"From: ({path.origin.x}, {path.origin.y})")
|
||||
print(f"To: ({path.destination.x}, {path.destination.y})")
|
||||
|
||||
# Walk the path step by step
|
||||
next_step = path.walk() # Consume and return next step
|
||||
print(f"Next: ({next_step.x}, {next_step.y})")
|
||||
|
||||
# Peek without consuming
|
||||
upcoming = path.peek() # Look at next step without consuming
|
||||
```
|
||||
|
||||
**AStarPath API:**
|
||||
| Method/Property | Description |
|
||||
|----------------|-------------|
|
||||
| `path.walk()` | Consume and return next step (Vector) |
|
||||
| `path.peek()` | Look at next step without consuming |
|
||||
| `path.remaining` | Number of steps left |
|
||||
| `len(path)` | Same as `remaining` |
|
||||
| `path.origin` | Start position (Vector) |
|
||||
| `path.destination` | End position (Vector) |
|
||||
|
||||
### Dijkstra Maps
|
||||
|
||||
Multi-source distance maps for efficient AI:
|
||||
|
||||
```python
|
||||
# Create Dijkstra map from a single source
|
||||
dm = grid.get_dijkstra_map((15, 15))
|
||||
|
||||
# Query distance from any cell to source
|
||||
d = dm.distance((0, 0))
|
||||
print(f"Distance from (0,0) to (15,15): {d}")
|
||||
|
||||
# Get full path from a position to the source
|
||||
path_points = dm.path_from((0, 0))
|
||||
print(f"Path length: {len(path_points)}")
|
||||
|
||||
# Get single next step toward source
|
||||
next_step = dm.step_from((0, 0))
|
||||
print(f"Next step: ({next_step.x}, {next_step.y})")
|
||||
```
|
||||
|
||||
**DijkstraMap API:**
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `dm.distance((x, y))` | Distance from cell to source |
|
||||
| `dm.path_from((x, y))` | Full path from cell to source |
|
||||
| `dm.step_from((x, y))` | Single step toward source |
|
||||
| `dm.to_heightmap()` | Convert to HeightMap for visualization |
|
||||
|
||||
---
|
||||
|
||||
## AI Patterns
|
||||
|
||||
### Pattern 1: Chase AI (Dijkstra)
|
||||
|
||||
Enemies chase player using a shared Dijkstra map:
|
||||
|
||||
```python
|
||||
def update_enemies(grid, player, enemies):
|
||||
# Compute Dijkstra map with player as source
|
||||
dm = grid.get_dijkstra_map((int(player.grid_x), int(player.grid_y)))
|
||||
|
||||
for enemy in enemies:
|
||||
ex, ey = int(enemy.grid_x), int(enemy.grid_y)
|
||||
next_step = dm.step_from((ex, ey))
|
||||
if next_step:
|
||||
nx, ny = int(next_step.x), int(next_step.y)
|
||||
if grid.at(nx, ny).walkable:
|
||||
enemy.grid_x = nx
|
||||
enemy.grid_y = ny
|
||||
```
|
||||
|
||||
**Advantage:** One Dijkstra map serves all enemies (O(n) compute, O(1) per enemy query).
|
||||
|
||||
### Pattern 2: Flee AI (Inverted Dijkstra)
|
||||
|
||||
Enemies flee from danger by moving to higher-distance cells:
|
||||
|
||||
```python
|
||||
def flee_from_player(grid, player, scared_npcs):
|
||||
dm = grid.get_dijkstra_map((int(player.grid_x), int(player.grid_y)))
|
||||
|
||||
for npc in scared_npcs:
|
||||
nx, ny = int(npc.grid_x), int(npc.grid_y)
|
||||
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
|
||||
cx, cy = nx + dx, ny + dy
|
||||
if grid.at(cx, cy).walkable:
|
||||
d = dm.distance((cx, cy))
|
||||
if d > best_distance:
|
||||
best_distance = d
|
||||
best_pos = (cx, cy)
|
||||
|
||||
if best_pos:
|
||||
npc.grid_x = best_pos[0]
|
||||
npc.grid_y = best_pos[1]
|
||||
```
|
||||
|
||||
### Pattern 3: Aggro Range with Spatial Queries
|
||||
|
||||
Use `entities_in_radius()` for aggro detection:
|
||||
|
||||
```python
|
||||
class Enemy:
|
||||
def __init__(self, grid, pos, aggro_range=10):
|
||||
self.entity = mcrfpy.Entity(grid_pos=pos, sprite_index=1, name="enemy")
|
||||
self.aggro_range = aggro_range
|
||||
grid.entities.append(self.entity)
|
||||
|
||||
def update(self, grid):
|
||||
ex, ey = int(self.entity.grid_x), int(self.entity.grid_y)
|
||||
nearby = grid.entities_in_radius((ex, ey), self.aggro_range)
|
||||
|
||||
# Filter for player entities
|
||||
for entity in nearby:
|
||||
if entity.name == "player":
|
||||
self.chase(grid, entity)
|
||||
return
|
||||
|
||||
def chase(self, grid, target):
|
||||
path = grid.find_path(
|
||||
(int(self.entity.grid_x), int(self.entity.grid_y)),
|
||||
(int(target.grid_x), int(target.grid_y))
|
||||
)
|
||||
if path and len(path) > 0:
|
||||
step = path.walk()
|
||||
self.entity.grid_x = int(step.x)
|
||||
self.entity.grid_y = int(step.y)
|
||||
```
|
||||
|
||||
### Pattern 4: State Machine AI
|
||||
|
||||
Complex behaviors using states:
|
||||
|
||||
```python
|
||||
class StateMachineAI:
|
||||
def __init__(self, grid, pos):
|
||||
self.entity = mcrfpy.Entity(grid_pos=pos, sprite_index=3, name="ai")
|
||||
self.state = "patrol"
|
||||
self.health = 100
|
||||
self.aggro_range = 10
|
||||
self.flee_threshold = 30
|
||||
grid.entities.append(self.entity)
|
||||
|
||||
def update(self, grid, player):
|
||||
if self.state == "patrol":
|
||||
# Check for player in range
|
||||
nearby = grid.entities_in_radius(
|
||||
(int(self.entity.grid_x), int(self.entity.grid_y)),
|
||||
self.aggro_range
|
||||
)
|
||||
for e in nearby:
|
||||
if e.name == "player":
|
||||
self.state = "chase"
|
||||
return
|
||||
|
||||
elif self.state == "chase":
|
||||
if self.health < self.flee_threshold:
|
||||
self.state = "flee"
|
||||
return
|
||||
# Chase with A*
|
||||
path = grid.find_path(
|
||||
(int(self.entity.grid_x), int(self.entity.grid_y)),
|
||||
(int(player.grid_x), int(player.grid_y))
|
||||
)
|
||||
if path and len(path) > 0:
|
||||
step = path.walk()
|
||||
self.entity.grid_x = int(step.x)
|
||||
self.entity.grid_y = int(step.y)
|
||||
|
||||
elif self.state == "flee":
|
||||
dm = grid.get_dijkstra_map(
|
||||
(int(player.grid_x), int(player.grid_y))
|
||||
)
|
||||
# Move away from player (maximize distance)
|
||||
# ... (see Flee AI pattern above)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### FOV: Only Recompute on Move
|
||||
|
||||
```python
|
||||
# Don't recompute every frame
|
||||
def on_player_move(dx, dy):
|
||||
# ... move player ...
|
||||
grid.compute_fov((new_x, new_y), radius=10) # Only on move
|
||||
```
|
||||
|
||||
### Pathfinding: Cache Dijkstra Maps
|
||||
|
||||
```python
|
||||
# Compute once per turn, query many times
|
||||
dm = grid.get_dijkstra_map((int(player.grid_x), int(player.grid_y)))
|
||||
|
||||
for enemy in enemies:
|
||||
step = dm.step_from((int(enemy.grid_x), int(enemy.grid_y)))
|
||||
# O(1) per enemy vs O(n log n) for individual A*
|
||||
```
|
||||
|
||||
### A* vs Dijkstra Trade-offs
|
||||
|
||||
| Method | Best For | Cost |
|
||||
|--------|----------|------|
|
||||
| `find_path()` (A*) | Single entity, single target | O(n log n) per call |
|
||||
| `get_dijkstra_map()` | Many entities, one target | O(n) compute, O(1) per query |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [[Grid-System]] - Grid fundamentals, cell properties
|
||||
- [[Entity-Management]] - Entity creation and movement
|
||||
- [[Animation-System]] - Smooth entity movement animations
|
||||
- [[Input-and-Events]] - Keyboard handler for movement
|
||||
|
||||
---
|
||||
|
||||
# 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 (`grid.find_path()` returns `AStarPath` object)
|
||||
- Dijkstra maps (`grid.get_dijkstra_map()` returns `DijkstraMap` object)
|
||||
- Field of view (`grid.compute_fov()` / `grid.is_in_fov()`)
|
||||
- Per-entity perspective rendering
|
||||
|
||||
**TCOD Integration:** `src/UIGrid.cpp` libtcod bindings
|
||||
|
||||
---
|
||||
|
||||
## Field of View (FOV)
|
||||
|
||||
### Basic FOV
|
||||
|
||||
```python
|
||||
import mcrfpy
|
||||
|
||||
# Setup grid with transparency
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400))
|
||||
|
||||
# Set tile properties (walkable + transparent)
|
||||
for x in range(50):
|
||||
for y in range(50):
|
||||
grid.at(x, y).transparent = True
|
||||
grid.at(x, y).walkable = True
|
||||
|
||||
# Add walls (opaque and unwalkable)
|
||||
grid.at(10, 10).transparent = False
|
||||
grid.at(10, 10).walkable = False
|
||||
|
||||
# Compute FOV from position with radius
|
||||
grid.compute_fov((25, 25), radius=10)
|
||||
|
||||
# Query visibility
|
||||
if grid.is_in_fov((25, 25)):
|
||||
print("Origin is visible")
|
||||
if not grid.is_in_fov((0, 0)):
|
||||
print("Far corner not visible")
|
||||
```
|
||||
|
||||
### Per-Entity Perspective
|
||||
|
||||
Each entity can have its own view of the map:
|
||||
|
||||
```python
|
||||
player = mcrfpy.Entity(grid_pos=(25, 25), sprite_index=0)
|
||||
grid.entities.append(player)
|
||||
|
||||
# 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)
|
||||
|
||||
# Clear perspective (show everything)
|
||||
grid.perspective = None
|
||||
```
|
||||
|
||||
### FOV + Fog of War Pattern
|
||||
|
||||
Use a ColorLayer to implement fog of war:
|
||||
|
||||
```python
|
||||
# Create grid with fog layer
|
||||
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(400, 400), layers=[])
|
||||
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
|
||||
grid.add_layer(fog)
|
||||
|
||||
# Start fully fogged
|
||||
fog.fill(mcrfpy.Color(0, 0, 0, 255))
|
||||
|
||||
# Setup transparency
|
||||
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)
|
||||
|
||||
def update_fov():
|
||||
"""Recompute FOV and update fog layer."""
|
||||
px, py = int(player.grid_x), int(player.grid_y)
|
||||
grid.compute_fov((px, py), radius=10)
|
||||
|
||||
for x in range(50):
|
||||
for y in range(50):
|
||||
if grid.is_in_fov((x, y)):
|
||||
fog.set((x, y), mcrfpy.Color(0, 0, 0, 0)) # Visible
|
||||
# Explored but not visible: keep at partial opacity
|
||||
# (track explored state separately if needed)
|
||||
|
||||
# Initial FOV
|
||||
update_fov()
|
||||
|
||||
# Update on movement
|
||||
def on_player_move(dx, dy):
|
||||
new_x = int(player.grid_x + dx)
|
||||
new_y = int(player.grid_y + dy)
|
||||
point = grid.at(new_x, new_y)
|
||||
if point and point.walkable:
|
||||
player.grid_x = new_x
|
||||
player.grid_y = new_y
|
||||
update_fov()
|
||||
```
|
||||
|
||||
### Movement + FOV Input Handling
|
||||
|
||||
```python
|
||||
scene = mcrfpy.Scene("game")
|
||||
scene.children.append(grid)
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
move_map = {
|
||||
mcrfpy.Key.W: (0, -1),
|
||||
mcrfpy.Key.A: (-1, 0),
|
||||
mcrfpy.Key.S: (0, 1),
|
||||
mcrfpy.Key.D: (1, 0),
|
||||
}
|
||||
|
||||
def handle_key(key, action):
|
||||
if action != mcrfpy.InputState.PRESSED:
|
||||
return
|
||||
if key in move_map:
|
||||
dx, dy = move_map[key]
|
||||
on_player_move(dx, dy)
|
||||
|
||||
scene.on_key = handle_key
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pathfinding
|
||||
|
||||
### A* Pathfinding
|
||||
|
||||
`grid.find_path()` returns an `AStarPath` object:
|
||||
|
||||
```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 between two points
|
||||
path = grid.find_path((10, 10), (20, 20))
|
||||
|
||||
if path and len(path) > 0:
|
||||
print(f"Path has {path.remaining} steps")
|
||||
print(f"From: ({path.origin.x}, {path.origin.y})")
|
||||
print(f"To: ({path.destination.x}, {path.destination.y})")
|
||||
|
||||
# Walk the path step by step
|
||||
next_step = path.walk() # Consume and return next step
|
||||
print(f"Next: ({next_step.x}, {next_step.y})")
|
||||
|
||||
# Peek without consuming
|
||||
upcoming = path.peek() # Look at next step without consuming
|
||||
```
|
||||
|
||||
**AStarPath API:**
|
||||
| Method/Property | Description |
|
||||
|----------------|-------------|
|
||||
| `path.walk()` | Consume and return next step (Vector) |
|
||||
| `path.peek()` | Look at next step without consuming |
|
||||
| `path.remaining` | Number of steps left |
|
||||
| `len(path)` | Same as `remaining` |
|
||||
| `path.origin` | Start position (Vector) |
|
||||
| `path.destination` | End position (Vector) |
|
||||
|
||||
### Dijkstra Maps
|
||||
|
||||
Multi-source distance maps for efficient AI:
|
||||
|
||||
```python
|
||||
# Create Dijkstra map from a single source
|
||||
dm = grid.get_dijkstra_map((15, 15))
|
||||
|
||||
# Query distance from any cell to source
|
||||
d = dm.distance((0, 0))
|
||||
print(f"Distance from (0,0) to (15,15): {d}")
|
||||
|
||||
# Get full path from a position to the source
|
||||
path_points = dm.path_from((0, 0))
|
||||
print(f"Path length: {len(path_points)}")
|
||||
|
||||
# Get single next step toward source
|
||||
next_step = dm.step_from((0, 0))
|
||||
print(f"Next step: ({next_step.x}, {next_step.y})")
|
||||
```
|
||||
|
||||
**DijkstraMap API:**
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `dm.distance((x, y))` | Distance from cell to source |
|
||||
| `dm.path_from((x, y))` | Full path from cell to source |
|
||||
| `dm.step_from((x, y))` | Single step toward source |
|
||||
| `dm.to_heightmap()` | Convert to HeightMap for visualization |
|
||||
|
||||
---
|
||||
|
||||
## AI Patterns
|
||||
|
||||
### Pattern 1: Chase AI (Dijkstra)
|
||||
|
||||
Enemies chase player using a shared Dijkstra map:
|
||||
|
||||
```python
|
||||
def update_enemies(grid, player, enemies):
|
||||
# Compute Dijkstra map with player as source
|
||||
dm = grid.get_dijkstra_map((int(player.grid_x), int(player.grid_y)))
|
||||
|
||||
for enemy in enemies:
|
||||
ex, ey = int(enemy.grid_x), int(enemy.grid_y)
|
||||
next_step = dm.step_from((ex, ey))
|
||||
if next_step:
|
||||
nx, ny = int(next_step.x), int(next_step.y)
|
||||
if grid.at(nx, ny).walkable:
|
||||
enemy.grid_x = nx
|
||||
enemy.grid_y = ny
|
||||
```
|
||||
|
||||
**Advantage:** One Dijkstra map serves all enemies (O(n) compute, O(1) per enemy query).
|
||||
|
||||
### Pattern 2: Flee AI (Inverted Dijkstra)
|
||||
|
||||
Enemies flee from danger by moving to higher-distance cells:
|
||||
|
||||
```python
|
||||
def flee_from_player(grid, player, scared_npcs):
|
||||
dm = grid.get_dijkstra_map((int(player.grid_x), int(player.grid_y)))
|
||||
|
||||
for npc in scared_npcs:
|
||||
nx, ny = int(npc.grid_x), int(npc.grid_y)
|
||||
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
|
||||
cx, cy = nx + dx, ny + dy
|
||||
if grid.at(cx, cy).walkable:
|
||||
d = dm.distance((cx, cy))
|
||||
if d > best_distance:
|
||||
best_distance = d
|
||||
best_pos = (cx, cy)
|
||||
|
||||
if best_pos:
|
||||
npc.grid_x = best_pos[0]
|
||||
npc.grid_y = best_pos[1]
|
||||
```
|
||||
|
||||
### Pattern 3: Aggro Range with Spatial Queries
|
||||
|
||||
Use `entities_in_radius()` for aggro detection:
|
||||
|
||||
```python
|
||||
class Enemy:
|
||||
def __init__(self, grid, pos, aggro_range=10):
|
||||
self.entity = mcrfpy.Entity(grid_pos=pos, sprite_index=1, name="enemy")
|
||||
self.aggro_range = aggro_range
|
||||
grid.entities.append(self.entity)
|
||||
|
||||
def update(self, grid):
|
||||
ex, ey = int(self.entity.grid_x), int(self.entity.grid_y)
|
||||
nearby = grid.entities_in_radius((ex, ey), self.aggro_range)
|
||||
|
||||
# Filter for player entities
|
||||
for entity in nearby:
|
||||
if entity.name == "player":
|
||||
self.chase(grid, entity)
|
||||
return
|
||||
|
||||
def chase(self, grid, target):
|
||||
path = grid.find_path(
|
||||
(int(self.entity.grid_x), int(self.entity.grid_y)),
|
||||
(int(target.grid_x), int(target.grid_y))
|
||||
)
|
||||
if path and len(path) > 0:
|
||||
step = path.walk()
|
||||
self.entity.grid_x = int(step.x)
|
||||
self.entity.grid_y = int(step.y)
|
||||
```
|
||||
|
||||
### Pattern 4: State Machine AI
|
||||
|
||||
Complex behaviors using states:
|
||||
|
||||
```python
|
||||
class StateMachineAI:
|
||||
def __init__(self, grid, pos):
|
||||
self.entity = mcrfpy.Entity(grid_pos=pos, sprite_index=3, name="ai")
|
||||
self.state = "patrol"
|
||||
self.health = 100
|
||||
self.aggro_range = 10
|
||||
self.flee_threshold = 30
|
||||
grid.entities.append(self.entity)
|
||||
|
||||
def update(self, grid, player):
|
||||
if self.state == "patrol":
|
||||
# Check for player in range
|
||||
nearby = grid.entities_in_radius(
|
||||
(int(self.entity.grid_x), int(self.entity.grid_y)),
|
||||
self.aggro_range
|
||||
)
|
||||
for e in nearby:
|
||||
if e.name == "player":
|
||||
self.state = "chase"
|
||||
return
|
||||
|
||||
elif self.state == "chase":
|
||||
if self.health < self.flee_threshold:
|
||||
self.state = "flee"
|
||||
return
|
||||
# Chase with A*
|
||||
path = grid.find_path(
|
||||
(int(self.entity.grid_x), int(self.entity.grid_y)),
|
||||
(int(player.grid_x), int(player.grid_y))
|
||||
)
|
||||
if path and len(path) > 0:
|
||||
step = path.walk()
|
||||
self.entity.grid_x = int(step.x)
|
||||
self.entity.grid_y = int(step.y)
|
||||
|
||||
elif self.state == "flee":
|
||||
dm = grid.get_dijkstra_map(
|
||||
(int(player.grid_x), int(player.grid_y))
|
||||
)
|
||||
# Move away from player (maximize distance)
|
||||
# ... (see Flee AI pattern above)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### FOV: Only Recompute on Move
|
||||
|
||||
```python
|
||||
# Don't recompute every frame
|
||||
def on_player_move(dx, dy):
|
||||
# ... move player ...
|
||||
grid.compute_fov((new_x, new_y), radius=10) # Only on move
|
||||
```
|
||||
|
||||
### Pathfinding: Cache Dijkstra Maps
|
||||
|
||||
```python
|
||||
# Compute once per turn, query many times
|
||||
dm = grid.get_dijkstra_map((int(player.grid_x), int(player.grid_y)))
|
||||
|
||||
for enemy in enemies:
|
||||
step = dm.step_from((int(enemy.grid_x), int(enemy.grid_y)))
|
||||
# O(1) per enemy vs O(n log n) for individual A*
|
||||
```
|
||||
|
||||
### A* vs Dijkstra Trade-offs
|
||||
|
||||
| Method | Best For | Cost |
|
||||
|--------|----------|------|
|
||||
| `find_path()` (A*) | Single entity, single target | O(n log n) per call |
|
||||
| `get_dijkstra_map()` | Many entities, one target | O(n) compute, O(1) per query |
|
||||
|
||||
---
|
||||
|
||||
## Related Documentation
|
||||
|
||||
- [[Grid-System]] - Grid fundamentals, cell properties
|
||||
- [[Entity-Management]] - Entity creation and movement
|
||||
- [[Animation-System]] - Smooth entity movement animations
|
||||
- [[Input-and-Events]] - Keyboard handler for movement
|
||||
|
||||
---
|
||||
|
||||
*Last updated: 2026-02-07*
|
||||
Loading…
Add table
Add a link
Reference in a new issue