Skip to content

Complete Game Tutorial

This tutorial shows how to combine multiple content packs to create a complete MUD game. You will learn how different systems integrate, how to make architecture decisions, and best practices for building a full game.

Overview

A complete MAID game typically consists of:

  1. Core Systems: Combat, magic, movement
  2. World Content: Rooms, NPCs, items
  3. Progression: Levels, skills, quests
  4. Economy: Currency, shops, trading
  5. Social: Chat, groups, guilds

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                     Your Game                                │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐          │
│  │   Combat    │  │    Magic    │  │   Economy   │          │
│  │   System    │  │   System    │  │   System    │          │
│  └──────┬──────┘  └──────┬──────┘  └──────┬──────┘          │
│         │                │                │                  │
│         └────────────────┼────────────────┘                  │
│                          │                                   │
│                  ┌───────▼────────┐                          │
│                  │ maid-classic-rpg│                          │
│                  └───────┬────────┘                          │
├──────────────────────────┼──────────────────────────────────┤
│                  ┌───────▼────────┐                          │
│                  │   maid-stdlib   │                          │
│                  └───────┬────────┘                          │
├──────────────────────────┼──────────────────────────────────┤
│                  ┌───────▼────────┐                          │
│                  │   maid-engine   │                          │
│                  └────────────────┘                          │
└─────────────────────────────────────────────────────────────┘

System Integration

How Systems Communicate

Systems communicate through the EventBus, not direct calls:

# Combat system emits damage event
await self.events.emit(DamageDealtEvent(
    source_id=attacker.id,
    target_id=target.id,
    damage=25,
))

# XP system listens for kills
async def _on_entity_death(self, event: EntityDeathEvent) -> None:
    if event.killer_id:
        await self._award_xp(event.killer_id, event.entity_id)

# Loot system also listens for kills
async def _on_entity_death(self, event: EntityDeathEvent) -> None:
    await self._spawn_loot(event.entity_id)

# Quest system tracks kills too
async def _on_entity_death(self, event: EntityDeathEvent) -> None:
    await self._update_kill_quests(event.killer_id, event.entity_id)

Shared Components

Systems share components to avoid duplication:

# Many systems use HealthComponent
player.add(HealthComponent(current=100, maximum=100))

# Combat reads it
health = player.get(HealthComponent)
health.damage(25)

# Magic reads it
health.heal(30)

# Regeneration system updates it
if health.current < health.maximum:
    health.heal(int(health.regeneration_rate * delta))

Example: Complete Game Structure

Project Layout

my-mud-game/
    src/
        my_mud_game/
            __init__.py
            main.py                    # Entry point
            config.py                  # Game configuration
            packs/
                __init__.py
                world/                 # World content pack
                    pack.py
                    rooms/
                    npcs/
                    items/
                gameplay/              # Gameplay systems pack
                    pack.py
                    systems/
                    commands/
                progression/           # Progression pack
                    pack.py
                    xp_system.py
                    skills.py
    data/
        world.yaml                    # World definition
        npcs.yaml                     # NPC definitions
        items.yaml                    # Item definitions
    tests/
    pyproject.toml

Main Entry Point

# src/my_mud_game/main.py
"""Main entry point for the MUD game."""

import asyncio

from maid_engine.core.engine import GameEngine
from maid_engine.config.settings import Settings
from maid_stdlib.pack import StdlibContentPack
from maid_classic_rpg.pack import ClassicRPGContentPack

from .packs.world import WorldContentPack
from .packs.gameplay import GameplayContentPack
from .packs.progression import ProgressionContentPack


async def main():
    """Start the game server."""
    # Load configuration
    settings = Settings(
        game=GameSettings(
            name="My MUD Game",
            tick_rate=4.0,
        ),
        telnet=TelnetSettings(
            port=4000,
        ),
        web=WebSettings(
            port=8080,
        ),
    )

    # Create engine
    engine = GameEngine(settings)

    # Load content packs in dependency order
    # Core library
    engine.load_content_pack(StdlibContentPack())

    # Classic RPG features (combat, magic, etc.)
    engine.load_content_pack(ClassicRPGContentPack())

    # Game-specific packs
    engine.load_content_pack(WorldContentPack())
    engine.load_content_pack(GameplayContentPack())
    engine.load_content_pack(ProgressionContentPack())

    # Start the server
    print("Starting My MUD Game...")
    await engine.start()

    # Keep running until shutdown
    try:
        while True:
            await asyncio.sleep(1)
    except KeyboardInterrupt:
        print("Shutting down...")
        await engine.stop()


if __name__ == "__main__":
    asyncio.run(main())

World Content Pack

# src/my_mud_game/packs/world/pack.py
"""World content pack - defines rooms, NPCs, items."""

from maid_engine.plugins.protocol import BaseContentPack


