Multi-World Support¶
MAID supports running multiple game worlds simultaneously, connected via portals. This enables instanced dungeons, parallel dimensions, or separate game areas.
Overview¶
The WorldManager coordinates multiple world instances:
from maid_engine.core.multiworld import WorldManager
manager = WorldManager()
# Create multiple worlds
main_world = manager.create_world("main", settings)
dungeon_world = manager.create_world("dungeon-1", settings)
# Connect with a portal
manager.create_portal(
source_world="main",
source_room=portal_room_id,
target_world="dungeon-1",
target_room=dungeon_entrance_id,
)
# Transition a player
await manager.transition_player(
player_id,
from_world="main",
to_world="dungeon-1",
to_room=dungeon_entrance_id,
)
Creating Worlds¶
Basic World Creation¶
from maid_engine.config import get_settings
manager = WorldManager()
settings = get_settings()
# Create a world with default settings
world = manager.create_world("main", settings)
# Create with custom properties
dungeon = manager.create_world(
"dungeon-dragon-lair",
settings,
display_name="Dragon's Lair",
description="A dangerous dungeon filled with treasure and dragons.",
max_players=10, # Limit concurrent players
is_public=False, # Hide from world list
)
World Instance Properties¶
Each world is wrapped in a WorldInstance:
@dataclass
class WorldInstance:
id: str # Unique identifier
world: World # The actual World object
display_name: str # Human-readable name
description: str # World description
max_players: int # 0 = unlimited
is_public: bool # Listed in world browser
metadata: dict[str, Any] # Custom data
Accessing Worlds¶
# Get a world by ID
world = manager.get_world("main")
# Get the full instance (with metadata)
instance = manager.get_instance("dungeon-1")
# Get default world
main = manager.default_world
# List all public worlds
public_worlds = manager.list_worlds(include_private=False)
# List all worlds
all_worlds = manager.list_worlds(include_private=True)
Portals¶
Creating Portals¶
Portals connect rooms in different worlds:
portal = manager.create_portal(
source_world="main",
source_room=town_portal_id,
target_world="dungeon-1",
target_room=dungeon_entrance_id,
bidirectional=True, # Can travel both ways
name="Dungeon Portal",
description="A swirling vortex of darkness.",
)
Portal Properties¶
@dataclass
class PortalConnection:
id: UUID
source_world_id: str
source_room_id: UUID
target_world_id: str
target_room_id: UUID
bidirectional: bool # Two-way travel
name: str
description: str
One-Way Portals¶
# One-way portal (no return)
exit_portal = manager.create_portal(
source_world="dungeon-1",
source_room=boss_room_id,
target_world="main",
target_room=graveyard_id,
bidirectional=False, # Can only go forward
name="Escape Rift",
description="A tear in reality leading back to the mortal world.",
)
Finding Portals¶
# Get portals in a room
portals = manager.get_portals_in_room("main", portal_room_id)
for portal in portals:
print(f"Portal to {portal.target_world_id}: {portal.name}")
Removing Portals¶
# Remove a specific portal
manager.remove_portal(portal.id)
# Removing a world removes its portals automatically
manager.remove_world("dungeon-1")
Player Transitions¶
Basic Transition¶
success = await manager.transition_player(
player_id=player.id,
from_world="main",
to_world="dungeon-1",
to_room=entrance_room_id,
)
if success:
print("Player transitioned successfully")
else:
print("Transition failed")
What Happens During Transition¶
- Player entity is copied from source world
- All components are deep-copied
- Inventory items are transferred with the player
- Player is removed from source world
- Player is created in target world with same ID
- Components and inventory are restored
- Player is placed in target room
Handling Transitions in Commands¶
async def enter_portal_handler(ctx: CommandContext) -> CommandResult:
"""Enter a portal."""
player = ctx.get_player()
room = ctx.get_room()
# Find portal in current room
portals = world_manager.get_portals_in_room(
ctx.world.world_id,
room.id,
)
if not portals:
return CommandResult(False, "There is no portal here.")
portal = portals[0] # Use first portal
# Transition player
success = await world_manager.transition_player(
player_id=player.id,
from_world=ctx.world.world_id,
to_world=portal.target_world_id,
to_room=portal.target_room_id,
)
if success:
# Get new world and room description
new_world = world_manager.get_world(portal.target_world_id)
new_room = new_world.get_room(portal.target_room_id)
return CommandResult(True, format_room(new_room))
else:
return CommandResult(False, "The portal flickers but nothing happens.")
World Lifecycle¶
Starting All Worlds¶
# Start up all worlds
await manager.startup_all()
# Or start individually
for instance in manager.list_worlds(include_private=True):
await instance.world.startup()
Ticking All Worlds¶
# Tick all worlds
await manager.tick_all(delta)
# Or manually iterate
for instance in manager.list_worlds(include_private=True):
await instance.world.tick(delta)
Shutting Down¶
Use Cases¶
Instanced Dungeons¶
Create private instances per party:
async def create_dungeon_instance(party_leader_id: UUID) -> str:
"""Create a private dungeon instance for a party."""
instance_id = f"dungeon-{party_leader_id}"
# Create the instance
world = manager.create_world(
instance_id,
settings,
display_name="Private Dungeon",
max_players=5,
is_public=False,
)
# Set up dungeon content
await populate_dungeon(world)
# Create entry portal
main = manager.get_world("main")
manager.create_portal(
source_world="main",
source_room=dungeon_entrance_room,
target_world=instance_id,
target_room=dungeon_start_room,
bidirectional=True,
)
return instance_id
async def cleanup_dungeon_instance(instance_id: str) -> None:
"""Clean up a dungeon instance when party leaves."""
# Check if empty
world = manager.get_world(instance_id)
if world and not get_players_in_world(world):
manager.remove_world(instance_id)
Player Housing¶
Each player gets their own world:
async def create_player_house(player_id: UUID) -> str:
"""Create a personal house world for a player."""
house_id = f"house-{player_id}"
world = manager.create_world(
house_id,
settings,
display_name=f"Player's House",
max_players=20, # Allow guests
is_public=False,
metadata={"owner": str(player_id)},
)
# Create house rooms
await setup_house_rooms(world)
# Create door from main world
player = main_world.get_entity(player_id)
house_location = player.get(HouseLocationComponent).room_id
manager.create_portal(
source_world="main",
source_room=house_location,
target_world=house_id,
target_room=house_entrance_room,
bidirectional=True,
name="House Door",
)
return house_id
PvP Arenas¶
Isolated combat zones:
async def start_arena_match(player_a: UUID, player_b: UUID) -> str:
"""Create an arena instance for a PvP match."""
arena_id = f"arena-{uuid4()}"
world = manager.create_world(
arena_id,
settings,
display_name="PvP Arena",
max_players=2,
is_public=False,
metadata={
"type": "pvp",
"participants": [str(player_a), str(player_b)],
},
)
await setup_arena(world)
# Teleport both players
await manager.transition_player(player_a, "main", arena_id, spawn_a)
await manager.transition_player(player_b, "main", arena_id, spawn_b)
return arena_id
Events¶
Subscribe to world transition events:
@events.subscribe(WorldTransitionEvent)
async def on_world_transition(event: WorldTransitionEvent) -> None:
"""Handle player moving between worlds."""
print(f"Player {event.player_id} transitioned:")
print(f" From: {event.from_world_id}")
print(f" To: {event.to_world_id}")
# Maybe notify other players
await broadcast_departure(event.from_world_id, event.player_id)
await broadcast_arrival(event.to_world_id, event.player_id)
Best Practices¶
1. Clean Up Unused Instances¶
async def cleanup_empty_instances(manager: WorldManager) -> int:
"""Remove instances with no players."""
removed = 0
for instance in manager.list_worlds(include_private=True):
# Skip main world
if instance.id == "main":
continue
# Check if empty and not recently created
player_count = count_players(instance.world)
if player_count == 0:
manager.remove_world(instance.id)
removed += 1
return removed
2. Validate Transitions¶
async def safe_transition(
manager: WorldManager,
player_id: UUID,
target_world_id: str,
target_room_id: UUID,
) -> tuple[bool, str]:
"""Transition with validation."""
# Check target world exists
target = manager.get_world(target_world_id)
if not target:
return False, "Target world does not exist."
# Check player limit
instance = manager.get_instance(target_world_id)
if instance.max_players > 0:
current = count_players(target)
if current >= instance.max_players:
return False, "Target world is full."
# Check target room exists
if not target.get_room(target_room_id):
return False, "Target room does not exist."
# Find current world
from_world_id = find_player_world(manager, player_id)
if not from_world_id:
return False, "Player is not in any world."
# Perform transition
success = await manager.transition_player(
player_id,
from_world_id,
target_world_id,
target_room_id,
)
return success, "Transitioned successfully." if success else "Transition failed."
3. Preserve Player State¶
Components are automatically copied, but ensure external state is handled:
async def on_transition(event: WorldTransitionEvent) -> None:
"""Handle external state during transition."""
# Clear combat state
combat_manager.remove_from_combat(event.player_id)
# Save any pending data
await save_player_data(event.player_id)
# Update session world reference
session = get_session(event.player_id)
if session:
session.current_world = event.to_world_id
4. Use Metadata¶
# Store instance-specific data
world = manager.create_world(
"event-dungeon",
settings,
metadata={
"event_type": "winter_festival",
"difficulty": "hard",
"rewards_claimed": [],
"created_at": datetime.now().isoformat(),
},
)
# Access later
instance = manager.get_instance("event-dungeon")
if instance.metadata.get("event_type") == "winter_festival":
apply_winter_theme(instance.world)
Next Steps¶
- AI Integration - Integrate AI providers
- Performance - Optimize multi-world performance