villsim/backend/config.py
Снесарев Максим 308f738c37 [new] add goap agents
2026-01-19 20:45:35 +03:00

282 lines
8.6 KiB
Python

"""Centralized configuration for the Village Simulation."""
from dataclasses import dataclass, field, asdict
from typing import Optional
import json
from pathlib import Path
@dataclass
class AgentStatsConfig:
"""Configuration for agent vital stats."""
# Maximum values
max_energy: int = 50
max_hunger: int = 100
max_thirst: int = 100 # Increased from 50 to give more buffer
max_heat: int = 100
# Starting values
start_energy: int = 50
start_hunger: int = 80
start_thirst: int = 80 # Increased from 40 to start with more buffer
start_heat: int = 100
# Decay rates per turn
energy_decay: int = 2
hunger_decay: int = 2
thirst_decay: int = 2 # Reduced from 3 to match hunger decay rate
heat_decay: int = 2
# Thresholds
critical_threshold: float = 0.25 # 25% triggers survival mode
low_energy_threshold: int = 15 # Minimum energy to work
@dataclass
class ResourceConfig:
"""Configuration for resource properties."""
# Decay rates (turns until spoilage, 0 = infinite)
meat_decay: int = 8 # Increased from 5 to give more time to use
berries_decay: int = 25
clothes_decay: int = 50
# Resource effects
meat_hunger: int = 30
meat_energy: int = 5
berries_hunger: int = 8 # Increased from 5
berries_thirst: int = 3 # Increased from 2
water_thirst: int = 50 # Increased from 40 for better thirst recovery
fire_heat: int = 15 # Increased from 10
@dataclass
class ActionConfig:
"""Configuration for action costs and outcomes."""
# Energy costs (positive = restore, negative = spend)
sleep_energy: int = 60
rest_energy: int = 10
hunt_energy: int = -15
gather_energy: int = -5
chop_wood_energy: int = -10
get_water_energy: int = -5
weave_energy: int = -8
build_fire_energy: int = -5
trade_energy: int = -1
# Success chances (0.0 to 1.0)
hunt_success: float = 0.7
chop_wood_success: float = 0.9
# Output quantities
hunt_meat_min: int = 1
hunt_meat_max: int = 3
hunt_hide_min: int = 0
hunt_hide_max: int = 1
gather_min: int = 2
gather_max: int = 5
chop_wood_min: int = 1
chop_wood_max: int = 2
@dataclass
class WorldConfig:
"""Configuration for world properties."""
width: int = 20
height: int = 20
initial_agents: int = 8
day_steps: int = 10
night_steps: int = 1
# Agent configuration
inventory_slots: int = 10
starting_money: int = 100
@dataclass
class MarketConfig:
"""Configuration for market behavior."""
turns_before_discount: int = 3
discount_rate: float = 0.15 # 15% discount after waiting
base_price_multiplier: float = 1.2 # Markup over production cost
@dataclass
class EconomyConfig:
"""Configuration for economic behavior and agent trading.
These values control how agents perceive the value of money and trading.
Higher values make agents more trade-oriented.
"""
# How much agents value money vs energy
# Higher = agents see money as more valuable (trade more)
energy_to_money_ratio: float = 150 # 1 energy ≈ 150 coins
# Minimum price floor for any market transaction
min_price: int = 100
# How strongly agents desire wealth (0-1)
# Higher = agents will prioritize building wealth
wealth_desire: float = 0.3
# Buy efficiency threshold (0-1)
# If market price < (threshold * fair_value), buy instead of gather
# 0.7 means: buy if price is 70% or less of the fair value
buy_efficiency_threshold: float = 0.7
# Minimum wealth target - agents want at least this much money
min_wealth_target: int = 5000
# Price adjustment limits
max_price_markup: float = 2.0 # Maximum price = 2x base value
min_price_discount: float = 0.5 # Minimum price = 50% of base value
@dataclass
class AIConfig:
"""Configuration for AI decision-making system.
Controls whether to use GOAP (Goal-Oriented Action Planning) or
the legacy priority-based system.
"""
# Use GOAP-based AI (True) or legacy priority-based AI (False)
use_goap: bool = True
# Maximum A* iterations for GOAP planner
goap_max_iterations: int = 50
# Maximum plan depth (number of actions in a plan)
goap_max_plan_depth: int = 3
# Fall back to reactive planning if GOAP fails to find a plan
reactive_fallback: bool = True
@dataclass
class SimulationConfig:
"""Master configuration containing all sub-configs."""
agent_stats: AgentStatsConfig = field(default_factory=AgentStatsConfig)
resources: ResourceConfig = field(default_factory=ResourceConfig)
actions: ActionConfig = field(default_factory=ActionConfig)
world: WorldConfig = field(default_factory=WorldConfig)
market: MarketConfig = field(default_factory=MarketConfig)
economy: EconomyConfig = field(default_factory=EconomyConfig)
ai: AIConfig = field(default_factory=AIConfig)
# Simulation control
auto_step_interval: float = 1.0 # Seconds between auto steps
def to_dict(self) -> dict:
"""Convert to dictionary."""
return {
"ai": asdict(self.ai),
"agent_stats": asdict(self.agent_stats),
"resources": asdict(self.resources),
"actions": asdict(self.actions),
"world": asdict(self.world),
"market": asdict(self.market),
"economy": asdict(self.economy),
"auto_step_interval": self.auto_step_interval,
}
@classmethod
def from_dict(cls, data: dict) -> "SimulationConfig":
"""Create from dictionary."""
return cls(
ai=AIConfig(**data.get("ai", {})),
agent_stats=AgentStatsConfig(**data.get("agent_stats", {})),
resources=ResourceConfig(**data.get("resources", {})),
actions=ActionConfig(**data.get("actions", {})),
world=WorldConfig(**data.get("world", {})),
market=MarketConfig(**data.get("market", {})),
economy=EconomyConfig(**data.get("economy", {})),
auto_step_interval=data.get("auto_step_interval", 1.0),
)
def save(self, path: str = "config.json") -> None:
"""Save configuration to JSON file."""
with open(path, "w") as f:
json.dump(self.to_dict(), f, indent=2)
@classmethod
def load(cls, path: str = "config.json") -> "SimulationConfig":
"""Load configuration from JSON file."""
try:
with open(path, "r") as f:
data = json.load(f)
return cls.from_dict(data)
except FileNotFoundError:
return cls() # Return defaults if file not found
# Global configuration instance
_config: Optional[SimulationConfig] = None
def get_config() -> SimulationConfig:
"""Get the global configuration instance.
Loads from config.json if not already loaded.
"""
global _config
if _config is None:
_config = load_config()
return _config
def load_config(path: str = "config.json") -> SimulationConfig:
"""Load configuration from JSON file, falling back to defaults."""
try:
config_path = Path(path)
if not config_path.is_absolute():
# Try relative to workspace root (villsim/)
# __file__ is backend/config.py, so .parent.parent is villsim/
workspace_root = Path(__file__).parent.parent
config_path = workspace_root / path
if config_path.exists():
with open(config_path, "r") as f:
data = json.load(f)
return SimulationConfig.from_dict(data)
except (FileNotFoundError, json.JSONDecodeError) as e:
print(f"Warning: Could not load config from {path}: {e}")
return SimulationConfig() # Return defaults if file not found
def set_config(config: SimulationConfig) -> None:
"""Set the global configuration instance."""
global _config
_config = config
def reset_config() -> SimulationConfig:
"""Reset configuration to defaults."""
global _config
_config = SimulationConfig()
_reset_all_caches()
return _config
def reload_config(path: str = "config.json") -> SimulationConfig:
"""Reload configuration from file and reset all caches."""
global _config
_config = load_config(path)
_reset_all_caches()
return _config
def _reset_all_caches() -> None:
"""Reset all module caches that depend on config values."""
try:
from backend.domain.action import reset_action_config_cache
reset_action_config_cache()
except ImportError:
pass
try:
from backend.domain.resources import reset_resource_cache
reset_resource_cache()
except ImportError:
pass