class WorldContentPack(BaseContentPack):
    """Content pack containing world definitions."""

    @property
    def manifest(self):
        from maid_engine.plugins.manifest import ContentPackManifest
        return ContentPackManifest(
            name="my-game-world",
            version="1.0.0",
            display_name="My Game World",
            dependencies={"stdlib": ">=0.1.0", "classic-rpg": ">=0.1.0"},
        )

    def get_dependencies(self):
        return ["stdlib", "classic-rpg"]

    async def on_load(self, engine):
        """Load world data and spawn initial entities."""
        await self._load_rooms(engine)
        await self._load_npcs(engine)
        await self._load_items(engine)

    async def _load_rooms(self, engine):
        """Create rooms from data files."""
        # Load from YAML/JSON or create programmatically
        world = engine.world

        # Create starting room
        start_room = world.rooms.create(
            name="Town Square",
            description="The bustling center of town.",
        )

        # Create connected rooms
        tavern = world.rooms.create(
            name="The Rusty Dagger Tavern",
            description="A cozy tavern with a roaring fireplace.",
        )

        # Connect rooms
        start_room.add_exit("north", tavern.id)
        tavern.add_exit("south", start_room.id)

    async def _load_npcs(self, engine):
        """Spawn NPCs in rooms."""
        # Create NPCs with AI and dialogue
        pass

    async def _load_items(self, engine):
        """Place items in the world."""
        pass

Gameplay Content Pack

# src/my_mud_game/packs/gameplay/pack.py
"""Custom gameplay systems."""

from maid_engine.plugins.protocol import BaseContentPack

from .systems import RestingSystem, HungerSystem


class GameplayContentPack(BaseContentPack):
    """Custom gameplay mechanics."""

    @property
    def manifest(self):
        from maid_engine.plugins.manifest import ContentPackManifest
        return ContentPackManifest(
            name="my-game-gameplay",
            version="1.0.0",
            dependencies={"stdlib": ">=0.1.0"},
        )

    def get_systems(self, world):
        return [
            RestingSystem(world),
            HungerSystem(world),
        ]

    def register_commands(self, registry):
        from .commands import register_commands
        register_commands(registry, self.manifest.name)

Best Practices

1. Use Events for Cross-System Communication

# Good: Events for loose coupling
await self.events.emit(PlayerLeveledUpEvent(player_id=player.id, new_level=10))

# Bad: Direct system calls
skill_system = self.world.systems.get(SkillSystem)
skill_system.unlock_skills_for_level(player_id, 10)

2. Keep Components Focused

# Good: Focused components
class HealthComponent(Component):
    current: int
    maximum: int

class ManaComponent(Component):
    current: int
    maximum: int

# Bad: Mega-component
class CharacterStatsComponent(Component):
    health: int
    max_health: int
    mana: int
    max_mana: int
    stamina: int
    # ... 50 more fields

3. Design for Extensibility

# Good: Can be extended by other packs
class CombatSystem(System):
    async def calculate_damage(self, attacker, defender, attack_type):
        base_damage = self._get_base_damage(attacker)

        # Emit event so other systems can modify
        event = DamageCalculationEvent(
            attacker_id=attacker.id,
            defender_id=defender.id,
            base_damage=base_damage,
            modifiers=[],
        )
        await self.events.emit(event)

        # Apply modifiers from other systems
        final_damage = base_damage
        for modifier in event.modifiers:
            final_damage = int(final_damage * modifier)

        return final_damage

4. Test Systems in Isolation

# Test combat system without magic system
@pytest.fixture
def combat_only_world(world):
    world.systems.register(CombatSystem(world))
    return world

async def test_basic_attack(combat_only_world):
    # Test combat in isolation
    pass

5. Document System Interactions

class QuestSystem(System):
    """Quest tracking and completion system.

    Dependencies:
    - StdlibContentPack: DescriptionComponent for quest givers
    - CombatSystem: Listens to EntityDeathEvent for kill quests
    - LootSystem: Listens to ItemPickedUpEvent for collection quests

    Events Emitted:
    - QuestAcceptedEvent: When player accepts a quest
    - QuestCompletedEvent: When quest objectives are met
    - QuestRewardEvent: When rewards are granted

    Events Listened:
    - EntityDeathEvent: Track kills for kill quests
    - ItemPickedUpEvent: Track items for collection quests
    - RoomEnterEvent: Track exploration for discovery quests
    """

Common Patterns

Loading World Data

# Load from YAML
import yaml

async def _load_rooms(self, engine):
    with open("data/rooms.yaml") as f:
        data = yaml.safe_load(f)

    for room_data in data["rooms"]:
        room = engine.world.rooms.create(
            id=room_data["id"],
            name=room_data["name"],
            description=room_data["description"],
        )
        for exit_dir, target in room_data.get("exits", {}).items():
            room.add_exit(exit_dir, target)

Spawning NPCs

async def spawn_npc(self, world, template_id: str, room_id: UUID):
    """Spawn an NPC from a template."""
    template = self.npc_templates[template_id]

    entity = world.entities.create()
    entity.add(PositionComponent(room_id=room_id))
    entity.add(DescriptionComponent(
        name=template["name"],
        short_desc=template["description"],
    ))
    entity.add(HealthComponent(
        current=template["health"],
        maximum=template["health"],
    ))
    entity.add(NPCComponent(
        template_id=template_id,
        behavior_type=template.get("behavior", "passive"),
    ))

    if template.get("hostile"):
        entity.add(AttackComponent(**template["attack"]))
        entity.add_tag("hostile")

    return entity

Saving Game State

async def save_player(self, player_id: UUID):
    """Save player state to document store."""
    player = self.world.entities.get(player_id)
    if not player:
        return

    # Serialize entity
    data = player.to_dict()

    # Save to document store
    players = self.document_store.get_collection("players")
    await players.upsert(player_id, data)

Summary

Building a complete game requires:

  1. Layered content packs that build on each other
  2. Event-driven communication between systems
  3. Shared components for common data
  4. Clear documentation of system interactions
  5. Comprehensive testing at all levels

Next Steps

Additional Resources