Architecture Overview¶
MAID is built on a modern, async-first architecture designed for extensibility and performance.
Package Layering¶
MAID follows a strict layered architecture where each package has clear responsibilities:
+------------------------------------------+
| Content Packs |
| (maid-classic-rpg, your-content-pack) |
+------------------------------------------+
|
v
+------------------------------------------+
| maid-stdlib |
| Common components, events, utilities |
+------------------------------------------+
|
v
+------------------------------------------+
| maid-engine |
| Core infrastructure (ECS, networking, |
| events, commands, storage, plugins) |
+------------------------------------------+
Package Responsibilities¶
maid-engine (Core Infrastructure) - Entity Component System (ECS) foundation - Event bus for pub/sub messaging - Tick-based game loop - Network servers (Telnet, WebSocket) - Command registry and processing - Document storage API - Content pack loading and lifecycle - Authentication and session management - No game-specific logic
maid-stdlib (Standard Library) - Common components (Health, Position, Inventory, etc.) - Standard events (RoomEnter, DamageDealt, ItemPickedUp, etc.) - Core commands (look, move, get, drop) - Utility functions (dice rolling, text formatting) - Building blocks for content packs
Content Packs (Game Logic) - Game-specific systems (combat, magic, crafting) - Custom commands and events - World content (rooms, items, NPCs) - Game rules and mechanics
Async Architecture¶
MAID is built entirely on Python's asyncio for non-blocking I/O:
# Everything is async
async def main():
engine = GameEngine(settings)
engine.load_content_pack(ClassicRPGContentPack())
await engine.start()
# Engine runs until stopped
await engine.run()
Benefits of Async Design¶
- High Concurrency - Handle hundreds of simultaneous connections
- Non-blocking I/O - Network and database operations don't block the game loop
- Clean Code - Async/await syntax is readable and maintainable
- Resource Efficient - Single-threaded event loop uses minimal resources
The Tick Loop¶
The game loop runs at a configurable tick rate (default: 4 ticks/second):
# Simplified tick loop
async def _tick_loop(self):
while not self._stop_event.is_set():
tick_start = time.monotonic()
delta = tick_start - self._last_tick_time
# Emit tick event
self._world.events.emit_sync(
TickEvent(tick_number=self._tick_count, delta=delta)
)
# Process world tick (all systems run)
await self._world.tick(delta)
# Sleep until next tick
sleep_time = self._tick_interval - (time.monotonic() - tick_start)
if sleep_time > 0:
await asyncio.sleep(sleep_time)
Systems receive the delta time and can perform time-based updates:
class RegenerationSystem(System):
async def update(self, delta: float) -> None:
for entity in self.entities.with_components(HealthComponent):
health = entity.get(HealthComponent)
# Regenerate 1 HP per second
health.current = min(
health.current + delta,
health.maximum
)
Network Stack¶
MAID supports multiple connection types simultaneously:
+------------------+
| MAIDServer |
+------------------+
|
+---------------+---------------+
| |
+-------v-------+ +-------v-------+
| Telnet Server | | Web Server |
| (port 4000) | | (port 8080) |
+---------------+ +---------------+
| |
+-------v-------+ +-------v-------+
|TelnetSessions | | WebSockets |
| GMCP, MSDP, | | JSON-based |
| MXP, MCCP | | protocol |
+---------------+ +---------------+
Telnet Server¶
The Telnet server supports traditional MUD protocols:
- GMCP (Generic MUD Communication Protocol) - Structured data exchange
- MSDP (MUD Server Data Protocol) - Variable-based data
- MXP (MUD eXtension Protocol) - Rich text formatting
- MCCP2 (MUD Client Compression Protocol) - Compression
- SSL/TLS - Secure connections
# Session lifecycle
session = TelnetSession(reader, writer)
await session.negotiate_protocols() # GMCP, etc.
await connection_handler(session, server) # Game logic
Web Server¶
The Web server provides:
- WebSocket API - Real-time gameplay
- REST API - Admin interface, external integrations
- Admin UI - React-based web administration
Session Management¶
Sessions abstract the connection type:
class Session(Protocol):
"""Protocol for player sessions."""
@property
def id(self) -> UUID: ...
@property
def player_id(self) -> UUID | None: ...
async def send(self, message: str) -> None: ...
async def send_gmcp(self, package: str, data: dict) -> None: ...
async def receive(self) -> str: ...
async def close(self) -> None: ...
Content packs work with the Session interface, not specific implementations.
Component Interactions¶
Here's how the major components interact during gameplay:
Player Input Game Output
| ^
v |
+----------+ +----------+
| Session | | Session |
+----------+ +----------+
| ^
v |
+------------------+ +------------------+
| CommandRegistry | | EventBus |
+------------------+ +------------------+
| ^
v |
+------------------+ +------------------+
| Command Handler |----->| Systems |
+------------------+ +------------------+
|
v
+------------------+
| Entities |
| (via components) |
+------------------+
- Player sends command through session
- CommandRegistry finds and executes the appropriate handler
- Handler modifies entities or emits events
- Systems process entities and events
- Results are sent back through the session
State Management¶
World State¶
The World class is the central state container:
class World:
entities: EntityManager # All game entities
events: EventBus # Event pub/sub
systems: SystemManager # ECS systems
def register_room(self, room_id: UUID, room_data: Any) -> None: ...
def get_room(self, room_id: UUID) -> Any | None: ...
def all_rooms(self) -> Iterator[tuple[UUID, Any]]: ...
Persistence¶
The DocumentStore API provides persistence:
class DocumentStore(ABC):
def register_schema(self, name: str, schema: type[BaseModel]) -> None: ...
def get_collection(self, name: str) -> DocumentCollection: ...
def has_collection(self, name: str) -> bool: ...
class DocumentCollection[T](ABC):
async def get(self, doc_id: UUID) -> T | None: ...
async def create(self, document: T, doc_id: UUID | None = None) -> UUID: ...
async def update(self, doc_id: UUID, document: T) -> bool: ...
async def delete(self, doc_id: UUID) -> bool: ...
async def query(self, options: QueryOptions) -> list[T]: ...
Implementations include:
- InMemoryDocumentStore - Testing and development
- PostgresDocumentStore - Production PostgreSQL storage
Persistence Layer¶
MAID provides durable entity persistence through a pipeline of cooperating components:
- DirtyTracker - Monitors component
__setattr__changes and flags entities as dirty - EntityPersistenceManager - Serializes dirty entities and coordinates save operations
- SaveScheduler - Batches and throttles writes to avoid overwhelming the storage backend
- DocumentStore - Abstract storage interface (InMemory, PostgreSQL implementations)
The GameEngine owns this pipeline: at startup it starts a SaveScheduler
background loop that periodically drains entities flagged by the DirtyTracker
and flushes them through the EntityPersistenceManager to the DocumentStore.
It runs only when persistence.enabled is set (the default) and produces
durable data only when a Postgres-backed DocumentStore is configured; with the
default in-memory store, entity state is not retained across restarts.
The separate
PersistenceSystemfrom maid-stdlib is a distinct, tick-based interval saver that content packs subclass to serialize characters. It is not what drives theDirtyTracker→EntityPersistenceManager→SaveSchedulerpipeline described above.
Migration Framework¶
Database schema changes are managed by the MigrationRunner:
- Migrations are scoped to namespaces so each content pack manages its own schema independently
- Migrations run in order at startup before content packs are loaded
- Supports both schema migrations and data transformations
Observability Layer¶
MAID includes built-in operational observability:
- ObservabilityRegistry - Central registration point for meters, tracers, and health checks
- PrometheusMeter - Exports tick timing, event throughput, and entity counts as Prometheus metrics
- HealthChecker - Aggregates health status from subsystems (database, network, tick loop)
- MaidContext - Propagates request-scoped context (correlation IDs, player info) through async call chains
Data Loader Pipeline¶
Content is loaded from YAML files through a 6-phase pipeline:
- Discovery - Scan content pack data directories for YAML files
- Parsing - Deserialize YAML into raw dictionaries
- Validation - Validate against registered document schemas
- Resolution - Resolve cross-references between entities (e.g., room exits, NPC inventories)
- Instantiation - Create ECS entities and attach components
- Indexing - Register entities with the World (room index, grid, etc.)
NPC Cognitive Systems¶
Advanced NPC behavior is built from layered cognitive subsystems:
- Memory - Episodic memories extracted from dialogue via LLM; stored per-NPC with decay
- Relationships - Tracks disposition, trust, and history between NPCs and players
- Knowledge - Graph of facts, rumors, and world knowledge accessible to NPCs
- Autonomy - Needs, goals, and daily schedules that drive independent NPC behavior
- Quest Generation - Synthesizes quests from world state, NPC knowledge, and player history
Configuration¶
Settings are loaded from environment variables with the MAID_ prefix:
Note:
get_settings()validates configuration on first call and raises while the adminsecret_keyis left at its default. To run this snippet outside the server, setMAID_DEBUG=true(development) or a secureMAID_ADMIN_SECRET_KEY(32+ characters) in your environment first.
from maid_engine.config.settings import get_settings
settings = get_settings()
print(settings.game.tick_rate) # 4.0
print(settings.telnet.port) # 4000
print(settings.ai.default_provider) # "anthropic"
Configuration is hierarchical using Pydantic models:
class Settings(BaseSettings):
game: GameSettings
telnet: TelnetSettings
web: WebSettings
database: DatabaseSettings
ai: AISettings
bridges: BridgeSettings
Hot Reload¶
MAID supports hot reloading content packs during development:
# Reload a content pack without restart.
# Use the pack's manifest name (e.g. "classic-rpg"), not the
# distribution/package name ("maid-classic-rpg").
await engine.hot_reload.reload_pack("classic-rpg")
The hot reload system: 1. Pauses the tick loop 2. Saves pack state (entities, systems) 3. Unloads the pack 4. Reloads Python modules 5. Re-registers commands, events, systems 6. Restores state 7. Resumes the tick loop
Further Reading¶
- ECS Concepts - Deep dive into Entity Component System
- Event System - Event-driven architecture
- Command System - Command processing
- Content Packs - Building content packs