"""World State representation for GOAP planning. The WorldState is a symbolic representation of the agent's current situation, used by the planner to reason about actions and goals. """ from dataclasses import dataclass, field from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from backend.domain.agent import Agent from backend.core.market import OrderBook @dataclass class WorldState: """Symbolic representation of the world from an agent's perspective. This captures all relevant state needed for GOAP planning: - Agent vital stats (as percentages 0-1) - Resource counts in inventory - Market availability - Economic state - Time of day The state uses normalized values (0-1) for stats to make threshold comparisons easy and consistent. """ # Vital stats as percentages (0.0 to 1.0) thirst_pct: float = 1.0 hunger_pct: float = 1.0 heat_pct: float = 1.0 energy_pct: float = 1.0 # Resource counts in inventory water_count: int = 0 food_count: int = 0 # meat + berries meat_count: int = 0 berries_count: int = 0 wood_count: int = 0 hide_count: int = 0 # Inventory state has_clothes: bool = False inventory_space: int = 0 inventory_full: bool = False # Economic state money: int = 0 is_wealthy: bool = False # Has comfortable money reserves # Market availability (can we buy these?) can_buy_water: bool = False can_buy_food: bool = False can_buy_meat: bool = False can_buy_berries: bool = False can_buy_wood: bool = False water_market_price: int = 0 food_market_price: int = 0 # Cheapest of meat/berries wood_market_price: int = 0 # Time state is_night: bool = False is_evening: bool = False # Near end of day step_in_day: int = 0 day_steps: int = 10 # Agent personality shortcuts (affect goal priorities) wealth_desire: float = 0.5 hoarding_rate: float = 0.5 risk_tolerance: float = 0.5 market_affinity: float = 0.5 is_trader: bool = False # Critical thresholds (from config) critical_threshold: float = 0.25 low_threshold: float = 0.45 # Calculated urgencies (how urgent is each need?) thirst_urgency: float = 0.0 hunger_urgency: float = 0.0 heat_urgency: float = 0.0 energy_urgency: float = 0.0 def __post_init__(self): """Calculate urgencies after initialization.""" self._calculate_urgencies() def _calculate_urgencies(self): """Calculate urgency values for each vital stat. Urgency is 0 when stat is full, and increases as stat decreases. Urgency > 1.0 when in critical range. """ # Urgency increases as stat decreases # 0.0 = no urgency, 1.0 = needs attention, 2.0+ = critical def calc_urgency(pct: float, critical: float, low: float) -> float: if pct >= low: return 0.0 elif pct >= critical: # Linear increase from 0 to 1 as we go from low to critical return 1.0 - (pct - critical) / (low - critical) else: # Exponential increase below critical return 1.0 + (critical - pct) / critical * 2.0 self.thirst_urgency = calc_urgency(self.thirst_pct, self.critical_threshold, self.low_threshold) self.hunger_urgency = calc_urgency(self.hunger_pct, self.critical_threshold, self.low_threshold) self.heat_urgency = calc_urgency(self.heat_pct, self.critical_threshold, self.low_threshold) # Energy urgency is different - we care about absolute level for work if self.energy_pct < 0.25: self.energy_urgency = 2.0 elif self.energy_pct < 0.40: self.energy_urgency = 1.0 else: self.energy_urgency = 0.0 def copy(self) -> "WorldState": """Create a copy of this world state.""" return WorldState( thirst_pct=self.thirst_pct, hunger_pct=self.hunger_pct, heat_pct=self.heat_pct, energy_pct=self.energy_pct, water_count=self.water_count, food_count=self.food_count, meat_count=self.meat_count, berries_count=self.berries_count, wood_count=self.wood_count, hide_count=self.hide_count, has_clothes=self.has_clothes, inventory_space=self.inventory_space, inventory_full=self.inventory_full, money=self.money, is_wealthy=self.is_wealthy, can_buy_water=self.can_buy_water, can_buy_food=self.can_buy_food, can_buy_meat=self.can_buy_meat, can_buy_berries=self.can_buy_berries, can_buy_wood=self.can_buy_wood, water_market_price=self.water_market_price, food_market_price=self.food_market_price, wood_market_price=self.wood_market_price, is_night=self.is_night, is_evening=self.is_evening, step_in_day=self.step_in_day, day_steps=self.day_steps, wealth_desire=self.wealth_desire, hoarding_rate=self.hoarding_rate, risk_tolerance=self.risk_tolerance, market_affinity=self.market_affinity, is_trader=self.is_trader, critical_threshold=self.critical_threshold, low_threshold=self.low_threshold, ) def to_dict(self) -> dict: """Convert to dictionary for debugging/logging.""" return { "vitals": { "thirst": round(self.thirst_pct, 2), "hunger": round(self.hunger_pct, 2), "heat": round(self.heat_pct, 2), "energy": round(self.energy_pct, 2), }, "urgencies": { "thirst": round(self.thirst_urgency, 2), "hunger": round(self.hunger_urgency, 2), "heat": round(self.heat_urgency, 2), "energy": round(self.energy_urgency, 2), }, "inventory": { "water": self.water_count, "meat": self.meat_count, "berries": self.berries_count, "wood": self.wood_count, "hide": self.hide_count, "space": self.inventory_space, }, "economy": { "money": self.money, "is_wealthy": self.is_wealthy, }, "market": { "can_buy_water": self.can_buy_water, "can_buy_food": self.can_buy_food, "can_buy_wood": self.can_buy_wood, }, "time": { "is_night": self.is_night, "is_evening": self.is_evening, "step": self.step_in_day, }, } def create_world_state( agent: "Agent", market: "OrderBook", step_in_day: int = 1, day_steps: int = 10, is_night: bool = False, ) -> WorldState: """Create a WorldState from an agent and market. This is the main factory function for creating world states. It extracts all relevant information from the agent and market. """ from backend.domain.resources import ResourceType from backend.config import get_config config = get_config() agent_config = config.agent_stats economy_config = getattr(config, 'economy', None) stats = agent.stats # Calculate stat percentages thirst_pct = stats.thirst / stats.MAX_THIRST hunger_pct = stats.hunger / stats.MAX_HUNGER heat_pct = stats.heat / stats.MAX_HEAT energy_pct = stats.energy / stats.MAX_ENERGY # Get resource counts water_count = agent.get_resource_count(ResourceType.WATER) meat_count = agent.get_resource_count(ResourceType.MEAT) berries_count = agent.get_resource_count(ResourceType.BERRIES) wood_count = agent.get_resource_count(ResourceType.WOOD) hide_count = agent.get_resource_count(ResourceType.HIDE) food_count = meat_count + berries_count # Check market availability def get_market_info(resource_type: ResourceType) -> tuple[bool, int]: """Get market availability and price for a resource.""" order = market.get_cheapest_order(resource_type) if order and order.seller_id != agent.id and agent.money >= order.price_per_unit: return True, order.price_per_unit return False, 0 can_buy_water, water_price = get_market_info(ResourceType.WATER) can_buy_meat, meat_price = get_market_info(ResourceType.MEAT) can_buy_berries, berries_price = get_market_info(ResourceType.BERRIES) can_buy_wood, wood_price = get_market_info(ResourceType.WOOD) # Can buy food if we can buy either meat or berries can_buy_food = can_buy_meat or can_buy_berries food_price = min( meat_price if can_buy_meat else float('inf'), berries_price if can_buy_berries else float('inf') ) food_price = food_price if food_price != float('inf') else 0 # Wealth calculation min_wealth_target = getattr(economy_config, 'min_wealth_target', 50) if economy_config else 50 wealth_target = int(min_wealth_target * (0.5 + agent.personality.wealth_desire)) is_wealthy = agent.money >= wealth_target # Trader check is_trader = agent.personality.trade_preference > 1.3 and agent.personality.market_affinity > 0.5 # Evening check (last 2 steps before night) is_evening = step_in_day >= day_steps - 2 return WorldState( thirst_pct=thirst_pct, hunger_pct=hunger_pct, heat_pct=heat_pct, energy_pct=energy_pct, water_count=water_count, food_count=food_count, meat_count=meat_count, berries_count=berries_count, wood_count=wood_count, hide_count=hide_count, has_clothes=agent.has_clothes(), inventory_space=agent.inventory_space(), inventory_full=agent.inventory_full(), money=agent.money, is_wealthy=is_wealthy, can_buy_water=can_buy_water, can_buy_food=can_buy_food, can_buy_meat=can_buy_meat, can_buy_berries=can_buy_berries, can_buy_wood=can_buy_wood, water_market_price=water_price, food_market_price=int(food_price), wood_market_price=wood_price, is_night=is_night, is_evening=is_evening, step_in_day=step_in_day, day_steps=day_steps, wealth_desire=agent.personality.wealth_desire, hoarding_rate=agent.personality.hoarding_rate, risk_tolerance=agent.personality.risk_tolerance, market_affinity=agent.personality.market_affinity, is_trader=is_trader, critical_threshold=agent_config.critical_threshold, low_threshold=0.45, # Could also be in config )