161 lines
5.0 KiB
Python
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,
|
|
)
|