"""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 (GOAP-based).""" # 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 AgeConfig: """Configuration for the age and lifecycle system. Age affects skills, energy costs, and creates birth/death cycles. Age is measured in "years" where 1 year = 1 simulation day. Population is controlled by economy: - Birth rate scales with village prosperity (food availability) - Parents transfer wealth to children at birth and death """ # Starting age range for initial agents min_start_age: int = 18 max_start_age: int = 35 # Age category thresholds young_age_threshold: int = 25 # Below this = young prime_age_start: int = 25 # Prime age begins prime_age_end: int = 50 # Prime age ends old_age_threshold: int = 50 # Above this = old # Lifespan base_max_age: int = 75 # Base maximum age max_age_variance: int = 10 # ± variance for max age age_per_day: int = 1 # How many "years" per sim day # Birth mechanics - economy controlled birth_cooldown_days: int = 20 # Days after birth before can birth again min_birth_age: int = 20 # Minimum age to give birth max_birth_age: int = 45 # Maximum age to give birth birth_base_chance: float = 0.02 # Base chance of birth per day birth_prosperity_multiplier: float = 3.0 # Max multiplier based on food abundance birth_food_requirement: int = 60 # Min hunger to attempt birth birth_energy_requirement: int = 25 # Min energy to attempt birth # Wealth transfer birth_wealth_transfer: float = 0.25 # Parent gives 25% wealth to child at birth inheritance_enabled: bool = True # Children inherit from dead parents child_start_age: int = 18 # Age children start at (adult) # Age modifiers for YOUNG agents (learning phase) young_skill_multiplier: float = 0.8 # Skills are 80% effective young_learning_multiplier: float = 1.4 # Learn 40% faster young_energy_cost_multiplier: float = 0.85 # 15% less energy cost # Age modifiers for PRIME agents (peak performance) prime_skill_multiplier: float = 1.0 prime_learning_multiplier: float = 1.0 prime_energy_cost_multiplier: float = 1.0 # Age modifiers for OLD agents (wisdom but frailty) old_skill_multiplier: float = 1.15 # Skills 15% more effective (wisdom) old_learning_multiplier: float = 0.6 # Learn 40% slower old_energy_cost_multiplier: float = 1.2 # 20% more energy cost old_max_energy_multiplier: float = 0.75 # 25% less max energy old_decay_multiplier: float = 1.15 # 15% faster stat decay @dataclass class StorageConfig: """Configuration for resource storage limits. Limits the total resources that can exist in the village economy. """ # Village-wide storage limits per resource type village_meat_limit: int = 100 village_berries_limit: int = 150 village_water_limit: int = 200 village_wood_limit: int = 200 village_hide_limit: int = 80 village_clothes_limit: int = 50 # Market limits market_order_limit_per_agent: int = 5 # Max active orders per agent market_total_order_limit: int = 500 # Max total market orders @dataclass class SinksConfig: """Configuration for resource sinks (ways resources leave the economy). These create pressure to keep producing resources rather than hoarding. """ # Daily decay of village storage (percentage) daily_village_decay_rate: float = 0.02 # 2% of stored resources decay daily # Money tax (redistributed or removed) daily_tax_rate: float = 0.01 # 1% wealth tax per day # Random events random_event_chance: float = 0.05 # 5% chance of event per day fire_event_resource_loss: float = 0.1 # 10% resources lost in fire theft_event_money_loss: float = 0.05 # 5% money stolen # Maintenance costs clothes_maintenance_per_day: int = 1 # Clothes degrade 1 durability/day fire_wood_cost_per_night: int = 1 # Wood consumed to stay warm at night @dataclass class PerformanceConfig: """Configuration for performance optimization. Controls logging and memory usage to keep simulation fast at high turn counts. """ # Logging control logging_enabled: bool = False # Enable file logging (disable for speed) detailed_logging: bool = False # Enable verbose per-agent logging log_flush_interval: int = 50 # Flush logs every N turns (not every turn) # Memory management max_turn_logs: int = 100 # Keep only last N turn logs in memory # Statistics calculation frequency stats_update_interval: int = 10 # Update expensive stats every N turns @dataclass class SimulationConfig: """Master configuration containing all sub-configs.""" performance: PerformanceConfig = field(default_factory=PerformanceConfig) 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) age: AgeConfig = field(default_factory=AgeConfig) storage: StorageConfig = field(default_factory=StorageConfig) sinks: SinksConfig = field(default_factory=SinksConfig) # Simulation control auto_step_interval: float = 1.0 # Seconds between auto steps def to_dict(self) -> dict: """Convert to dictionary.""" return { "performance": asdict(self.performance), "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), "age": asdict(self.age), "storage": asdict(self.storage), "sinks": asdict(self.sinks), "auto_step_interval": self.auto_step_interval, } @classmethod def from_dict(cls, data: dict) -> "SimulationConfig": """Create from dictionary.""" return cls( performance=PerformanceConfig(**data.get("performance", {})), 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", {})), age=AgeConfig(**data.get("age", {})), storage=StorageConfig(**data.get("storage", {})), sinks=SinksConfig(**data.get("sinks", {})), 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 try: from backend.core.ai import reset_ai_config_cache reset_ai_config_cache() except ImportError: pass