This is the full workflow: define components, write processors, wire them into a world, run it.
The Pattern¶
Every simulation follows the same structure:
- Define components -- the data your entities carry
- Write processors -- the rules that transform that data each tick
- Create a world and register processors
- Spawn entities with initial component values
- Run -- the engine drains commands, applies them, steps processors, persists state
Complete Example¶
Agents gain experience each tick proportional to their skill. A second processor computes a rating. Copy this, run it.
uv run python examples/simulation_script.py
Source: examples/simulation_script.py
import asyncio
import daft
from daft import DataFrame, col
from archetype.core.aio.async_processor import AsyncProcessor
from archetype.core.aio.async_system import AsyncSystem
from archetype.core.aio.async_world import AsyncWorld
from archetype.core.component import Component
from archetype.core.config import RunConfig, WorldConfig
# ── Step 1: Define components ───────────────────────────────────────────
class Agent(Component):
name: str = ""
role: str = ""
skill: float = 1.0
experience: float = 0.0
rating: float = 0.0
# ── Step 2: Write processors ───────────────────────────────────────────
class ExperienceProcessor(AsyncProcessor):
"""Each tick, agents gain experience proportional to their skill."""
components = (Agent,)
priority = 10
async def process(self, df: DataFrame, **kwargs) -> DataFrame:
return df.with_column(
"agent__experience",
col("agent__experience") + col("agent__skill") * 2.0,
)
class RatingProcessor(AsyncProcessor):
"""Compute a rating from experience and skill."""
components = (Agent,)
priority = 50
async def process(self, df: DataFrame, **kwargs) -> DataFrame:
return df.with_column(
"agent__rating",
col("agent__experience") * col("agent__skill") / 10.0,
)
# ── Step 3-5: Create world, spawn entities, run ────────────────────────
async def main():
# (In-memory querier/updater omitted for brevity — see full source)
...
await world.create_entity([Agent(name="Alice", role="engineer", skill=3.0)])
await world.create_entity([Agent(name="Bob", role="designer", skill=2.0)])
await world.create_entity([Agent(name="Charlie", role="manager", skill=1.5)])
await world.run(RunConfig(num_steps=10))
Output:
Alice: skill=3.0, experience=60, rating=18.0
Bob: skill=2.0, experience=40, rating=8.0
Charlie: skill=1.5, experience=30, rating=4.5
Key Concepts¶
Processors Declare Their Requirements¶
A processor's components tuple says which entities it operates on. The engine routes the right data to the right processor automatically.
class MoveProcessor(AsyncProcessor):
components = (Agent, Position) # Only entities with BOTH
priority = 20
If you spawn an Agent without Position, MoveProcessor won't touch it. Spawn one with both, and it will.
Priority Controls Order¶
Lower priority runs first within each tick:
class GatherInput(AsyncProcessor):
priority = 1 # First: read sensors
class Think(AsyncProcessor):
priority = 10 # Second: decide
class Act(AsyncProcessor):
priority = 20 # Third: execute
class Record(AsyncProcessor):
priority = 100 # Last: log
Shared State via Resources¶
Processors can share configuration and services through the world's Resources container:
from dataclasses import dataclass
@dataclass
class SimConfig:
decay_rate: float = 2.0
max_energy: float = 100.0
# Register
world.resources.insert(SimConfig(decay_rate=3.0))
# Access in processor
class DecayProcessor(AsyncProcessor):
components = (Agent,)
priority = 1
async def process(self, df, resources=None, **kwargs):
config = resources.require(SimConfig)
return df.with_columns({
"agent__energy": col("agent__energy") - config.decay_rate,
})
Mutations Are Deferred¶
Spawn, despawn, add/remove components -- all mutations queue during a tick and apply at the start of the next tick. This keeps each tick consistent.
# These don't take effect until the next step()
await container.command_service.submit(wid, spawn_cmd, ctx)
await container.command_service.submit(wid, spawn_cmd, ctx)
# Now they materialize
await container.simulation_service.step(wid)
Fork to Compare Strategies¶
Run the same starting state with different processors or parameters:
# Base world with 100 ticks of history
await container.simulation_service.run(wid, RunConfig(num_steps=100))
# Fork and try a different strategy
fork = await container.world_service.fork_world(
source_world_id=wid,
name="aggressive",
storage_config=StorageConfig(),
)
await fork.system.add_processor(AggressiveStrategy())
await container.simulation_service.run(fork.world_id, RunConfig(num_steps=50))
# Compare outcomes
base_state = await container.query_service.get_world_state(wid)
fork_state = await container.query_service.get_world_state(fork.world_id)
What's Next¶
- Components -- field types, column prefixing, archetype signatures
- Processors -- LLM-powered processors, structured outputs, testing
- Examples -- runnable examples for every pattern