304 lines
9.5 KiB
Python
304 lines
9.5 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
|
|
max_heat: int = 100
|
|
max_faith: int = 100 # NEW: Religious faith level
|
|
|
|
# Starting values
|
|
start_energy: int = 50
|
|
start_hunger: int = 80
|
|
start_thirst: int = 80
|
|
start_heat: int = 100
|
|
start_faith: int = 50 # NEW: Start with moderate faith
|
|
|
|
# Decay rates per turn
|
|
energy_decay: int = 2
|
|
hunger_decay: int = 2
|
|
thirst_decay: int = 2
|
|
heat_decay: int = 2
|
|
faith_decay: int = 1 # NEW: Faith decays slowly without religious activity
|
|
|
|
# Thresholds
|
|
critical_threshold: float = 0.25
|
|
low_energy_threshold: int = 15
|
|
|
|
|
|
@dataclass
|
|
class ResourceConfig:
|
|
"""Configuration for resource properties."""
|
|
# Decay rates (turns until spoilage, 0 = infinite)
|
|
meat_decay: int = 8
|
|
berries_decay: int = 25
|
|
clothes_decay: int = 50
|
|
oil_decay: int = 0 # NEW: Oil doesn't decay
|
|
fuel_decay: int = 0 # NEW: Refined fuel doesn't decay
|
|
|
|
# Resource effects
|
|
meat_hunger: int = 30
|
|
meat_energy: int = 5
|
|
berries_hunger: int = 8
|
|
berries_thirst: int = 3
|
|
water_thirst: int = 50
|
|
fire_heat: int = 15
|
|
fuel_heat: int = 35 # NEW: Fuel provides more heat than wood
|
|
oil_energy: int = 0 # NEW: Raw oil has no direct use
|
|
fuel_energy: int = 8 # NEW: Refined fuel provides energy
|
|
|
|
|
|
@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
|
|
|
|
# NEW: Oil industry actions
|
|
drill_oil_energy: int = -10
|
|
refine_energy: int = -8
|
|
|
|
# NEW: Religious actions
|
|
pray_energy: int = -2
|
|
preach_energy: int = -4
|
|
|
|
# NEW: Diplomatic actions
|
|
negotiate_energy: int = -3
|
|
declare_war_energy: int = -5
|
|
make_peace_energy: int = -3
|
|
|
|
# Success chances (0.0 to 1.0)
|
|
hunt_success: float = 0.7
|
|
chop_wood_success: float = 0.9
|
|
drill_oil_success: float = 0.6 # NEW: Harder to extract oil
|
|
|
|
# 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
|
|
|
|
# NEW: Oil output
|
|
drill_oil_min: int = 1
|
|
drill_oil_max: int = 3
|
|
|
|
# NEW: Religious action effects
|
|
pray_faith_gain: int = 25
|
|
preach_faith_spread: int = 15
|
|
preach_convert_chance: float = 0.15
|
|
|
|
|
|
@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
|
|
|
|
# NEW: World features
|
|
oil_fields_count: int = 3 # Number of oil field locations
|
|
temple_count: int = 2 # Number of temple/religious locations
|
|
|
|
|
|
@dataclass
|
|
class MarketConfig:
|
|
"""Configuration for market behavior."""
|
|
turns_before_discount: int = 3
|
|
discount_rate: float = 0.15
|
|
base_price_multiplier: float = 1.2
|
|
|
|
|
|
@dataclass
|
|
class EconomyConfig:
|
|
"""Configuration for economic behavior and agent trading."""
|
|
energy_to_money_ratio: float = 1.5
|
|
wealth_desire: float = 0.3
|
|
buy_efficiency_threshold: float = 0.7
|
|
min_wealth_target: int = 50
|
|
max_price_markup: float = 2.0
|
|
min_price_discount: float = 0.5
|
|
|
|
# NEW: Oil economy
|
|
oil_base_price: int = 25 # Oil is valuable
|
|
fuel_base_price: int = 40 # Refined fuel is more valuable
|
|
|
|
|
|
@dataclass
|
|
class ReligionConfig:
|
|
"""Configuration for religion system."""
|
|
num_religions: int = 3 # Number of different religions
|
|
conversion_resistance: float = 0.5 # How hard to convert agents
|
|
zealot_threshold: float = 0.80 # Faith level for zealot behavior
|
|
faith_trade_bonus: float = 0.10 # Bonus when trading with same religion
|
|
same_religion_bonus: float = 0.15 # General bonus with same religion
|
|
different_religion_penalty: float = 0.10 # Penalty with different religion
|
|
holy_war_threshold: float = 0.90 # Faith level to trigger religious conflict
|
|
|
|
|
|
@dataclass
|
|
class DiplomacyConfig:
|
|
"""Configuration for diplomacy and faction system."""
|
|
num_factions: int = 4 # Number of factions
|
|
starting_relations: int = 50 # Neutral starting relations (0-100)
|
|
alliance_threshold: int = 75 # Relations needed for alliance
|
|
war_threshold: int = 25 # Relations below this = hostile
|
|
relation_decay: int = 1 # Relations decay towards neutral
|
|
trade_relation_boost: int = 2 # Trading improves relations
|
|
war_damage_multiplier: float = 1.5 # Extra damage during war
|
|
peace_treaty_duration: int = 20 # Turns peace treaty lasts
|
|
war_exhaustion_rate: int = 2 # How fast war exhaustion builds
|
|
|
|
|
|
@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)
|
|
religion: ReligionConfig = field(default_factory=ReligionConfig) # NEW
|
|
diplomacy: DiplomacyConfig = field(default_factory=DiplomacyConfig) # NEW
|
|
|
|
# Simulation control
|
|
auto_step_interval: float = 1.0
|
|
|
|
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),
|
|
"religion": asdict(self.religion),
|
|
"diplomacy": asdict(self.diplomacy),
|
|
"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", {})),
|
|
religion=ReligionConfig(**data.get("religion", {})),
|
|
diplomacy=DiplomacyConfig(**data.get("diplomacy", {})),
|
|
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()
|
|
|
|
|
|
# Global configuration instance
|
|
_config: Optional[SimulationConfig] = None
|
|
|
|
|
|
def get_config() -> SimulationConfig:
|
|
"""Get the global configuration instance."""
|
|
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():
|
|
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()
|
|
|
|
|
|
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
|