417 lines
14 KiB
Python
417 lines
14 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 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
|
|
|