Skip to content

ADR-001: Entity Component System Architecture

Status

Accepted

Date

2024-01-15

Context

MUD engines have traditionally modeled game entities (players, NPCs, items, rooms) using deep class hierarchies. A typical approach looks like: GameObject -> Character -> Player -> Warrior. This pattern leads to rigid inheritance trees, diamond inheritance problems, and god-objects that accumulate behavior over time. Adding a new capability (e.g., making an item also act as a container) requires restructuring the class tree.

MAID needs to support a wide range of entity types that can be dynamically composed at runtime: an NPC might gain inventory, a room might gain weather effects, a player might gain faction membership. Content packs (third-party gameplay modules) need to attach arbitrary data to entities without modifying the core engine.

Decision

Use an Entity Component System (ECS) architecture where:

  • Entities (Entity in packages/maid-engine/src/maid_engine/core/ecs/entity.py) are lightweight identifiers (UUIDs) that group components together. They carry no game logic themselves.
  • Components (Component in packages/maid-engine/src/maid_engine/core/ecs/component.py) are pure data containers built on Pydantic BaseModel. Examples include HealthComponent, PositionComponent, InventoryComponent, and DialogueComponent in packages/maid-stdlib/src/maid_stdlib/components/. They use validate_assignment=True and extra="forbid" for strict data integrity.
  • Systems (System in packages/maid-engine/src/maid_engine/core/ecs/system.py) contain all logic. They query entities by component type using EntityManager.with_components() and process matching entities each tick. Systems are managed by SystemManager, which runs them in priority order.

The EntityManager maintains indexed lookups (_by_component, _by_tag) for efficient queries. Components are keyed by their Python type, and entities support both type-based queries and Protocol-based queries via Entity.find_by_protocol().

Consequences

Positive

  • Composition over inheritance: Any entity can have any combination of components. An item can become a container by adding InventoryComponent. An NPC can become a quest-giver by adding DialogueComponent.
  • Content pack extensibility: Content packs like maid-classic-rpg define their own components (e.g., CharacterStatsComponent, CorpseComponent) without modifying the engine or stdlib.
  • Serialization: Since components are Pydantic models, serialization to/from JSON and document stores is built-in via model_dump() and Entity.to_dict().
  • Efficient queries: EntityManager.with_components() uses set intersection starting from the smallest index, making multi-component queries fast.

Negative

  • Debugging complexity: An entity's behavior is spread across multiple systems. To understand what happens when a player attacks, you must trace through CombatSystem, DamageSystem, and HealthSystem rather than reading a single Player.attack() method.
  • Component coupling: Systems often need to access multiple components together. A system processing movement needs both PositionComponent and MovementComponent, creating implicit coupling between component types.
  • Runtime type safety: Component queries return dynamic types. While entity.get(HealthComponent) is type-hinted, the actual set of components on an entity is only known at runtime.

Alternatives Considered

Deep Class Hierarchy

The traditional MUD approach (e.g., Player(Character(LivingEntity(Entity)))) was rejected because it creates rigid structures that cannot be extended by content packs without modifying the base classes.

Mixin-Based Composition

Python mixins (e.g., class Player(Attackable, Inventoried, Positioned)) were considered. While more flexible than deep hierarchies, mixins still require compile-time decisions about entity capabilities and lead to method name collisions and MRO complexity. ECS allows runtime composition.

Data-Oriented Design (SoD)

A pure structure-of-arrays ECS (components stored in contiguous arrays per type) was considered for cache efficiency. This was rejected because Python's object model does not benefit from cache-line optimization the way C/C++ does, and Pydantic models provide more value through validation and serialization.