villsim/backend/core/goap/world_state.py
2026-01-19 21:03:30 +03:00

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
)