Asset Collection System Design¶
Generic Story-Layer Framework for Managing Asset Collections¶
Status: Draft/Tentative - Pending MVP validation
Layer: Application (tangl.story.concepts.asset)
Version: 3.8
Executive Summary¶
This document defines a generic asset collection framework at the story (application) layer that supports:
Wallets - countable/fungible assets (gold, tokens, currency)
Inventories - unconstrained discrete asset collections (bag of items)
Loadouts - constrained discrete asset collections with slots/validation (outfits, vehicles, credentials)
Domain-specific implementations (wearables, vehicle parts, documents) are author-layer mechanics that build on these application-layer primitives.
Architectural Layers¶
┌─────────────────────────────────────────────────────────────┐
│ AUTHOR LAYER (tangl.mechanics) │
│ │
│ OutfitManager VehicleManager CredentialsPacket │
│ DeckManager RobotLoadout etc. │
│ │
│ Domain-specific rules, validation, rendering │
└─────────────────────────────────────────────────────────────┘
↓ extends
┌─────────────────────────────────────────────────────────────┐
│ APPLICATION LAYER (tangl.story.concepts.asset) │
│ │
│ AssetCollection (abstract base) │
│ ├─ AssetWallet (countable assets) │
│ ├─ AssetInventory (discrete, no constraints) │
│ └─ ComponentManager (discrete, with constraints) │
│ │
│ Generic patterns, reusable validation, dispatch hooks │
└─────────────────────────────────────────────────────────────┘
↓ uses
┌─────────────────────────────────────────────────────────────┐
│ CORE LAYER (tangl.core) │
│ │
│ AssetType (singleton definition) │
│ DiscreteAsset (wrapped node for graph) │
│ CountableAsset (unwrapped type for counting) │
│ │
│ Foundational primitives, no domain knowledge │
└─────────────────────────────────────────────────────────────┘
Core Concepts¶
Asset Types¶
AssetType (singleton definition)
Immutable definition shared across all instances
Loaded from YAML or created programmatically
Instance inheritance via
from_refpatternExamples: “iron_sword”, “gold_coin”, “passport”
CountableAsset (fungible)
Tracked by count/quantity in a wallet
No graph nodes created
Examples: currency, tokens, resources
DiscreteAsset (unique)
Wrapped in graph nodes with instance state
Can be linked, owned, modified individually
Examples: specific sword, specific document
Collection Types¶
1. AssetWallet (Countable Collections)¶
Purpose: Manage counts of fungible assets
Use Cases:
Currency systems (gold, gems, credits)
Resources (wood, stone, food)
Tokens/points (experience, reputation)
Consumables (potions, ammo)
Characteristics:
No graph nodes
Fast addition/subtraction
Overflow/underflow checking
Value aggregation
Example:
wallet.gain(gold=50, gems=3)
if wallet.can_afford(gold=30):
wallet.spend(gold=30)
total_value = wallet.total_value()
2. AssetInventory (Unconstrained Discrete Collections)¶
Purpose: Simple bag/container of discrete assets
Use Cases:
General inventory (backpack, chest)
Quest items collection
Loot drops
Storage containers
Characteristics:
Graph nodes for each item
No slot constraints
Optional weight/capacity limits
Simple add/remove
Example:
inventory.add(sword_token)
inventory.add(potion_token)
items = inventory.items # All items
weapons = inventory.items_of_type(Weapon)
3. ComponentManager (Constrained Discrete Collections)¶
Purpose: Complex collections with slots, validation, dependencies
Use Cases:
Outfits (clothing layers, body coverage)
Vehicles (parts, power budgets, slots)
Credentials (requirements, expiry, dependencies)
Card decks (zones, limits, costs)
Robot loadouts (compatibility, resources)
Characteristics:
Graph nodes for each component
Slot/position constraints
Resource budgets
Dependencies between components
Validation rules
Aggregate properties
Example:
outfit.put_on(jacket)
outfit.open(jacket) # State transition
errors = outfit.validate_configuration()
desc = outfit.describe() # "wearing open jacket, jeans"
Design: AssetCollection Base¶
class AssetCollection(ABC):
"""
Abstract base for all asset collection types.
Provides common interface for:
- Checking contents
- Adding/removing assets
- Validation
- Serialization
- Description/rendering
Subclasses implement specific storage and validation logic.
"""
def __init__(self, owner: Node):
"""
Args:
owner: The node that owns this collection
"""
self.owner = owner
# ==================
# Abstract Interface
# ==================
@abstractmethod
def contains(self, asset_identifier: str) -> bool:
"""Check if collection contains asset by label/type."""
...
@abstractmethod
def add(self, asset, *, ctx: Context = None) -> None:
"""Add asset to collection with validation."""
...
@abstractmethod
def remove(self, asset, *, ctx: Context = None) -> None:
"""Remove asset from collection."""
...
@abstractmethod
def clear(self) -> None:
"""Remove all assets."""
...
@abstractmethod
def validate(self, *, context: dict = None) -> list[str]:
"""
Validate current collection state.
Returns:
List of error messages (empty if valid)
"""
...
@abstractmethod
def describe(self) -> str:
"""Generate narrative description of collection."""
...
# ==================
# Common Helpers
# ==================
def is_valid(self, *, context: dict = None) -> bool:
"""Check if collection is valid."""
return len(self.validate(context=context)) == 0
def get_errors(self, *, context: dict = None) -> list[str]:
"""Get validation errors."""
return self.validate(context=context)
Design: AssetWallet (Countable)¶
class AssetWallet(Counter[str], AssetCollection):
"""
Collection for countable/fungible assets.
Stores counts keyed by asset label. Does not create graph nodes.
Example:
wallet = AssetWallet(player)
wallet.gain(gold=50, gems=3)
wallet.spend(gold=10)
wallet.can_afford(gold=30) # True
wallet.total_value() # 43.0 (if gold=1.0, gems=10.0)
"""
def __init__(self, owner: Node):
AssetCollection.__init__(self, owner)
Counter.__init__(self)
# ==================
# Wallet-Specific API
# ==================
def gain(self, **amounts: float) -> None:
"""
Add countable assets.
Args:
**amounts: Keyword args of asset_label=count
Example:
wallet.gain(gold=50, gems=3)
"""
self.update(amounts)
def can_afford(self, **amounts: float) -> bool:
"""
Check if wallet contains sufficient assets.
Args:
**amounts: Required amounts by asset label
Returns:
True if all requirements met
"""
for label, required in amounts.items():
if self[label] < required:
return False
return True
def spend(self, **amounts: float) -> None:
"""
Remove assets (raises if insufficient).
Args:
**amounts: Amounts to remove by asset label
Raises:
ValueError: If insufficient assets
"""
if not self.can_afford(**amounts):
raise ValueError(
f"Insufficient assets: need {amounts}, have {dict(self)}"
)
for label, amount in amounts.items():
self[label] -= amount
def total_value(self, asset_manager: AssetManager = None) -> float:
"""
Calculate total value of all assets.
Args:
asset_manager: Manager to look up asset values
Returns:
Sum of (count * asset.value) for all assets
"""
if not asset_manager:
# Try to get from owner's world
asset_manager = getattr(self.owner.graph, 'asset_manager', None)
if not asset_manager:
raise ValueError("No asset_manager available for value calculation")
total = 0.0
for label, count in self.items():
asset_type = asset_manager.get_countable_type('currency', label)
total += asset_type.value * count
return total
# ==================
# AssetCollection Interface
# ==================
def contains(self, asset_label: str) -> bool:
"""Check if wallet has any of this asset."""
return self[asset_label] > 0
def add(self, asset_label: str, amount: float = 1.0, *, ctx: Context = None):
"""Add amount of asset."""
self.gain(**{asset_label: amount})
def remove(self, asset_label: str, amount: float = 1.0, *, ctx: Context = None):
"""Remove amount of asset."""
self.spend(**{asset_label: amount})
def clear(self):
"""Remove all assets."""
Counter.clear(self)
def validate(self, *, context: dict = None) -> list[str]:
"""Validate wallet state (always valid for basic wallet)."""
return []
def describe(self) -> str:
"""Describe wallet contents."""
if not self:
return "empty wallet"
items = sorted(self.items(), key=lambda x: -x[1])
parts = [f"{count:.0f} {label}" for label, count in items if count > 0]
return ", ".join(parts)
Design: AssetInventory (Unconstrained Discrete)¶
class AssetInventory(AssetCollection):
"""
Collection for discrete assets without constraints.
Simple bag/container that holds discrete asset nodes.
Optional capacity limits, but no slot/compatibility rules.
Example:
inventory = AssetInventory(player)
inventory.add(sword_token)
inventory.add(potion_token)
items = inventory.items # All items
weapons = inventory.items_of_type(Weapon)
"""
def __init__(self, owner: Node, *, max_items: int = None, max_weight: float = None):
"""
Args:
owner: Owning node
max_items: Maximum number of items (None = unlimited)
max_weight: Maximum weight capacity (None = unlimited)
"""
super().__init__(owner)
self.max_items = max_items
self.max_weight = max_weight
# ==================
# Collection Access
# ==================
@property
def items(self) -> list[DiscreteAsset]:
"""All items in inventory."""
return self.owner.find_children(DiscreteAsset)
def items_of_type(self, asset_type: Type[DiscreteAsset]) -> list[DiscreteAsset]:
"""Get items of specific type."""
return [item for item in self.items if isinstance(item, asset_type)]
def get_item(self, label: str) -> Optional[DiscreteAsset]:
"""Get specific item by label."""
matches = [item for item in self.items if item.label == label]
return matches[0] if matches else None
def count(self) -> int:
"""Number of items in inventory."""
return len(self.items)
def total_weight(self) -> float:
"""Total weight of all items."""
return sum(
getattr(item.singleton, 'weight', 0.0)
for item in self.items
)
# ==================
# Validation
# ==================
def can_add(self, item: DiscreteAsset) -> tuple[bool, list[str]]:
"""
Check if item can be added.
Returns:
(can_add, error_messages)
"""
errors = []
# Check item count
if self.max_items is not None:
if self.count() >= self.max_items:
errors.append(f"Inventory full ({self.max_items} items)")
# Check weight
if self.max_weight is not None:
item_weight = getattr(item.singleton, 'weight', 0.0)
if self.total_weight() + item_weight > self.max_weight:
errors.append(
f"Too heavy: {self.total_weight() + item_weight:.1f} > {self.max_weight}"
)
return len(errors) == 0, errors
def validate(self, *, context: dict = None) -> list[str]:
"""Validate current inventory state."""
errors = []
# Check total count
if self.max_items is not None:
if self.count() > self.max_items:
errors.append(f"Too many items: {self.count()} > {self.max_items}")
# Check total weight
if self.max_weight is not None:
total = self.total_weight()
if total > self.max_weight:
errors.append(f"Overweight: {total:.1f} > {self.max_weight}")
return errors
# ==================
# Mutation API
# ==================
def add(self, item: DiscreteAsset, *, ctx: Context = None) -> None:
"""
Add item to inventory.
Args:
item: Discrete asset node
ctx: Optional context for dispatch
Raises:
ValueError: If capacity exceeded
"""
can_add, errors = self.can_add(item)
if not can_add:
raise ValueError(f"Cannot add item: {errors}")
# Dispatch validation
if ctx:
from tangl.story.dispatch import story_dispatch
story_dispatch.dispatch(
item, ctx=ctx, task="validate_inventory_add", inventory=self
)
# Link item to owner
self.owner.add_child(item)
item.owner_id = self.owner.uid
# Lifecycle event
if ctx:
story_dispatch.dispatch(
item, ctx=ctx, task="inventory_add", inventory=self
)
def remove(self, item: DiscreteAsset, *, ctx: Context = None) -> None:
"""
Remove item from inventory.
Args:
item: Item to remove
ctx: Optional context for dispatch
"""
if item not in self.items:
raise ValueError(f"Item {item.label} not in inventory")
# Lifecycle event
if ctx:
from tangl.story.dispatch import story_dispatch
story_dispatch.dispatch(
item, ctx=ctx, task="inventory_remove", inventory=self
)
# Unlink from owner
item.owner_id = None
self.owner.remove_child(item)
def clear(self):
"""Remove all items."""
for item in list(self.items):
self.remove(item)
# ==================
# AssetCollection Interface
# ==================
def contains(self, asset_label: str) -> bool:
"""Check if inventory contains item."""
return self.get_item(asset_label) is not None
def describe(self) -> str:
"""Describe inventory contents."""
if not self.items:
return "empty inventory"
count = self.count()
weight_info = f", {self.total_weight():.1f} lbs" if self.max_weight else ""
return f"{count} items{weight_info}"
Design: ComponentManager (Constrained Discrete)¶
class ComponentManager(AssetCollection, Generic[CT]):
"""
Collection for discrete assets with complex constraints.
Supports:
- Slot/position assignments
- Layering/priority
- Resource budgets (power, weight, etc.)
- Inter-component dependencies
- State machines
- Temporal validation (expiry, cooldowns)
- Context-aware rules (destination, mission)
- Aggregate property calculation
This is the base class for domain-specific managers like
OutfitManager, VehicleManager, CredentialsPacket, etc.
Example:
class OutfitManager(ComponentManager[Wearable]):
slot_capacity = {BodyRegion.UPPER: 10, ...}
tracked_resources = []
def is_visible(self, item):
# Custom visibility logic
...
"""
def __init__(self, owner: Node):
super().__init__(owner)
# ==================
# Configuration (Override in Subclass)
# ==================
slot_capacity: ClassVar[dict[str, int]] = {}
"""Maximum components per slot. Empty = no slot constraints."""
tracked_resources: ClassVar[list[str]] = []
"""Resource names to validate (power, weight, etc.)."""
required_components: ClassVar[list[str]] = []
"""Component labels that must be present."""
# ==================
# Collection Access
# ==================
@property
def components(self) -> list[CT]:
"""All components managed by this collection."""
return self.owner.find_children(DiscreteAsset)
def get_component(self, label: str) -> Optional[CT]:
"""Get component by label."""
matches = [c for c in self.components if c.label == label]
return matches[0] if matches else None
def get_component_of_type(self, component_type: Type[CT]) -> Optional[CT]:
"""Get single component of specific type (chassis, engine, etc)."""
results = [c for c in self.components if isinstance(c, component_type)]
return results[0] if results else None
def get_components_of_type(self, component_type: Type[CT]) -> list[CT]:
"""Get all components of specific type (weapons, gadgets, etc)."""
return [c for c in self.components if isinstance(c, component_type)]
def by_slot(self, slot: str) -> list[CT]:
"""Get components in a specific slot."""
return [
c for c in self.components
if getattr(c.singleton, 'slot', None) == slot
]
def by_layer(self, layer: int) -> list[CT]:
"""Get components at a specific layer/priority."""
return [
c for c in self.components
if getattr(c.singleton, 'layer', 0) == layer
]
# ==================
# Visibility (Override for Complex Rules)
# ==================
def is_visible(self, component: CT) -> bool:
"""
Check if component is visible/active.
Override in subclass for domain-specific visibility rules
(e.g., layering, coverage, occlusion).
Default: Visible if not in 'off' or 'disabled' state.
"""
state = getattr(component, 'state', None)
return state not in {'off', 'disabled', 'removed'}
def visible_components(self) -> list[CT]:
"""Get all visible components."""
return [c for c in self.components if self.is_visible(c)]
# ==================
# Validation
# ==================
def validate(self, *, context: dict = None) -> list[str]:
"""
Full validation. Returns list of error messages.
Runs all validation checks:
- Required components
- Resource budgets
- Inter-component dependencies
- Temporal constraints
- Slot capacity
- Context-specific rules
- Custom subclass rules
"""
errors = []
# Required components
errors.extend(self._check_required_components())
# Resource budgets
errors.extend(self._validate_resources())
# Dependencies between components
errors.extend(self._check_dependencies())
# Temporal constraints
if context and 'current_time' in context:
errors.extend(self._check_temporal_validity(
current_time=context['current_time']
))
# Slot capacity
errors.extend(self._check_slot_capacity())
# Context-specific rules
if context:
errors.extend(self._validate_in_context(context=context))
# Subclass-specific rules
errors.extend(self._validate_custom())
return errors
def _check_required_components(self) -> list[str]:
"""Check that required components are present."""
errors = []
for required_label in self.required_components:
if not self.get_component(required_label):
errors.append(f"Missing required component: {required_label}")
return errors
def _validate_resources(self) -> list[str]:
"""Check resource budgets (power, weight, etc)."""
errors = []
for resource in self.tracked_resources:
used, capacity = self._check_resource_budget(resource)
if capacity is not None and used > capacity:
errors.append(
f"{resource} overload: {used:.1f} > {capacity:.1f}"
)
return errors
def _check_resource_budget(self, resource: str) -> tuple[float, Optional[float]]:
"""
Calculate resource usage.
Returns:
(used, capacity) tuple. capacity=None means unlimited.
"""
# Get capacity from owner
capacity = getattr(self.owner, f'max_{resource}', None)
# Sum costs from components
used = sum(
getattr(c.singleton, f'{resource}_cost', 0.0)
for c in self.components
)
return used, capacity
def _check_dependencies(self) -> list[str]:
"""Validate inter-component dependencies."""
errors = []
for component in self.components:
requires = getattr(component.singleton, 'requires', [])
for req in requires:
if not self.get_component(req):
errors.append(
f"{component.label} requires {req}"
)
return errors
def _check_temporal_validity(self, *, current_time: int) -> list[str]:
"""Check time-based validity (expiry, cooldowns)."""
errors = []
for component in self.components:
# Check expiry
expiry = getattr(component, 'expiry_time', None)
if expiry is not None and expiry < current_time:
errors.append(f"{component.label} expired")
# Check cooldown
cooldown = getattr(component, 'cooldown_until', None)
if cooldown is not None and cooldown > current_time:
errors.append(f"{component.label} on cooldown")
return errors
def _check_slot_capacity(self) -> list[str]:
"""Check slot capacity constraints."""
errors = []
for slot, max_count in self.slot_capacity.items():
actual = len(self.by_slot(slot))
if actual > max_count:
errors.append(
f"Slot '{slot}' has {actual} components (max {max_count})"
)
return errors
def _validate_in_context(self, *, context: dict) -> list[str]:
"""
Context-specific validation (destination, mission, etc).
Override in subclass for domain-specific context rules.
"""
return []
def _validate_custom(self) -> list[str]:
"""
Subclass-specific validation rules.
Override in subclass for additional domain logic.
"""
return []
# ==================
# Aggregate Properties
# ==================
def aggregate_property(self, prop: str, default: float = 0) -> float:
"""
Sum a property across all components.
Includes:
- Base value from singleton definition
- Bonuses from instance state
- Penalties from instance state
Example:
total_defense = manager.aggregate_property('defense_bonus')
"""
total = default
for component in self.components:
# From singleton definition
total += getattr(component.singleton, prop, 0)
# From instance bonuses
total += getattr(component, f'{prop}_bonus', 0)
# From instance penalties
total -= getattr(component, f'{prop}_penalty', 0)
return total
# ==================
# Mutation API
# ==================
def can_add(self, component: CT) -> tuple[bool, list[str]]:
"""
Check if component can be added.
Returns:
(can_add, error_messages)
"""
# Would need to temporarily add and validate
# For now, just return True
# Subclasses can override for pre-checks
return True, []
def add(self, component: CT, *, ctx: Context = None) -> None:
"""
Add component with validation.
Args:
component: Component to add
ctx: Optional context for dispatch
Raises:
ValueError: If validation fails
"""
can_add, errors = self.can_add(component)
if not can_add:
raise ValueError(f"Cannot add component: {errors}")
# Dispatch validation
if ctx:
from tangl.story.dispatch import story_dispatch
story_dispatch.dispatch(
component, ctx=ctx, task="validate_component_add", manager=self
)
# Link to owner
self.owner.add_child(component)
component.owner_id = self.owner.uid
# Lifecycle event
if ctx:
story_dispatch.dispatch(
component, ctx=ctx, task="component_add", manager=self
)
def remove(self, component: CT, *, ctx: Context = None) -> None:
"""Remove component from manager."""
if component not in self.components:
raise ValueError(f"{component.label} not in collection")
# Lifecycle event
if ctx:
from tangl.story.dispatch import story_dispatch
story_dispatch.dispatch(
component, ctx=ctx, task="component_remove", manager=self
)
# Unlink
component.owner_id = None
self.owner.remove_child(component)
def clear(self):
"""Remove all components."""
for component in list(self.components):
self.remove(component)
# ==================
# AssetCollection Interface
# ==================
def contains(self, asset_label: str) -> bool:
"""Check if collection contains component."""
return self.get_component(asset_label) is not None
def describe(self) -> str:
"""
Generate narrative description.
Override in subclass for rich domain-specific descriptions.
Default: Simple count.
"""
visible = self.visible_components()
if not visible:
return "no components"
return f"{len(visible)} components"
Mixin Pattern for Owners¶
class HasAssetWallet:
"""Mixin for nodes with countable asset wallets."""
_wallet: Optional[AssetWallet] = None
@property
def wallet(self) -> AssetWallet:
"""Access the asset wallet."""
if self._wallet is None:
self._wallet = AssetWallet(self)
return self._wallet
class HasAssetInventory:
"""Mixin for nodes with unconstrained inventories."""
_inventory: Optional[AssetInventory] = None
@property
def inventory(self) -> AssetInventory:
"""Access the inventory."""
if self._inventory is None:
self._inventory = AssetInventory(self)
return self._inventory
class HasComponents:
"""Mixin for nodes with component managers."""
# Subclass must override
_component_manager_class: ClassVar[Type[ComponentManager]] = ComponentManager
@property
def components(self) -> ComponentManager:
"""Access the component manager."""
return self._component_manager_class(self)
Usage Examples¶
Example 1: Wallet (Countable Assets)¶
from tangl.story.concepts.asset import CountableAsset, AssetWallet, HasAssetWallet
# Define currency types
class Currency(CountableAsset):
value: float = 1.0
symbol: str = "$"
Currency(label='gold', value=1.0, symbol='🪙')
Currency(label='gems', value=10.0, symbol='💎')
# Use wallet
class Player(Actor, HasAssetWallet):
pass
player = Player(label='alice', graph=story_graph)
# Transactions
player.wallet.gain(gold=50, gems=3)
print(player.wallet.describe()) # "50 gold, 3 gems"
if player.wallet.can_afford(gold=30):
player.wallet.spend(gold=30)
print("Bought sword!")
total = player.wallet.total_value() # 20 + 30 = 50
Example 2: Inventory (Unconstrained Discrete)¶
from tangl.story.concepts.asset import AssetType, DiscreteAsset, AssetInventory
# Define item types
class Item(AssetType):
weight: float = 1.0
value: int = 10
Item(label='sword', weight=3.5, value=50)
Item(label='potion', weight=0.5, value=20)
# Create tokens
graph = Graph(label='game')
sword = DiscreteAsset[Item](label='sword', graph=graph)
potion = DiscreteAsset[Item](label='potion', graph=graph)
# Use inventory
player = Player(label='bob', graph=graph)
player.inventory.add(sword)
player.inventory.add(potion)
print(player.inventory.describe()) # "2 items, 4.0 lbs"
# Check capacity
if player.inventory.count() < player.inventory.max_items:
player.inventory.add(another_item)
Example 3: Components (Constrained Discrete)¶
# See OutfitManager, VehicleManager examples in previous docs
# These are author-layer implementations in tangl.mechanics
Migration Path¶
Phase 1: MVP - Core Infrastructure¶
Goal: Get basic discrete and countable assets working
Tasks:
Fix
DiscreteAssetgeneric typingImplement
AssetWallet(countable)Implement
AssetInventory(simple discrete)Complete
AssetManagerwith YAML loadingComprehensive tests for all three
Deliverables:
test_countable_asset.py(8 tests)test_discrete_asset.py(10 tests)test_asset_wallet.py(12 tests)test_asset_inventory.py(15 tests)test_asset_manager.py(12 tests)Example YAML files
Time: ~1 week
Phase 2: Application Layer - Component Manager Base¶
Goal: Extract and generalize the pattern
Tasks:
Create
ComponentManagerbase classPort
OutfitManagerto use baseAdd dispatch hooks for lifecycle events
Documentation and examples
Deliverables:
test_component_manager.py(20 tests)test_outfit_manager.py(25 tests)API documentation
Extension guide
Time: ~1 week
Open Design Questions¶
1. Naming¶
Current:
AssetWallet- countableAssetInventory- discrete, unconstrainedComponentManager- discrete, constrained
Alternatives:
AssetBaginstead ofAssetInventory?AssetLoadoutinstead ofComponentManager?SlottedCollectioninstead ofComponentManager?
2. Single vs Multiple Managers¶
Option A: One node can have multiple collection types
class Player(Actor, HasAssetWallet, HasAssetInventory, HasComponents):
@property
def outfit(self) -> OutfitManager:
return OutfitManager(self)
Option B: Collections are exclusive
class Player(Actor, HasAssetWallet):
# Can only have wallet, not inventory
Recommendation: Option A - allow mixing
3. Dispatch Integration¶
When to emit events?
On add/remove? (Yes)
On state transitions? (Yes)
On validation? (Maybe - for vetoing)
How to pass context?
Always require
ctxparameter?Make optional with
ctx=None?
Recommendation: Optional context, emit on mutations
4. Serialization¶
How to serialize collections?
Wallet: Just counts dict
{"gold": 50}Inventory: Reference item UIDs
["uid1", "uid2"]ComponentManager: Same as inventory?
What about state?
Wallet: No special state
Inventory: Capacity limits persisted?
ComponentManager: Slot assignments persisted on items?
Recommendation: Collections serialize as item references, state on items
Success Criteria¶
MVP (Phase 1)¶
✅ AssetWallet works with countable assets
✅ AssetInventory works with discrete assets
✅ Both integrate with AssetManager
✅ YAML loading for asset definitions
✅ All tests pass
✅ Can create player with wallet and inventory
Application Layer (Phase 2)¶
✅ ComponentManager base class defined
✅ Supports multiple component types
✅ Resource budgets, dependencies, validation
✅ Dispatch hooks for lifecycle events
✅ OutfitManager uses base
Conclusion¶
This design provides:
Clear layering - Core → Application → Author
Incremental implementation - MVP first, then generalize
Flexibility - Simple wallets to complex loadouts
Extensibility - Easy to add new domains
The key insight: asset collections are a spectrum, from simple counts to complex constrained systems. The design supports all cases with a common base abstraction while allowing domain-specific specialization.
Next steps: Validate core infrastructure (Phase 1 MVP), then extract patterns (Phase 2), then prove generalization (Phase 3).