Skip to content

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

  1. Player entity is copied from source world
  2. All components are deep-copied
  3. Inventory items are transferred with the player
  4. Player is removed from source world
  5. Player is created in target world with same ID
  6. Components and inventory are restored
  7. 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

# Shutdown all worlds
await manager.shutdown_all()

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