Skip to content

Components in MAID

Components are pure data containers that define the properties and state of entities. They contain no logic - all behavior is implemented in Systems.

What is a Component?

A component is a Pydantic model that holds data about one aspect of an entity. For example:

  • HealthComponent - stores health points
  • PositionComponent - stores location information
  • InventoryComponent - stores carried items
from maid_engine.core.ecs import Component

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

Creating Components

Basic Component

from maid_engine.core.ecs import Component

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

With Default Values

class HealthComponent(Component):
    current: int = 100
    maximum: int = 100
    regeneration_rate: float = 0.0

With Validation

Components inherit from Pydantic's BaseModel, so you get validation:

from pydantic import Field, field_validator

class HealthComponent(Component):
    current: int = Field(ge=0)
    maximum: int = Field(ge=1)

    @field_validator("current")
    @classmethod
    def current_not_exceed_maximum(cls, v, info):
        if "maximum" in info.data and v > info.data["maximum"]:
            return info.data["maximum"]
        return v

With Complex Types

from uuid import UUID
from datetime import datetime
from enum import Enum

class DamageType(Enum):
    PHYSICAL = "physical"
    MAGICAL = "magical"
    FIRE = "fire"

class CombatComponent(Component):
    target_id: UUID | None = None
    damage_type: DamageType = DamageType.PHYSICAL
    last_attack: datetime | None = None
    attack_cooldown: float = 1.0

Component Design Patterns

Single Responsibility

Each component should represent one concept:

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

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

class StaminaComponent(Component):
    current: int
    maximum: int
# Bad - too many responsibilities
class ResourcesComponent(Component):
    health: int
    max_health: int
    mana: int
    max_mana: int
    stamina: int
    max_stamina: int

Marker Components

Use empty components as markers when you need to flag entities:

class InCombatComponent(Component):
    """Marker for entities currently in combat."""
    pass

class InvisibleComponent(Component):
    """Marker for invisible entities."""
    pass

# Usage
entity.add(InCombatComponent())

# Query
for entity in manager.with_components(InCombatComponent):
    # Entity is in combat
    pass

Reference Components

Use UUIDs to reference other entities:

class TargetComponent(Component):
    """Component that references another entity."""
    target_id: UUID

class OwnerComponent(Component):
    """Component that references an owner entity."""
    owner_id: UUID

class EquipmentSlots(Component):
    """References to equipped item entities."""
    weapon: UUID | None = None
    armor: UUID | None = None
    helmet: UUID | None = None

Collection Components

For lists of related data:

from uuid import UUID

class InventoryComponent(Component):
    """Stores references to item entities."""
    items: list[UUID] = []
    max_slots: int = 20

class SkillsComponent(Component):
    """Stores learned skills with levels."""
    skills: dict[str, int] = {}  # skill_name -> level

State Components

For tracking state over time:

from datetime import datetime

class CooldownComponent(Component):
    """Tracks ability cooldowns."""
    cooldowns: dict[str, datetime] = {}  # ability_name -> ready_at

class BuffsComponent(Component):
    """Tracks active buffs/effects."""
    buffs: dict[str, float] = {}  # buff_name -> remaining_duration

Component Type Identifier

Each component has a type identifier used for serialization:

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

# Automatic type identifier (class name)
print(HealthComponent.get_type())  # "HealthComponent"

# Custom type identifier
class HealthComponent(Component):
    component_type = "maid_stdlib.health"
    current: int
    maximum: int

print(HealthComponent.get_type())  # "maid_stdlib.health"

Working with Components

Creating Instances

# Positional arguments (in field order)
health = HealthComponent(100, 100)

# Keyword arguments (recommended)
health = HealthComponent(current=100, maximum=100)

# With defaults
health = HealthComponent(current=50)  # maximum uses default

Accessing Data

health = entity.get(HealthComponent)

# Read
print(health.current)
print(health.maximum)

# Write (triggers validation)
health.current = 75
health.current = min(health.current + 10, health.maximum)

Serialization

# To dictionary
health = HealthComponent(current=75, maximum=100)
data = health.model_dump()
# {"current": 75, "maximum": 100}

# From dictionary
health = HealthComponent.model_validate({"current": 75, "maximum": 100})

# To JSON
json_str = health.model_dump_json()

# From JSON
health = HealthComponent.model_validate_json('{"current": 75, "maximum": 100}')

Copying

# Shallow copy
health_copy = health.model_copy()

# Deep copy
health_copy = health.model_copy(deep=True)

# Copy with modifications
damaged_health = health.model_copy(update={"current": health.current - 10})

Component Configuration

Components use Pydantic's configuration system:

from pydantic import ConfigDict

class HealthComponent(Component):
    # Inherited from Component base class:
    # - validate_assignment=True (validate on attribute assignment)
    # - use_enum_values=True (serialize enums as values)
    # - extra="forbid" (error on unknown fields)

    current: int
    maximum: int

