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