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.
This is an architectural overview, not a copy-paste program. The larger snippets illustrate structure and patterns; where they show engine APIs, this page uses the real ones (
world.create_entity(),world.register_room(),world.place_entity_in_room(),world.entities.get(),engine.stop()). A fewpass-bodied methods are intentional stubs for you to fill in.Constructing
Settings()requires a secure admin key. In development setMAID_DEBUG=true(which relaxes the check) or provide a 32+ characterMAID_ADMIN_SECRET_KEY; otherwiseSettings()raises a validation error.
Overview¶
A complete MAID game typically consists of:
- Core Systems: Combat, magic, movement
- World Content: Rooms, NPCs, items
- Progression: Levels, skills, quests
- Economy: Currency, shops, trading
- 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 (damage_type is required)
await self.events.emit(DamageDealtEvent(
source_id=attacker.id,
target_id=target.id,
damage=25,
damage_type="physical",
))
# 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 (
GameSettings,
Settings,
TelnetSettings,
WebSettings,
)
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."""
# Settings() validates the admin secret key. For local development, run with
# MAID_DEBUG=true in the environment (or set a 32+ char MAID_ADMIN_SECRET_KEY)
# so this construction does not raise a validation error.
settings = Settings(
name="My MUD Game",
game=GameSettings(
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 and run until a shutdown signal arrives.
#
# NOTE: engine.start() installs its own asyncio SIGINT/SIGTERM handlers
# (see GameEngine._setup_signals). Those handlers call engine.stop() and
# SUPPRESS the default KeyboardInterrupt, so a manual
# `while True: await asyncio.sleep(1)` / `except KeyboardInterrupt` loop
# would never exit on Ctrl-C. Use engine.run() instead: it starts the
# engine, waits on the internal stop event (set by the signal handlers or
# by any call to engine.stop()), then shuts down cleanly.
print("Starting My MUD Game...")
await engine.run()
print("Shutting down...")
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."""
# Rooms are plain data dicts registered with the world by UUID. Exits map
# a direction to the destination room's UUID.
from uuid import uuid4
world = engine.world
start_id = uuid4()
tavern_id = uuid4()
# Register the starting room, with an exit north to the tavern.
world.register_room(
start_id,
{
"name": "Town Square",
"description": "The bustling center of town.",
"exits": {"north": tavern_id},
},
)
# Register a connected room, with an exit south back to the square.
world.register_room(
tavern_id,
{
"name": "The Rusty Dagger Tavern",
"description": "A cozy tavern with a roaring fireplace.",
"exits": {"south": start_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
from uuid import UUID, uuid4
async def _load_rooms(self, engine):
with open("data/rooms.yaml") as f:
data = yaml.safe_load(f)
world = engine.world
# register_room() and PositionComponent.room_id use UUIDs, but YAML files
# use readable string ids. Assign a UUID to every string id first, then
# translate each exit's destination id through the same map.
id_map: dict[str, UUID] = {room["id"]: uuid4() for room in data["rooms"]}
for room_data in data["rooms"]:
exits = {
direction: id_map[dest]
for direction, dest in room_data.get("exits", {}).items()
}
world.register_room(
id_map[room_data["id"]],
{
"name": room_data["name"],
"description": room_data["description"],
"exits": exits,
},
)
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(
behavior_type=template.get("behavior", "passive"),
))
if template.get("hostile"):
# CombatComponent (maid_stdlib.components) holds attack/defense stats.
entity.add(CombatComponent(
attack_power=template.get("attack_power", 5),
defense=template.get("defense", 0),
))
entity.add_tag("hostile")
# Index the entity into the room so movement/look see it.
world.place_entity_in_room(entity.id, room_id)
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)
Where
self.document_storecomes from. There is noworld.document_store. The store is owned by the engine — reach it viaengine.document_store(a property returning the activeDocumentStore), aStorageAwaresystem whoseset_storage()was called during load, orctx.document_storeinside a command. Store one of those on your class inon_load. Also noteDocumentCollection.upsert(doc_id, document)expects a registered schema model (aBaseModel), so register a collection schema and pass a model instance rather than a raw dict in production code.
Summary¶
Building a complete game requires:
- Layered content packs that build on each other
- Event-driven communication between systems
- Shared components for common data
- Clear documentation of system interactions
- Comprehensive testing at all levels
Next Steps¶
- Review the Combat System Tutorial for detailed system implementation
- Review the Magic System Tutorial for spell mechanics
- Check the Content Pack Guides for detailed API documentation
Additional Resources¶
- ECS Overview - Entity Component System patterns
- Events Reference - Built-in event types
- Components Reference - Built-in components
- Publishing Guide - Share your game