Grid System Guide¶
Overview¶
The MAID Grid System provides a coordinate-based spatial indexing system for organizing rooms in a navigable 3D space. It enables efficient O(1) coordinate lookups, spatial queries like radius searches, and A* pathfinding between rooms.
Key features: - 3D Coordinate System: X (east/west), Y (north/south), Z (up/down) - O(1) Lookups: Fast room lookup by coordinate or room ID - Spatial Queries: Find rooms within radius, rectangle, or nearest neighbor - A* Pathfinding: Efficient pathfinding with terrain cost support - Auto Exit Creation: Automatic bidirectional exit creation between adjacent rooms - Blocked Coordinates: Reserve coordinates to prevent room placement - Serialization: Export/import grid state for persistence
Quick Start¶
from maid_engine.core.grid import GridManager, GridCoord, GridRoom
from uuid import uuid4
# Create a grid manager with auto-exit creation
grid = GridManager(auto_create_exits=True, allow_diagonals=True)
# Register rooms at coordinates
room1_id = uuid4()
room2_id = uuid4()
grid.register_room(room1_id, GridCoord(0, 0), terrain_type="plains")
grid.register_room(room2_id, GridCoord(1, 0), terrain_type="forest")
# Auto-created exits: room1 has exit east to room2, room2 has exit west to room1
# Look up rooms
room = grid.get_room_at(GridCoord(0, 0))
coord = grid.get_coord(room1_id)
# Find path between rooms
result = grid.find_path(room1_id, room2_id)
if result.found:
print(f"Path: {result.directions}") # ['e']
Core Classes¶
GridCoord¶
An immutable 3D coordinate in the grid.
from maid_engine.core.grid import GridCoord
# Create coordinates
origin = GridCoord(0, 0, 0)
east = GridCoord(1, 0, 0)
up = GridCoord(0, 0, 1)
# Arithmetic operations
delta = east - origin # GridCoord(1, 0, 0)
combined = origin + GridCoord(5, 5, 0) # GridCoord(5, 5, 0)
# Distance calculations
origin.distance_to(GridCoord(3, 4, 0)) # 5.0 (Euclidean)
origin.manhattan_distance(GridCoord(3, 4, 0)) # 7
origin.chebyshev_distance(GridCoord(3, 4, 0)) # 4
# Get neighbors
origin.neighbors() # [n, s, e, w] neighbors
origin.neighbors(diagonals=True) # [n, s, e, w, ne, nw, se, sw]
origin.neighbors(diagonals=True, vertical=True) # All 10 neighbors
# Direction utilities
origin.direction_to(GridCoord(1, 1, 0)) # "ne"
GridCoord.from_direction("ne", origin) # GridCoord(1, 1, 0)
GridRoom¶
Metadata for a room at a specific grid coordinate.
from maid_engine.core.grid import GridRoom, GridCoord
from uuid import uuid4
room = GridRoom(
room_id=uuid4(),
coord=GridCoord(5, 10),
movement_cost=1.5, # Higher = harder to traverse
terrain_type="forest",
blocked=False,
metadata={"zone": "wilderness"},
)
# Serialize
data = room.to_dict()
restored = GridRoom.from_dict(data)
GridManager¶
The main interface for managing the grid.
from maid_engine.core.grid import GridManager, GridCoord
from maid_engine.core.world import World
from maid_engine.config.settings import Settings
# Create with World reference for event integration
world = World(Settings())
grid = GridManager(
world=world,
auto_create_exits=True,
allow_diagonals=True,
allow_vertical=False,
)
# Or create standalone
grid = GridManager()
Configuration Options¶
| Option | Type | Default | Description |
|---|---|---|---|
world |
World \| None |
None |
World reference for events and entity lookups |
auto_create_exits |
bool |
False |
Automatically create exits between adjacent rooms |
allow_diagonals |
bool |
False |
Include diagonal movement (ne, nw, se, sw) |
allow_vertical |
bool |
False |
Include vertical movement (u, d) |
Room Registration¶
Basic Registration¶
from uuid import uuid4
from maid_engine.core.grid import GridManager, GridCoord
grid = GridManager(auto_create_exits=True)
# Register with room ID and coordinate
room_id = uuid4()
grid.register_room(
room_id,
GridCoord(5, 10),
movement_cost=1.0,
terrain_type="plains",
metadata={"zone": "starting_area"},
)
# Or register a pre-built GridRoom
from maid_engine.core.grid import GridRoom
grid_room = GridRoom(
room_id=uuid4(),
coord=GridCoord(6, 10),
terrain_type="forest",
)
grid.register_room(grid_room)
Bulk Registration¶
# Create a 10x10 grid of rooms
def make_room(coord):
room_id = uuid4()
# Create actual room entity in world...
return room_id
rooms = grid.create_grid_area(
GridCoord(0, 0),
GridCoord(9, 9),
room_factory=make_room,
)
print(f"Created {len(rooms)} rooms")
Blocked Coordinates¶
# Block coordinates to prevent room placement
grid.block_coord(GridCoord(5, 5))
# Check if blocked
grid.is_blocked(GridCoord(5, 5)) # True
# Unblock
grid.unblock_coord(GridCoord(5, 5))
Spatial Queries¶
Point Lookups¶
# O(1) coordinate lookup
room = grid.get_room_at(GridCoord(5, 10))
room = grid.get_room_at_xyz(5, 10, 0)
# Reverse lookup: get coordinate from room ID
coord = grid.get_coord(room_id)
# Check existence
if grid.has_room_at(GridCoord(5, 10)):
print("Room exists")
Range Queries¶
# Find rooms within Euclidean radius
nearby = grid.rooms_in_radius(GridCoord(5, 10), radius=5.0)
# Find rooms in rectangular region
region = grid.rooms_in_rect(
GridCoord(0, 0),
GridCoord(10, 10),
)
# Find nearest room to a coordinate
nearest = grid.nearest_room(GridCoord(100, 100), max_distance=50.0)
Iteration¶
# Iterate all rooms
for room in grid:
print(f"{room.coord}: {room.terrain_type}")
# Get count
print(f"Total rooms: {len(grid)}")
# Check containment
if GridCoord(5, 10) in grid:
print("Coordinate has a room")
Pathfinding¶
The GridManager provides A* pathfinding with terrain cost support.
from maid_engine.core.grid import GridManager
grid = GridManager(allow_diagonals=True)
# ... register rooms ...
# Find path between two rooms
result = grid.find_path(start_room_id, end_room_id)
if result.found:
print(f"Path length: {result.length}")
print(f"Total cost: {result.total_cost}")
print(f"Directions: {result.directions}") # ['n', 'ne', 'e', ...]
print(f"Rooms: {result.rooms}") # List of room UUIDs
print(f"Path: {result.path}") # List of GridCoords
# With constraints
result = grid.find_path(
start_room_id,
end_room_id,
max_length=20, # Maximum path length
can_pass=lambda room: not room.blocked and room.terrain_type != "water",
)
PathResult Fields¶
| Field | Type | Description |
|---|---|---|
found |
bool |
Whether a path was found |
path |
list[GridCoord] |
Coordinates from start to end |
rooms |
list[UUID] |
Room IDs along the path |
directions |
list[str] |
Direction strings to follow |
total_cost |
float |
Total movement cost |
rooms_searched |
int |
Number of rooms evaluated |
length |
int |
Number of steps (property) |
Movement Costs¶
Pathfinding considers terrain movement costs:
from uuid import uuid4
# Higher movement_cost = less desirable path
difficult_id = uuid4()
grid.register_room(difficult_id, GridCoord(1, 0), movement_cost=2.0) # Difficult terrain
# Impassable terrain: register a GridRoom with blocked=True at its OWN
# coordinate (a distinct room_id and free coord — registering a second room on
# an occupied coord raises CoordinateOccupiedError). A* pathfinding checks each
# room's `blocked` attribute, so a blocked room is never routed through.
blocked_id = uuid4()
grid.register_room(GridRoom(room_id=blocked_id, coord=GridCoord(2, 0), blocked=True))
# NOTE: `block_coord()` does NOT make an already-registered room impassable to
# pathfinding — it only marks a coordinate so no room can be *placed* there
# (register_room raises CoordinateBlockedError for a blocked coord). A room
# registered before you call block_coord() keeps blocked=False and remains
# fully traversable. Use GridRoom(blocked=True) to block movement.
Exit Management¶
When auto_create_exits is enabled, the grid automatically creates bidirectional exits between adjacent rooms.
Exit keys are the short direction codes (n, s, e, w, ne, nw,
se, sw, u, d) — get_exit/remove_exit do not normalize long names.
# Get all exits from a room
exits = grid.get_exits(room_id) # {"n": UUID(...), "e": UUID(...)}
# Get specific exit (use the short code, not "north")
destination = grid.get_exit(room_id, "e")
# Remove an exit
grid.remove_exit(room_id, "e")
Direction Constants¶
The grid system provides direction constants:
from maid_engine.core.grid import (
DIRECTION_OFFSETS, # {"n": (0, 1, 0), "s": (0, -1, 0), ...}
DIRECTION_NAMES, # {(0, 1, 0): "n", (0, -1, 0): "s", ...}
CARDINAL_DIRECTIONS, # ("n", "s", "e", "w")
DIAGONAL_DIRECTIONS, # ("ne", "nw", "se", "sw")
VERTICAL_DIRECTIONS, # ("u", "d")
)
Serialization¶
Export and import grid state for persistence:
import json
from uuid import UUID
# Export grid state
data = grid.export_grid()
with open("grid.json", "w") as f:
json.dump(data, f)
# Import grid state
with open("grid.json", "r") as f:
data = json.load(f)
grid.import_grid(data)
# Merge with existing data (no clear)
grid.import_grid(data, clear_existing=False)
# Custom room ID resolver: remap specific old room UUIDs to new ones while
# passing every other UUID string through unchanged. Use an explicit branch so
# the UUID(...) fallback is only evaluated for unmapped ids (dict.get(s, UUID(s))
# would parse s eagerly on every call, even for mapped keys).
id_map = {
"550e8400-e29b-41d4-a716-446655440000": UUID("11111111-1111-1111-1111-111111111111"),
}
def resolve_room_id(room_id_str: str) -> UUID:
mapped = id_map.get(room_id_str)
if mapped is not None:
return mapped
return UUID(room_id_str)
grid.import_grid(data, room_resolver=resolve_room_id)
Integration with World¶
When a World reference is provided, the GridManager emits events on room changes:
from maid_engine.core.world import World
from maid_engine.config.settings import Settings
from maid_engine.core.events import GridRoomAddedEvent, GridRoomRemovedEvent
world = World(Settings())
grid = GridManager(world=world)
# Listen for grid events. EventBus.subscribe takes (event_type, handler)
# and returns a handler id — it is not a decorator.
async def on_room_added(event: GridRoomAddedEvent) -> None:
print(f"Room {event.room_id} added at ({event.coord_x}, {event.coord_y})")
async def on_room_removed(event: GridRoomRemovedEvent) -> None:
print(f"Room {event.room_id} removed")
world.events.subscribe(GridRoomAddedEvent, on_room_added)
world.events.subscribe(GridRoomRemovedEvent, on_room_removed)
Builder Commands¶
MAID provides in-game commands for grid management (BUILDER access level):
@grid.create <min_x> <min_y> <max_x> <max_y> [--terrain <type>] [--connect] - Create grid area
@grid.path <from> <to> - Find path between rooms
@grid.map [radius] [--center <x> <y>] - Render ASCII map
Error Handling¶
The grid system provides specific exceptions:
from maid_engine.core.grid import (
GridError, # Base exception
CoordinateOccupiedError, # Room already at coordinate
CoordinateBlockedError, # Coordinate is blocked
RoomNotFoundError, # Room not in grid
)
try:
grid.register_room(room_id, GridCoord(0, 0))
except CoordinateOccupiedError as e:
print(f"Cannot place room: {e.coord} is occupied")
except CoordinateBlockedError as e:
print(f"Cannot place room: {e.coord} is blocked")
try:
result = grid.find_path(unknown_id, room2_id)
except RoomNotFoundError as e:
print(f"Room not found: {e.identifier}")
Performance Considerations¶
- Registration: O(1) for individual rooms, O(n) for bulk with auto-exits
- Coordinate Lookup: O(1) via hash map
- Radius Query: O(n) where n is total rooms (iterates all)
- Rectangle Query: O(n) where n is total rooms
- Pathfinding: O(n log n) typical case with A*
For very large grids (10,000+ rooms), consider:
- Using rectangular queries to limit search space
- Setting max_length on pathfinding to bound search
- Disabling auto_create_exits for bulk operations
Example: Creating a Town¶
from maid_engine.core.grid import GridManager, GridCoord
from uuid import uuid4
grid = GridManager(auto_create_exits=True, allow_diagonals=False)
# Town layout:
# [Inn]---[Square]---[Shop]
# |
# [Gate]
def create_room(name, coord, terrain="stone"):
room_id = uuid4()
grid.register_room(room_id, coord, terrain_type=terrain)
return room_id
inn = create_room("The Rusty Mug Inn", GridCoord(0, 0))
square = create_room("Town Square", GridCoord(1, 0))
shop = create_room("General Store", GridCoord(2, 0))
gate = create_room("Town Gate", GridCoord(1, -1), terrain="road")
# All exits are created automatically!
# inn <-> square <-> shop
# square <-> gate
# Verify connectivity
result = grid.find_path(inn, gate)
print(f"Inn to Gate: {result.directions}") # ['e', 's']