Core Concepts¶
This guide introduces the fundamental concepts you need to understand when building with MAID. These concepts form the foundation of MAID's architecture and are essential for creating content packs, systems, and game functionality.
Architecture Overview¶
MAID is built on a layered architecture that separates infrastructure from content:
+----------------------------------------+
| maid-classic-rpg | <- Game content
+----------------------------------------+
| maid-stdlib | <- Common components
+----------------------------------------+
| maid-engine | <- Pure infrastructure
+----------------------------------------+
- maid-engine: Core infrastructure including ECS, networking, event bus, and plugin system
- maid-stdlib: Reusable components, base systems, events, and utilities
- maid-classic-rpg: Traditional MUD gameplay content (combat, magic, quests)
Entity Component System (ECS)¶
MAID uses an Entity Component System architecture for game objects. This pattern separates data (components) from behavior (systems), making your code modular and flexible.
Entities¶
Entities are unique identifiers that represent game objects. An entity is essentially just a UUID that groups components together.
from maid_engine.core.ecs import Entity, EntityManager
# Create an entity manager
manager = EntityManager()
# Create a new entity
player = manager.create()
# Entities have unique IDs
print(player.id) # UUID('...')
# Entities can have tags for quick lookups
player.add_tag("player")
player.add_tag("online")
Components¶
Components are pure data containers with no behavior. They define what an entity is or has.
from maid_engine.core.ecs import Component
from uuid import UUID
class PositionComponent(Component):
"""Tracks where an entity is located."""
room_id: UUID
x: int = 0
y: int = 0
class HealthComponent(Component):
"""Tracks entity health."""
current: int
maximum: int
@property
def is_alive(self) -> bool:
return self.current > 0
Components are attached to entities:
from uuid import uuid4
# Add components to an entity
player.add(PositionComponent(room_id=uuid4()))
player.add(HealthComponent(current=100, maximum=100))
# Query components
if player.has(HealthComponent):
health = player.get(HealthComponent)
print(f"Health: {health.current}/{health.maximum}")
Systems¶
Systems contain the logic that operates on entities with specific components. They are executed each game tick.
from maid_engine.core.ecs import System
class RegenerationSystem(System):
"""Regenerates health for living entities."""
priority = 50 # Lower runs earlier
async def update(self, delta: float) -> None:
for entity in self.entities.with_components(HealthComponent):
health = entity.get(HealthComponent)
if health.is_alive and health.current < health.maximum:
# Regenerate based on time since last tick
regen = health.regeneration_rate * delta
health.heal(int(regen))
Content Packs¶
Content packs are the primary way to extend MAID. They package systems, commands, events, and data into reusable modules.
What Content Packs Provide¶
- Systems: Game logic that runs each tick
- Commands: Player-executable actions
- Events: Communication channels between systems
- Document Schemas: Data persistence definitions
- Lifecycle Hooks: Setup and teardown logic
Content Pack Protocol¶
Every content pack implements the ContentPack protocol:
from maid_engine.plugins.protocol import BaseContentPack
class MyContentPack(BaseContentPack):
@property
def manifest(self) -> ContentPackManifest:
"""Metadata about this pack."""
...
def get_dependencies(self) -> list[str]:
"""Packs that must load before this one."""
...
def get_systems(self, world: World) -> list[System]:
"""ECS systems to register."""
...
def get_events(self) -> list[type[Event]]:
"""Event types defined by this pack."""
...
def register_commands(self, registry: CommandRegistry) -> None:
"""Commands provided by this pack."""
...
async def on_load(self, engine: GameEngine) -> None:
"""Called when the pack is loaded."""
...
Dependency Resolution¶
Content packs can depend on other packs. MAID automatically resolves and loads them in the correct order:
# In your pack's manifest
dependencies = {"stdlib": ">=0.1.0"}
# Or in get_dependencies()
def get_dependencies(self) -> list[str]:
return ["stdlib"]
Event Bus¶
The event bus enables decoupled communication between systems. Events are messages that describe something that happened in the game.
Defining Events¶
Events are dataclasses that inherit from Event:
from dataclasses import dataclass
from uuid import UUID
from maid_engine.core.events import Event
@dataclass
class PlayerDamagedEvent(Event):
"""Emitted when a player takes damage."""
player_id: UUID
damage: int
source_id: UUID | None = None
Subscribing to Events¶
Systems and other code can subscribe to specific event types:
async def on_player_damaged(event: PlayerDamagedEvent):
print(f"Player {event.player_id} took {event.damage} damage!")
# Subscribe to the event
world.events.subscribe(PlayerDamagedEvent, on_player_damaged)
Emitting Events¶
When something happens, emit an event:
event = PlayerDamagedEvent(
player_id=player.id,
damage=25,
source_id=monster.id,
)
await world.events.emit(event)
Event Priority¶
Handlers can specify priority to control execution order:
from maid_engine.core.events import EventPriority
world.events.subscribe(
PlayerDamagedEvent,
damage_reduction_handler,
priority=EventPriority.HIGH # Runs before normal handlers
)
Canceling Events¶
Handlers can cancel events to prevent further processing:
async def god_mode_handler(event: PlayerDamagedEvent):
if is_god_mode(event.player_id):
event.cancel() # No damage in god mode!
Commands¶
Commands are actions that players can execute. MAID's layered command registry allows content packs to override commands from lower-priority packs.
Defining Commands¶
from maid_engine.commands.registry import CommandContext
async def look_command(ctx: CommandContext) -> bool:
"""Look at your surroundings."""
# Get player's current room
player = ctx.world.entities.get(ctx.player_id)
if not player:
return False
position = player.try_get(PositionComponent)
if not position:
await ctx.session.send("You are nowhere.")
return True
# Describe the room...
await ctx.session.send("You see a room.")
return True
Registering Commands¶
def register_commands(registry: LayeredCommandRegistry, pack_name: str) -> None:
registry.register(
name="look",
handler=look_command,
pack_name=pack_name,
aliases=["l", "examine"],
category="movement",
description="Look at your surroundings",
usage="look [target]",
access_level=AccessLevel.PLAYER,
)
Command Priorities¶
When multiple packs register the same command, the highest priority wins:
# Set pack priorities (higher number = higher priority)
registry.set_pack_priority("my-game", 100)
registry.set_pack_priority("stdlib", 50)
# my-game's "look" command will be used instead of stdlib's
The World¶
The World is the central state container for a game instance. It holds:
- EntityManager: All game entities
- EventBus: Event pub/sub system
- SystemManager: All registered systems
from maid_engine.core.world import World
world = World()
# Access entities
player = world.entities.create()
# Access events
await world.events.emit(some_event)
# Access systems
await world.systems.update(delta_time)
Game Loop¶
MAID uses a tick-based game loop. Each tick:
- The game calculates the delta time since the last tick
- A
TickEventis emitted - All enabled systems are updated in priority order
- Pending events are processed
The tick rate is configurable (default: 4 ticks per second):
Document Store¶
The document store provides persistence for game data. It uses a simple CRUD interface with collections.
from maid_engine.storage.document_store import QueryOptions, SortOrder
# Get a collection
characters = store.get_collection("characters")
# Create a document
char_id = await characters.create(character_data)
# Read a document
character = await characters.get(char_id)
# Query documents
high_level = await characters.query(QueryOptions(
filters={"level": 10},
order_by="name",
order=SortOrder.ASC,
limit=20,
))
# Update a document
await characters.update(char_id, updated_data)
# Delete a document
await characters.delete(char_id)
Networking¶
MAID supports multiple network protocols:
- Telnet: Traditional MUD protocol with GMCP, MXP, MCCP support
- WebSocket: Modern web client support
- REST API: External integrations
Players connect through network sessions:
# In command handlers, use ctx.session to communicate
await ctx.session.send("Hello, player!")
# Sessions have player information
player_id = ctx.player_id
Configuration¶
MAID uses environment variables with the MAID_ prefix:
# Core settings
MAID_DEBUG=true
MAID_LOG_LEVEL=DEBUG
# Game settings
MAID_GAME__TICK_RATE=4.0
# Network settings
MAID_TELNET__PORT=4000
MAID_WEB__PORT=8080
# AI settings
MAID_AI__DEFAULT_PROVIDER=anthropic
MAID_AI__ANTHROPIC_API_KEY=sk-...
Settings can also be configured via a .env file in your project root.
Next Steps¶
Now that you understand the core concepts, you can: