3 Grid TCOD Integration
John McCardle edited this page 2026-02-07 23:48:48 +00:00

Grid TCOD Integration

Overview

McRogueFace integrates with libtcod for FOV (field of view), A* pathfinding, and Dijkstra maps. The integration automatically synchronizes each grid's walkability and transparency properties with an internal TCODMap.

Parent Page: Grid-System

Related Pages:

Key Files:

  • src/UIGrid.cpp - TCODMap synchronization, FOV, pathfinding
  • src/UIGrid.h - TCODMap, TCODPath, TCODDijkstra members

The World State Layer

Cell Properties as World Physics

Each grid cell (GridPoint) has properties that drive TCOD algorithms:

Visual Layer (ColorLayer/TileLayer) - What's displayed (colors, sprites)
          |
World State Layer (GridPoint)       - Physical properties (walkable, transparent)
          |
Perspective Layer                   - Per-entity knowledge (FOV results)
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))

cell = grid.at(10, 10)
cell.walkable = True       # Affects pathfinding
cell.transparent = True    # Affects FOV
cell.tilesprite = 0        # Visual tile index (legacy)

Automatic Synchronization: When you set cell.walkable or cell.transparent, the internal TCODMap is automatically updated. There is no manual sync step required.


Field of View (FOV)

Computing FOV

FOV determines which cells are visible from a given position:

grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))

# Make all cells transparent
for x in range(50):
    for y in range(50):
        grid.at(x, y).transparent = True
        grid.at(x, y).walkable = True

# Add some walls
for x in range(20, 30):
    grid.at(x, 15).transparent = False
    grid.at(x, 15).walkable = False

# Compute FOV from position with radius
grid.compute_fov((25, 25), radius=10)

# Query visibility of specific cells
if grid.is_in_fov((25, 25)):
    print("Origin is visible")

if not grid.is_in_fov((25, 5)):
    print("Behind wall is not visible")

API:

  • grid.compute_fov((x, y), radius=N) - Compute FOV from position
  • grid.is_in_fov((x, y)) - Query if cell is currently visible

FOV with Fog Overlay

Use a ColorLayer to visualize FOV:

grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[])

# Create fog overlay above entities
fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid.add_layer(fog)
fog.fill(mcrfpy.Color(0, 0, 0, 255))  # Start fully hidden

# After computing FOV, reveal visible cells
def update_fog(grid, fog, pos, radius=10):
    grid.compute_fov(pos, radius=radius)
    w, h = grid.grid_size
    for x in range(w):
        for y in range(h):
            if grid.is_in_fov((x, y)):
                fog.set((x, y), mcrfpy.Color(0, 0, 0, 0))    # Visible
            else:
                fog.set((x, y), mcrfpy.Color(0, 0, 0, 192))  # Dim

update_fog(grid, fog, (25, 25))

A* Pathfinding

Finding Paths

Find the shortest path between two walkable cells:

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 - returns AStarPath object
path = grid.find_path((5, 5), (25, 25))

if path is not None and len(path) > 0:
    # Walk the path (consumes next step)
    next_step = path.walk()
    print(f"Next step: ({next_step.x}, {next_step.y})")
    
    # Peek at next step without consuming
    upcoming = path.peek()
    
    # Check remaining steps
    print(f"Remaining: {path.remaining}")
    
    # Check endpoints
    print(f"From: {path.origin}")
    print(f"To: {path.destination}")

AStarPath Object

Property/Method Description
len(path) Total steps in path
path.walk() Get and consume next step (returns Vector)
path.peek() View next step without consuming
path.remaining Steps remaining
path.origin Start position (Vector)
path.destination End position (Vector)

Moving Entities Along Paths

player = mcrfpy.Entity(grid_pos=(5, 5), sprite_index=0)
grid.entities.append(player)

# Find path to target
path = grid.find_path(
    (int(player.grid_x), int(player.grid_y)),
    (25, 25)
)

if path and len(path) > 0:
    step = path.walk()
    player.grid_x = int(step.x)
    player.grid_y = int(step.y)

Dijkstra Maps

Computing Dijkstra Maps

Dijkstra maps compute distances from a goal to all reachable cells. Useful for multi-enemy AI where many entities path toward the same target:

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

# Create Dijkstra map from goal position
dm = grid.get_dijkstra_map((15, 15))

# Query distance from any cell to goal
d = dm.distance((0, 0))
print(f"Distance from (0,0) to goal: {d}")

# Get full path from any cell to goal
path = dm.path_from((0, 0))
print(f"Path length: {len(path)}")

# Get just the next step toward goal
next_step = dm.step_from((0, 0))
print(f"Next step: ({next_step.x}, {next_step.y})")

DijkstraMap Object

Method Description
dm.distance((x, y)) Distance from cell to goal
dm.path_from((x, y)) Full path from cell to goal
dm.step_from((x, y)) Next step from cell toward goal

Dijkstra vs A*

Feature A* (find_path) Dijkstra (get_dijkstra_map)
Goals Single target Single target, query from anywhere
Computation One path at a time One map, unlimited queries
Use case Single entity, single target Many entities, same target
Performance Fast per query O(n) once, then O(1) per query

Rule of thumb: 1-5 entities -> A* per entity. 10+ entities with same goal -> Dijkstra map.


Entity Perspective System

Setting Grid Perspective

grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600))
player = mcrfpy.Entity(grid_pos=(25, 25), sprite_index=0)
grid.entities.append(player)

# Assign perspective (property, not method)
grid.perspective = player

