302 lines
9.1 KiB
Python
302 lines
9.1 KiB
Python
"""AI decision system for agents in the Village Simulation.
|
|
|
|
This module provides the main entry point for AI decisions, supporting
|
|
both GOAP (Goal-Oriented Action Planning) and BDI (Belief-Desire-Intention)
|
|
reasoning systems.
|
|
|
|
AI System Options:
|
|
- GOAP: Fast, reactive planning with goal prioritization
|
|
- BDI: Persistent beliefs, long-term desires, plan commitment
|
|
|
|
Configure via config.json "ai.use_bdi" (default: false for backward compatibility)
|
|
|
|
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
|
|
# GOAP/BDI fields
|
|
goal_name: str = ""
|
|
plan_length: int = 0
|
|
bdi_info: dict = field(default_factory=dict)
|
|
|
|
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,
|
|
"goal_name": self.goal_name,
|
|
"plan_length": self.plan_length,
|
|
"bdi_info": self.bdi_info,
|
|
}
|
|
|
|
|
|
# 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(round(energy_cost / avg_output))
|
|
|
|
|
|
# Cached config values to avoid repeated lookups
|
|
_cached_ai_config = None
|
|
_cached_economy_config = None
|
|
_cached_use_bdi = 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 _should_use_bdi() -> bool:
|
|
"""Check if BDI should be used (cached)."""
|
|
global _cached_use_bdi
|
|
if _cached_use_bdi is None:
|
|
from backend.config import get_config
|
|
config = get_config()
|
|
ai_config = getattr(config, 'ai', None)
|
|
_cached_use_bdi = getattr(ai_config, 'use_bdi', False) if ai_config else False
|
|
return _cached_use_bdi
|
|
|
|
|
|
def reset_ai_config_cache():
|
|
"""Reset the cached config values (call after config reload)."""
|
|
global _cached_ai_config, _cached_economy_config, _cached_use_bdi
|
|
_cached_ai_config = None
|
|
_cached_economy_config = None
|
|
_cached_use_bdi = None
|
|
|
|
# Also reset BDI state if it was being used
|
|
try:
|
|
from backend.core.bdi import reset_bdi_state
|
|
reset_bdi_state()
|
|
except ImportError:
|
|
pass
|
|
|
|
|
|
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.
|
|
|
|
Uses either GOAP or BDI based on config setting "ai.use_bdi".
|
|
|
|
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
|
|
"""
|
|
if _should_use_bdi():
|
|
return _get_bdi_decision(
|
|
agent=agent,
|
|
market=market,
|
|
step_in_day=step_in_day,
|
|
day_steps=day_steps,
|
|
current_turn=current_turn,
|
|
is_night=is_night,
|
|
)
|
|
else:
|
|
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,
|
|
)
|
|
|
|
|
|
def _get_goap_decision(
|
|
agent: Agent,
|
|
market: "OrderBook",
|
|
step_in_day: int,
|
|
day_steps: int,
|
|
current_turn: int,
|
|
is_night: bool,
|
|
) -> AIDecision:
|
|
"""Get an AI decision using GOAP (Goal-Oriented Action Planning)."""
|
|
from backend.core.goap.goap_ai import get_goap_decision
|
|
|
|
goap_decision = 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,
|
|
)
|
|
|
|
# Convert GOAP AIDecision to our AIDecision (they should be compatible)
|
|
return AIDecision(
|
|
action=goap_decision.action,
|
|
target_resource=goap_decision.target_resource,
|
|
order_id=goap_decision.order_id,
|
|
quantity=goap_decision.quantity,
|
|
price=goap_decision.price,
|
|
reason=goap_decision.reason,
|
|
trade_items=[
|
|
TradeItem(
|
|
order_id=t.order_id,
|
|
resource_type=t.resource_type,
|
|
quantity=t.quantity,
|
|
price_per_unit=t.price_per_unit,
|
|
)
|
|
for t in goap_decision.trade_items
|
|
],
|
|
adjust_order_id=goap_decision.adjust_order_id,
|
|
new_price=goap_decision.new_price,
|
|
goal_name=goap_decision.goal_name,
|
|
plan_length=goap_decision.plan_length,
|
|
)
|
|
|
|
|
|
def _get_bdi_decision(
|
|
agent: Agent,
|
|
market: "OrderBook",
|
|
step_in_day: int,
|
|
day_steps: int,
|
|
current_turn: int,
|
|
is_night: bool,
|
|
) -> AIDecision:
|
|
"""Get an AI decision using BDI (Belief-Desire-Intention)."""
|
|
from backend.core.bdi import get_bdi_decision
|
|
|
|
bdi_decision = get_bdi_decision(
|
|
agent=agent,
|
|
market=market,
|
|
step_in_day=step_in_day,
|
|
day_steps=day_steps,
|
|
current_turn=current_turn,
|
|
is_night=is_night,
|
|
)
|
|
|
|
# Convert BDI AIDecision to our AIDecision
|
|
return AIDecision(
|
|
action=bdi_decision.action,
|
|
target_resource=bdi_decision.target_resource,
|
|
order_id=bdi_decision.order_id,
|
|
quantity=bdi_decision.quantity,
|
|
price=bdi_decision.price,
|
|
reason=bdi_decision.reason,
|
|
trade_items=[
|
|
TradeItem(
|
|
order_id=t.order_id,
|
|
resource_type=t.resource_type,
|
|
quantity=t.quantity,
|
|
price_per_unit=t.price_per_unit,
|
|
)
|
|
for t in bdi_decision.trade_items
|
|
],
|
|
adjust_order_id=bdi_decision.adjust_order_id,
|
|
new_price=bdi_decision.new_price,
|
|
goal_name=bdi_decision.goal_name,
|
|
plan_length=bdi_decision.plan_length,
|
|
bdi_info=bdi_decision.bdi_info,
|
|
)
|
|
|
|
|
|
def on_agent_death(agent_id: str) -> None:
|
|
"""Clean up AI state when an agent dies.
|
|
|
|
Call this from the engine when an agent is removed.
|
|
"""
|
|
if _should_use_bdi():
|
|
try:
|
|
from backend.core.bdi import remove_agent_bdi_state
|
|
remove_agent_bdi_state(agent_id)
|
|
except ImportError:
|
|
pass
|