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 pointsPositionComponent- stores location informationInventoryComponent- 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