"""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 = 1.5 # 1 energy ≈ 1.5 coins # 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 = 50 # 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 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) # Simulation control auto_step_interval: float = 1.0 # Seconds between auto steps def to_dict(self) -> dict: """Convert to dictionary.""" return { "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( 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