Page:
AI and Pathfinding
Pages
AI and Pathfinding
Adding Python Bindings
Animation System
Design Proposals
Development Workflow
Entity Management
Grid Interaction Patterns
Grid Rendering Pipeline
Grid System
Grid TCOD Integration
Headless Mode
Home
Input and Events
Issue Roadmap
LLM Agent Testbed Architecture
Performance Optimization Workflow
Performance and Profiling
Procedural-Generation
Proposal: Next Generation Grid & Entity System
Python Binding Layer
Rendering and Visuals
Strategic Direction
UI Component Hierarchy
UI Widget Patterns
Writing Tests
No results
3
AI and Pathfinding
John McCardle edited this page 2026-02-07 23:47:47 +00:00
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()returnsAStarPathobject) - Dijkstra maps (
grid.get_dijkstra_map()returnsDijkstraMapobject) - 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
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:
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:
# 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
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:
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:
# 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:
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:
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:
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:
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
# 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
# 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