Allowing Extra Fields

If you need to allow unknown fields (not recommended):

from pydantic import ConfigDict

class FlexibleComponent(Component):
    model_config = ConfigDict(extra="allow")

    known_field: str
    # Can now have additional fields

Organizing Components

By Content Pack

Organize components within your content pack:

my-content-pack/
└── src/
    └── my_pack/
        ├── __init__.py
        ├── pack.py
        └── components/
            ├── __init__.py
            ├── combat.py      # CombatComponent, DamageComponent
            ├── character.py   # HealthComponent, ManaComponent
            └── inventory.py   # InventoryComponent, ItemComponent

Export from Package

# my_pack/components/__init__.py
from .combat import CombatComponent, DamageComponent
from .character import HealthComponent, ManaComponent
from .inventory import InventoryComponent, ItemComponent

__all__ = [
    "CombatComponent",
    "DamageComponent",
    "HealthComponent",
    "ManaComponent",
    "InventoryComponent",
    "ItemComponent",
]

Best Practices

1. Keep Components Small

# Good - focused
class PositionComponent(Component):
    room_id: UUID
    x: int = 0
    y: int = 0

# Bad - too much
class EntityStateComponent(Component):
    room_id: UUID
    x: int
    y: int
    health: int
    mana: int
    target: UUID | None
    # ... many more fields

2. Use Type Hints

# Good - clear types
class InventoryComponent(Component):
    items: list[UUID] = []
    gold: int = 0
    max_weight: float = 100.0

# Bad - no types
class InventoryComponent(Component):
    items = []  # What type?
    gold = 0
    max_weight = 100.0

3. Provide Sensible Defaults

class HealthComponent(Component):
    current: int = 100  # New entities start with full health
    maximum: int = 100
    regeneration_rate: float = 0.0  # No regen by default

4. Document Components

class PositionComponent(Component):
    """Tracks an entity's location within the game world.

    Attributes:
        room_id: UUID of the room containing this entity
        x: X coordinate within the room (0-based)
        y: Y coordinate within the room (0-based)
    """

    room_id: UUID
    x: int = 0
    y: int = 0

5. Use Validators for Constraints

from pydantic import Field, model_validator

class HealthComponent(Component):
    current: int = Field(ge=0)  # Must be >= 0
    maximum: int = Field(ge=1)  # Must be >= 1

    @model_validator(mode="after")
    def clamp_current(self):
        if self.current > self.maximum:
            self.current = self.maximum
        return self

6. Avoid Business Logic

Components should not contain methods that implement game logic:

# Bad - logic in component
class HealthComponent(Component):
    current: int
    maximum: int

    def take_damage(self, amount: int) -> None:
        self.current = max(0, self.current - amount)

    def heal(self, amount: int) -> None:
        self.current = min(self.maximum, self.current + amount)
# Good - just data, logic in systems
class HealthComponent(Component):
    current: int
    maximum: int

# In a system:
class DamageSystem(System):
    async def apply_damage(self, entity: Entity, amount: int) -> None:
        health = entity.get(HealthComponent)
        health.current = max(0, health.current - amount)

Example: Complete Component Set

Here's an example of a coherent set of components for an RPG:

from uuid import UUID
from enum import Enum
from datetime import datetime
from maid_engine.core.ecs import Component

# Character identity
class NameComponent(Component):
    """Entity's display name and title."""
    name: str
    title: str = ""

# Physical attributes
class PositionComponent(Component):
    """Entity's location in the world."""
    room_id: UUID
    x: int = 0
    y: int = 0

# Resources
class HealthComponent(Component):
    """Entity's health points."""
    current: int = 100
    maximum: int = 100

class ManaComponent(Component):
    """Entity's magical energy."""
    current: int = 50
    maximum: int = 50

# Combat
class CombatStatsComponent(Component):
    """Entity's combat statistics."""
    attack: int = 10
    defense: int = 5
    speed: int = 10

class TargetComponent(Component):
    """Entity's current combat target."""
    target_id: UUID

# Inventory
class InventoryComponent(Component):
    """Entity's carried items."""
    items: list[UUID] = []
    gold: int = 0
    max_slots: int = 20

# Equipment slots
class EquipmentComponent(Component):
    """Entity's equipped items."""
    weapon: UUID | None = None
    armor: UUID | None = None
    shield: UUID | None = None
    accessory: UUID | None = None

# For items
class ItemComponent(Component):
    """Properties of an item entity."""
    item_type: str
    value: int = 0
    weight: float = 1.0
    stackable: bool = False
    stack_size: int = 1

# AI/behavior
class AIComponent(Component):
    """AI behavior configuration."""
    behavior_type: str = "passive"
    aggro_range: float = 5.0
    wander_radius: float = 10.0

Next Steps

  • Entities - Learn about creating and managing entities
  • Systems - Learn about implementing systems that process components