320 lines
11 KiB
Python
320 lines
11 KiB
Python
"""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
|
|
|
|
# Profession preferences (0.5-1.5 range, higher = more preferred)
|
|
gather_preference: float = 1.0
|
|
hunt_preference: float = 1.0
|
|
trade_preference: float = 1.0
|
|
|
|
# Skill levels (0.0-1.0, higher = more skilled)
|
|
hunting_skill: float = 0.0
|
|
gathering_skill: float = 0.0
|
|
trading_skill: float = 0.0
|
|
|
|
# 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,
|
|
gather_preference=agent.personality.gather_preference,
|
|
hunt_preference=agent.personality.hunt_preference,
|
|
trade_preference=agent.personality.trade_preference,
|
|
hunting_skill=agent.skills.hunting,
|
|
gathering_skill=agent.skills.gathering,
|
|
trading_skill=agent.skills.trading,
|
|
critical_threshold=agent_config.critical_threshold,
|
|
low_threshold=0.45, # Could also be in config
|
|
)
|
|
|