# Grid rendering now uses player's FOV for visibility
grid.compute_fov((int(player.grid_x), int(player.grid_y)), radius=10)

FOV Update on Movement

scene = mcrfpy.Scene("game")
grid = mcrfpy.Grid(grid_size=(50, 50), pos=(0, 0), size=(800, 600), layers=[])

fog = mcrfpy.ColorLayer(name="fog", z_index=1)
grid.add_layer(fog)
fog.fill(mcrfpy.Color(0, 0, 0, 255))

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)
grid.perspective = player
scene.children.append(grid)

def update_fov():
    """Call after player moves"""
    px, py = int(player.grid_x), int(player.grid_y)
    grid.compute_fov((px, py), radius=10)
    w, h = grid.grid_size
    for x in range(w):
        for y in range(h):
            if grid.is_in_fov((x, y)):
                fog.set((x, y), mcrfpy.Color(0, 0, 0, 0))

def on_key(key, action):
    if action != mcrfpy.InputState.PRESSED:
        return
    dx, dy = 0, 0
    if key == mcrfpy.Key.W: dy = -1
    elif key == mcrfpy.Key.S: dy = 1
    elif key == mcrfpy.Key.A: dx = -1
    elif key == mcrfpy.Key.D: dx = 1
    
    if dx or dy:
        nx = int(player.grid_x) + dx
        ny = int(player.grid_y) + dy
        if grid.at(nx, ny).walkable:
            player.grid_x = nx
            player.grid_y = ny
            update_fov()

scene.on_key = on_key
update_fov()  # Initial FOV

Common Patterns

Opening a Door

def open_door(grid, door_x, door_y):
    """Open door - update world state (auto-syncs to TCOD)"""
    cell = grid.at(door_x, door_y)
    cell.walkable = True
    cell.transparent = True
    cell.tilesprite = 2  # Open door sprite
    
    # Recompute FOV if player nearby
    px, py = int(player.grid_x), int(player.grid_y)
    grid.compute_fov((px, py), radius=10)

Dynamic Obstacle

def boulder_falls(grid, x, y):
    """Boulder blocks cell"""
    cell = grid.at(x, y)
    cell.walkable = False
    cell.transparent = False
    cell.tilesprite = 3  # Boulder sprite
    # TCOD map auto-updated - paths through this cell now invalid

Chase AI with Dijkstra

def update_enemies(grid, player, enemies):
    """Move all enemies toward player using Dijkstra map"""
    px, py = int(player.grid_x), int(player.grid_y)
    dm = grid.get_dijkstra_map((px, py))
    
    for enemy in enemies:
        ex, ey = int(enemy.grid_x), int(enemy.grid_y)
        next_step = dm.step_from((ex, ey))
        if next_step is not None:
            enemy.grid_x = int(next_step.x)
            enemy.grid_y = int(next_step.y)

Spatial Queries

# Find entities near a position
nearby = grid.entities_in_radius((int(enemy.grid_x), int(enemy.grid_y)), 5.0)
for entity in nearby:
    print(f"Nearby: {entity.name}")

Performance Considerations

FOV Cost

FOV computation time scales with radius and grid size. Only compute when the entity moves:

last_pos = [None]

def update_fov_if_moved():
    px, py = int(player.grid_x), int(player.grid_y)
    if last_pos[0] != (px, py):
        grid.compute_fov((px, py), radius=10)
        last_pos[0] = (px, py)

Pathfinding Cost

  • Limit search distance for distant targets
  • Use Dijkstra maps for many entities with same goal
  • Cache paths and recompute only when grid changes

Cell Property Changes

Setting walkable or transparent auto-syncs to TCOD. For bulk changes, set all properties first, then compute FOV/paths:

# Set many cells, then compute once
for x in range(100):
    for y in range(100):
        grid.at(x, y).walkable = compute_walkable(x, y)

# Single FOV computation after all changes
grid.compute_fov((px, py), radius=10)

Troubleshooting

Issue: Pathfinding Returns None

Causes:

  1. Target is unreachable (blocked by walls)
  2. Start or end position is non-walkable

Debug:

path = grid.find_path((x1, y1), (x2, y2))
if path is None or len(path) == 0:
    print(f"Start walkable: {grid.at(x1, y1).walkable}")
    print(f"End walkable: {grid.at(x2, y2).walkable}")

Issue: FOV Doesn't Match Expected

Cause: Cell transparent property not set correctly.

Fix: Ensure walls have transparent = False:

cell = grid.at(x, y)
cell.walkable = False
cell.transparent = False  # Must set both for walls

Issue: Entity Can See Through Glass

Glass cells should block movement but allow sight:

glass = grid.at(x, y)
glass.walkable = False     # Can't walk through
glass.transparent = True   # CAN see through

API Quick Reference

FOV:

  • grid.compute_fov((x, y), radius=N) - Compute FOV from position
  • grid.is_in_fov((x, y)) - Check if cell is visible

A Pathfinding:*

  • grid.find_path((x1, y1), (x2, y2)) - Returns AStarPath object

Dijkstra Maps:

  • grid.get_dijkstra_map((x, y)) - Returns DijkstraMap object
  • dm.distance((x, y)) - Distance to goal
  • dm.path_from((x, y)) - Full path to goal
  • dm.step_from((x, y)) - Next step toward goal

Spatial Queries:

  • grid.entities_in_radius((x, y), radius) - Find nearby entities

Perspective:

  • grid.perspective = entity - Set FOV perspective entity

Cell Properties:

  • cell.walkable - Bool, affects pathfinding
  • cell.transparent - Bool, affects FOV

Navigation: