161 lines
5.0 KiB
Python

"""AI decision system for agents in the Village Simulation.
This module provides shared AI classes and utilities used by the GOAP
(Goal-Oriented Action Planning) system.
GOAP Benefits:
- Agents plan multi-step sequences to achieve goals
- Goals are dynamically prioritized based on state
- More emergent and adaptive behavior
- Easier to extend with new goals and actions
Major features:
- Each agent has unique personality traits affecting all decisions
- Emergent professions: Hunters, Gatherers, Traders, Generalists
- Class inequality through varied strategies and skills
- Traders focus on arbitrage (buy low, sell high)
- Personality affects: risk tolerance, hoarding, market participation
"""
from dataclasses import dataclass, field
from typing import Optional, TYPE_CHECKING
from backend.domain.agent import Agent
from backend.domain.action import ActionType, ACTION_CONFIG
from backend.domain.resources import ResourceType
if TYPE_CHECKING:
from backend.core.market import OrderBook
@dataclass
class TradeItem:
"""A single item to buy/sell in a trade."""
order_id: str
resource_type: ResourceType
quantity: int
price_per_unit: int
@dataclass
class AIDecision:
"""A decision made by the AI for an agent."""
action: ActionType
target_resource: Optional[ResourceType] = None
order_id: Optional[str] = None
quantity: int = 1
price: int = 0
reason: str = ""
# For multi-item trades
trade_items: list[TradeItem] = field(default_factory=list)
# For price adjustments
adjust_order_id: Optional[str] = None
new_price: Optional[int] = None
def to_dict(self) -> dict:
return {
"action": self.action.value,
"target_resource": self.target_resource.value if self.target_resource else None,
"order_id": self.order_id,
"quantity": self.quantity,
"price": self.price,
"reason": self.reason,
"trade_items": [
{
"order_id": t.order_id,
"resource_type": t.resource_type.value,
"quantity": t.quantity,
"price_per_unit": t.price_per_unit,
}
for t in self.trade_items
],
"adjust_order_id": self.adjust_order_id,
"new_price": self.new_price,
}
# Resource to action for gathering
RESOURCE_ACTIONS: dict[ResourceType, ActionType] = {
ResourceType.MEAT: ActionType.HUNT,
ResourceType.BERRIES: ActionType.GATHER,
ResourceType.WATER: ActionType.GET_WATER,
ResourceType.WOOD: ActionType.CHOP_WOOD,
ResourceType.HIDE: ActionType.HUNT,
ResourceType.CLOTHES: ActionType.WEAVE,
}
# Energy cost to gather each resource (used for efficiency calculations)
def get_energy_cost(resource_type: ResourceType) -> int:
"""Get the energy cost to produce one unit of a resource."""
action = RESOURCE_ACTIONS.get(resource_type)
if not action:
return 10
config = ACTION_CONFIG.get(action)
if not config:
return 10
energy_cost = abs(config.energy_cost)
avg_output = max(1, (config.min_output + config.max_output) / 2) if config.output_resource else 1
return int(energy_cost / avg_output)
# Cached config values to avoid repeated lookups
_cached_ai_config = None
_cached_economy_config = None
def _get_ai_config():
"""Get AI-relevant configuration values (cached)."""
global _cached_ai_config
if _cached_ai_config is None:
from backend.config import get_config
_cached_ai_config = get_config().agent_stats
return _cached_ai_config
def _get_economy_config():
"""Get economy/market configuration values (cached)."""
global _cached_economy_config
if _cached_economy_config is None:
from backend.config import get_config
_cached_economy_config = getattr(get_config(), 'economy', None)
return _cached_economy_config
def reset_ai_config_cache():
"""Reset the cached config values (call after config reload)."""
global _cached_ai_config, _cached_economy_config
_cached_ai_config = None
_cached_economy_config = None
def get_ai_decision(
agent: Agent,
market: "OrderBook",
step_in_day: int = 1,
day_steps: int = 10,
current_turn: int = 0,
is_night: bool = False,
) -> AIDecision:
"""Get an AI decision for an agent using GOAP (Goal-Oriented Action Planning).
Args:
agent: The agent to make a decision for
market: The market order book
step_in_day: Current step within the day
day_steps: Total steps per day
current_turn: Current simulation turn
is_night: Whether it's currently night time
Returns:
AIDecision with the chosen action and parameters
"""
from backend.core.goap.goap_ai import get_goap_decision
return get_goap_decision(
agent=agent,
market=market,
step_in_day=step_in_day,
day_steps=day_steps,
current_turn=current_turn,
is_night=is_night,
)