Wilderness System Guide¶
Overview¶
The MAID Wilderness System provides on-demand procedural generation of outdoor areas. As players explore, rooms are generated using deterministic noise-based terrain generation, creating consistent, reproducible wilderness landscapes that can expand infinitely within defined boundaries.
Key features: - On-Demand Generation: Rooms created as players explore - Deterministic Terrain: Same coordinates always produce same terrain - Biome System: Multiple biome types with unique characteristics - Landmark Support: Fixed points of interest that anchor procedural terrain - Resource/Encounter Spawning: Procedural content spawning based on biome - Automatic Cleanup: Stale empty rooms cleaned up to manage memory - Player Tracking: Track players across wilderness rooms
Quick Start¶
from maid_engine.world import WildernessManager, WildernessConfig, Landmark
from maid_engine.core.world import World
from maid_engine.config.settings import Settings
from maid_stdlib.components.core import DescriptionComponent
# Create wilderness manager
world = World(Settings())
config = WildernessConfig(
seed=42, # Deterministic seed
min_x=-1000, max_x=1000,
min_y=-1000, max_y=1000,
cleanup_interval=300.0, # Clean up every 5 minutes
room_max_age=600.0, # Rooms stale after 10 minutes empty
)
# A room_factory is REQUIRED to create real rooms. Without one, the manager
# only allocates a deterministic UUID and creates NO entity and NO room index
# entry — get_or_create_room() returns a GeneratedRoom whose room_id points at
# nothing (useful only for tests). See "Custom Room Factory" below.
def room_factory(x, y, biome_name, biome_def, rng):
# Generate name/description once — each call advances the RNG, so calling
# generate_name(rng) twice would produce two different names.
name = biome_def.generate_name(rng)
description = biome_def.generate_description(rng)
room = world.create_entity()
room.add(DescriptionComponent(name=name, long_desc=description))
world.register_room(room.id, {"name": name, "exits": {}})
return room.id
wilderness = WildernessManager(world=world, config=config, room_factory=room_factory)
# Register a real room to anchor the town landmark, then add the landmark.
town_room = world.create_entity()
world.register_room(town_room.id, {"name": "Starting Town", "exits": {}})
wilderness.add_landmark(Landmark(
coord=(0, 0),
name="Starting Town",
room_id=town_room.id,
radius=5,
biome_override="plains",
))
# Get or create a room at coordinates (uses room_factory to build a real entity)
room = wilderness.get_or_create_room(100, 200)
print(f"Room: {room.room_id}, Biome: {room.biome}")
# Get spawned content for the room
content = wilderness.get_spawned_content(100, 200)
print(f"Resources: {content.resources}")
print(f"Encounters: {content.encounters}")
# Start cleanup loop
await wilderness.start()
Core Classes¶
WildernessConfig¶
Configuration for wilderness generation and management.
from maid_engine.world import WildernessConfig
config = WildernessConfig(
seed=42, # Random seed for deterministic generation
terrain_config=None, # Custom TerrainConfig (optional)
min_x=-5000, max_x=5000, # X boundaries
min_y=-5000, max_y=5000, # Y boundaries
cleanup_interval=300.0, # Seconds between cleanup runs
room_max_age=600.0, # Seconds before stale room cleanup
default_z=0, # Z-level for wilderness rooms
grid_id="wilderness", # Grid identifier
cleanup_batch_size=100, # Max rooms per cleanup cycle
cleanup_yield_interval=10, # Yield interval during cleanup
)
| Field | Type | Default | Description |
|---|---|---|---|
seed |
int |
0 |
Random seed for deterministic terrain |
terrain_config |
TerrainConfig \| None |
None |
Custom terrain configuration |
min_x, max_x |
int |
-1000, 1000 |
X coordinate boundaries |
min_y, max_y |
int |
-1000, 1000 |
Y coordinate boundaries |
cleanup_interval |
float |
300.0 |
Seconds between cleanup runs (minimum effective: 10.0) |
room_max_age |
float |
600.0 |
Seconds before stale room cleanup |
default_z |
int |
0 |
Z-level for all wilderness rooms |
grid_id |
str |
"wilderness" |
Grid identifier for wilderness coordinates |
cleanup_batch_size |
int |
100 |
Max rooms to clean per cycle (0 = unlimited) |
cleanup_yield_interval |
int |
10 |
Yield to event loop every N rooms during cleanup |
WildernessManager¶
The main interface for wilderness generation and management.
from maid_engine.world import WildernessManager, WildernessConfig
from maid_engine.core.world import World
from maid_engine.config.settings import Settings
# Create with world reference (World requires a Settings instance)
world = World(Settings())
wilderness = WildernessManager(
world=world,
config=WildernessConfig(seed=42),
)
# Or standalone for testing (world is optional; a standalone grid is created)
wilderness = WildernessManager(config=WildernessConfig(seed=42))
GeneratedRoom¶
Metadata for a procedurally generated room.
from maid_engine.world import GeneratedRoom
from uuid import UUID
import time
room = GeneratedRoom(
room_id=UUID("..."),
coord=(100, 200),
biome="forest",
created_at=time.time(),
last_visited=time.time(),
player_count=0,
)
# Check if room is stale
if room.is_stale(max_age=600.0):
print("Room can be cleaned up")
Landmark¶
Fixed points of interest in the wilderness. A landmark's coord, radius, and
biome_override shape procedural generation within its radius of influence.
Note:
room_idis stored on the landmark (and included in exported grid state) as a reference for your own content, but the generator does not read it. When a wilderness coordinate is first visited, a new room entity is always created for it — a landmark does not swap in a pre-existing room. Onlybiome_override(pluscoord/radius) changes what gets generated.
from maid_engine.world import Landmark
from uuid import uuid4
# A landmark that forces the "plains" biome within 5 tiles of the origin.
town = Landmark(
coord=(0, 0),
name="Starting Town",
room_id=town_room_id, # reference id only; not used to reuse a room
radius=5, # Influence radius
biome_override="plains", # Force biome in radius
)
# room_id is optional and defaults to None.
dungeon = Landmark(
coord=(500, 300),
name="Dragon's Lair",
radius=3,
description="A dark cave entrance looms in the mountainside.",
biome_override="mountains",
metadata={"dungeon_id": "dragons_lair", "level_range": (15, 20)},
)
Terrain Generation¶
The wilderness uses noise-based terrain generation to create biomes.
TerrainConfig¶
from maid_engine.world import TerrainConfig
config = TerrainConfig(
seed=42,
scale=100.0, # Larger = wider terrain features
octaves=4, # Detail layers
persistence=0.5, # Amplitude decrease per octave
lacunarity=2.0, # Frequency increase per octave
water_level=0.3, # Below this = water
mountain_level=0.7, # Above this = mountains
moisture_scale=80.0, # Moisture noise scale
moisture_octaves=3, # Moisture detail
temperature_scale=150.0, # Temperature noise scale
base_temperature=0.5, # 0=arctic, 1=tropical
)
TerrainGenerator¶
from maid_engine.world import TerrainGenerator, TerrainConfig
generator = TerrainGenerator(TerrainConfig(seed=42))
# Get terrain type at coordinate
terrain = generator.get_terrain(100, 200) # "forest"
# Get full terrain data
data = generator.get_terrain_data(100, 200)
print(f"Type: {data.terrain_type}")
print(f"Elevation: {data.elevation}") # 0-1
print(f"Moisture: {data.moisture}") # 0-1
print(f"Temperature: {data.temperature}") # 0-1
print(f"Movement Cost: {data.movement_cost}")
# Individual values
elevation = generator.get_elevation(100, 200)
moisture = generator.get_moisture(100, 200)
temperature = generator.get_temperature(100, 200)
Biome Types¶
The terrain generator produces these biome types:
| Biome | Movement Cost | Conditions |
|---|---|---|
water |
3.0 | Low elevation (< 0.3) |
plains |
1.0 | Default for moderate elevation |
forest |
1.0 | Moderate moisture (> 0.5) |
desert |
1.5 | Hot (> 0.7) and dry (< 0.3) |
tundra |
1.5 | Cold (< 0.25) |
mountains |
2.0 | High elevation (> 0.7) |
swamp |
2.0 | High moisture near water level |
hills |
1.3 | Elevated terrain with moderate moisture |
Biome Definitions¶
Biomes define room characteristics, resources, and encounters.
BiomeDefinition¶
from maid_stdlib.world import BiomeDefinition, ResourceSpawn, EncounterSpawn
from maid_stdlib.world import register_biome
custom_biome = BiomeDefinition(
name="enchanted_forest",
display_name="Enchanted Forest",
movement_cost=1.5,
sector_type="outdoor",
description_templates=[
"Ethereal light filters through ancient trees.",
"Magical energies crackle in the air.",
],
name_templates=["Mystical Grove", "Fey Crossing", "Ancient Woods"],
resources=[
ResourceSpawn("fey_dust", spawn_chance=0.2, min_quantity=1, max_quantity=3),
ResourceSpawn("moonflower", spawn_chance=0.1),
],
encounters=[
EncounterSpawn("sprite", spawn_chance=0.15, min_level=3, max_level=8),
EncounterSpawn("treant", spawn_chance=0.05, min_level=10, max_level=15),
],
ambient_sounds=["Bells chime softly in the distance."],
ambient_messages=["A butterfly made of light dances past."],
)
register_biome(custom_biome)
ResourceSpawn¶
from maid_stdlib.world import ResourceSpawn
from random import Random
resource = ResourceSpawn(
resource_type="iron_ore",
spawn_chance=0.15, # 15% chance to spawn
min_quantity=1,
max_quantity=3,
)
# Roll for quantity when spawned
rng = Random(42)
quantity = resource.roll_quantity(rng)
EncounterSpawn¶
from maid_stdlib.world import EncounterSpawn
from random import Random
encounter = EncounterSpawn(
encounter_type="wolf",
spawn_chance=0.20,
min_level=3,
max_level=7,
group_size_min=2,
group_size_max=5,
)
rng = Random(42)
level = encounter.roll_level(rng)
group_size = encounter.roll_group_size(rng)
Default Biomes¶
Access default biome definitions:
from maid_stdlib.world import (
get_biome,
get_all_biome_names,
DEFAULT_BIOMES,
)
# Get specific biome
forest = get_biome("forest")
print(f"Movement cost: {forest.movement_cost}")
# List all biomes
names = get_all_biome_names() # ['desert', 'forest', 'hills', ...]
# Access default definitions
plains = DEFAULT_BIOMES["plains"]
Room Generation¶
On-Demand Generation¶
# Get existing room or generate new one
room = wilderness.get_or_create_room(100, 200)
# Check if room exists
if wilderness.is_generated(100, 200):
room = wilderness.get_generated_room(100, 200)
# Get biome without generating room
biome = wilderness.get_biome(100, 200) # "forest"
# Preview terrain without generating
info = wilderness.preview_terrain(100, 200)
print(f"Biome: {info['biome']}")
print(f"Elevation: {info['elevation']:.2f}")
print(f"Movement Cost: {info['movement_cost']}")
print(f"In Bounds: {info['in_bounds']}")
Spawned Content¶
# Get spawned content for a room
content = wilderness.get_spawned_content(100, 200)
if content:
print(f"Room Name: {content.room_name}")
print(f"Description: {content.room_description}")
print(f"Resources: {content.resources}") # ['wood', 'mushroom']
print(f"Encounters: {content.encounters}") # ['deer']
Custom Room Factory¶
from maid_stdlib.components.core import DescriptionComponent
def my_room_factory(x, y, biome_name, biome_def, rng):
"""Create a room entity in the world and return its UUID.
The factory signature is (x, y, biome_name, biome_def, rng) -> UUID.
"""
# Generate name/description once — each call advances the RNG, so calling
# generate_name(rng) twice would produce two different names.
name = biome_def.generate_name(rng)
description = biome_def.generate_description(rng)
room = world.create_entity()
room.add(DescriptionComponent(name=name, long_desc=description))
# Register the entity in the world's room index so players can enter it.
world.register_room(room.id, {"name": name, "exits": {}})
return room.id
wilderness = WildernessManager(
world=world,
config=config,
room_factory=my_room_factory,
)
Note: There is no
RoomComponent. Room name/description live onDescriptionComponent(name,short_desc,long_desc). Do not add aPositionComponentto a room entity —PositionComponent.room_idis a required field describing which room an entity is in, not a room's own coordinates. Wilderness placement is handled by the manager's grid, not by aPositionComponenton the room.
Landmark Management¶
Adding Landmarks¶
from maid_engine.world import Landmark
# Add a landmark
wilderness.add_landmark(Landmark(
coord=(0, 0),
name="Starting Town",
room_id=town_id,
radius=5,
))
# Add landmark with biome override
wilderness.add_landmark(Landmark(
coord=(100, 100),
name="Oasis",
radius=10,
biome_override="plains", # Force plains in desert area
))
Querying Landmarks¶
# Get landmark at exact coordinate
landmark = wilderness.get_landmark(0, 0)
# Get landmark influencing coordinate (checks radius)
landmark = wilderness.get_landmark_at(3, 3) # Within Starting Town radius
# Get all landmarks
landmarks = wilderness.get_all_landmarks()
# Find landmarks in range
nearby = wilderness.get_landmarks_in_range(50, 50, radius=100)
nearby = wilderness.landmarks_in_radius(50, 50, radius=100.0)
# Remove landmark
removed = wilderness.remove_landmark(0, 0)
Player Tracking¶
Tracking Player Movement¶
# Track player entering room
wilderness.player_entered(room_id, player_id)
# Track player leaving room
wilderness.player_left(room_id, player_id)
# Alternative: update by coordinates
wilderness.increment_player_count(100, 200)
wilderness.decrement_player_count(100, 200)
# Update last visited timestamp
wilderness.update_last_visited(100, 200)
Querying Player Locations¶
# Get players in a room
players = wilderness.get_players_in_room(room_id)
# Get room where player is located
room_id = wilderness.get_player_room(player_id)
# Get all occupied rooms
occupied = wilderness.get_occupied_rooms()
for room in occupied:
print(f"{room.coord}: {room.player_count} players")
Cleanup System¶
Automatic Cleanup¶
# Start the cleanup loop
await wilderness.start()
# ... game runs ...
# Stop the cleanup loop
await wilderness.stop()
# Check if running
if wilderness.is_running:
print("Cleanup loop active")
Manual Cleanup¶
# Get stale rooms
stale = wilderness.get_stale_rooms()
# Manual cleanup
removed = wilderness.cleanup_stale_rooms_sync()
print(f"Removed {removed} rooms")
# Clear all generated rooms
wilderness.clear()
Cleanup Configuration¶
| Config | Default | Description |
|---|---|---|
cleanup_interval |
300.0 | Seconds between automatic cleanup |
room_max_age |
600.0 | Seconds before empty room is stale |
Set cleanup_interval=0 to disable automatic cleanup.
Statistics and Debugging¶
stats = wilderness.get_stats()
print(f"Generated Rooms: {stats['total_generated']}")
print(f"Landmarks: {stats['total_landmarks']}")
print(f"Stale Rooms: {stats['stale_count']}")
print(f"Players in Wilderness: {stats['player_count']}")
print(f"Biome Distribution: {stats['biome_counts']}")
print(f"Bounds: {stats['bounds']}")
Boundary Handling¶
# Check if coordinate is in bounds
if wilderness.is_in_bounds(500, 500):
room = wilderness.get_or_create_room(500, 500)
else:
print("Outside wilderness boundaries")
# Get boundary message for movement
msg = wilderness.get_edge_description(1000, 500, "east")
if msg:
print(msg) # "You cannot travel further east..."
Serialization¶
Export State¶
import json
state = wilderness.export_state()
with open("wilderness.json", "w") as f:
json.dump(state, f)
Import State¶
Important —
import_statedoes not recreate room entities. It restores the generated-room metadata, landmarks, spawned content, and the grid's coordinate→room-id index, but it does not recreate the ECS room entities or the world room-index entries (the implementation notes: "Room entities must be recreated separately."). Immediately after importing,world.get_entity(room_id)andworld.get_room(room_id)returnNonefor every imported coordinate, so those rooms are blank and cannot be looked at even though the grid can path to them.Recreate the entities yourself, reusing the exported room ids (via
world.create_entity(entity_id=...)) so they match the grid's index, and pull the name/description back out of the exportedspawned_content:
from uuid import UUID
from maid_stdlib.components.core import DescriptionComponent
wilderness.import_state(state)
# Rebuild the ECS room entities + world room index from the export. Reuse the
# exported room_id so entities line up with the grid's coordinate index.
content_by_coord = {tuple(c["coord"]): c for c in state.get("spawned_content", [])}
for room_data in state.get("generated_rooms", []):
room_id = UUID(room_data["room_id"])
coord = tuple(room_data["coord"])
content = content_by_coord.get(coord)
name = content["room_name"] if content else room_data["biome"].title()
description = content["room_description"] if content else ""
entity = world.create_entity(entity_id=room_id)
entity.add(DescriptionComponent(name=name, long_desc=description))
world.register_room(room_id, {"name": name, "exits": {}})
The export includes: - Configuration settings - All generated rooms - All landmarks - Spawned content - Last cleanup timestamp
Builder Commands¶
MAID provides in-game commands for wilderness management (BUILDER access level):
@wilderness.stats - Show wilderness statistics
@wilderness.preview [<x> <y>] - Preview terrain at coordinate
@wilderness.landmark add <x> <y> <name> [--radius <n>] [--biome <type>] - Add landmark
@wilderness.landmark remove <x> <y> - Remove landmark
@wilderness.landmark list [--near <radius>] - List all landmarks
Integration with Content Packs¶
Content packs can extend the wilderness system:
from maid_engine.plugins.manifest import ContentPackManifest
from maid_engine.plugins.protocol import BaseContentPack
from maid_engine.plugins import WildernessContentMixin
from maid_engine.world import Landmark, WildernessConfig
from maid_stdlib.world import BiomeDefinition, register_biome
class MyContentPack(BaseContentPack, WildernessContentMixin):
@property
def manifest(self) -> ContentPackManifest:
return ContentPackManifest(name="my-wilderness", version="1.0.0")
async def on_load(self, engine) -> None:
await super().on_load(engine)
# Register custom biomes
register_biome(BiomeDefinition(
name="haunted_forest",
display_name="Haunted Forest",
movement_cost=2.0,
description_templates=["Twisted trees claw at a grey, silent sky."],
name_templates=["Haunted Forest"],
))
# The mixin creates a WildernessManager for engine.world (World has no
# built-in `wilderness` attribute) and starts its cleanup loop.
wilderness = await self.setup_wilderness(engine, WildernessConfig(seed=42))
wilderness.add_landmark(Landmark(
coord=(0, 0),
name="Starting Area",
radius=10,
))
async def on_unload(self, engine) -> None:
await self.teardown_wilderness(engine)
await super().on_unload(engine)
Example: Creating a Wilderness Zone¶
from maid_engine.world import (
WildernessManager,
WildernessConfig,
TerrainConfig,
Landmark,
)
from maid_stdlib.components.core import DescriptionComponent
# Configure terrain for a temperate climate
terrain_config = TerrainConfig(
seed=12345,
scale=150.0, # Larger features
water_level=0.25, # Less water
mountain_level=0.75, # Higher mountains
base_temperature=0.6, # Slightly warm
)
# Configure wilderness
config = WildernessConfig(
seed=12345,
terrain_config=terrain_config,
min_x=-500, max_x=500,
min_y=-500, max_y=500,
cleanup_interval=120.0, # Aggressive cleanup
room_max_age=300.0,
)
# A room_factory is required to build real, enterable rooms (see "Custom Room
# Factory"). Without one the manager only allocates deterministic UUIDs.
def room_factory(x, y, biome_name, biome_def, rng):
name = biome_def.generate_name(rng)
description = biome_def.generate_description(rng)
room = world.create_entity()
room.add(DescriptionComponent(name=name, long_desc=description))
world.register_room(room.id, {"name": name, "exits": {}})
return room.id
wilderness = WildernessManager(
world=world, config=config, room_factory=room_factory
)
# Add key locations
wilderness.add_landmark(Landmark(
coord=(0, 0),
name="Central Town",
room_id=town_id,
radius=8,
biome_override="plains",
))
wilderness.add_landmark(Landmark(
coord=(200, 150),
name="Ancient Ruins",
radius=5,
description="Crumbling stone pillars mark the entrance to forgotten ruins.",
metadata={"dungeon_id": "ancient_ruins"},
))
wilderness.add_landmark(Landmark(
coord=(-100, 300),
name="Hermit's Hut",
radius=2,
description="A small wooden hut sits alone in the wilderness.",
))
# Start the wilderness system
await wilderness.start()