"""Personality and skill system for agents in the Village Simulation. Each agent has unique personality traits that affect their behavior: - How much they value wealth vs. survival resources - What activities they prefer (hunting, gathering, trading) - How willing they are to take risks - How much they hoard vs. trade Agents also develop skills over time based on their actions: - Skills improve with practice - Better skills = better outcomes This creates emergent professions: - Hunters: High hunting skill, prefer meat production - Gatherers: High gathering skill, prefer berries/water/wood - Traders: High trading skill, focus on buy low / sell high arbitrage """ import random from dataclasses import dataclass, field from enum import Enum from typing import Optional class ProfessionType(Enum): """Emergent profession types based on behavior patterns.""" HUNTER = "hunter" GATHERER = "gatherer" WOODCUTTER = "woodcutter" TRADER = "trader" GENERALIST = "generalist" @dataclass class PersonalityTraits: """Unique personality traits that affect agent behavior. These are set at birth and don't change during the agent's life. They create natural diversity in the population. """ # How much the agent values accumulating wealth (0.1 = minimal, 0.9 = greedy) wealth_desire: float = 0.3 # How much the agent hoards resources vs trades them (0.1 = trades freely, 0.9 = hoards) hoarding_rate: float = 0.5 # Willingness to take risks (0.1 = very cautious, 0.9 = risk-taker) # Affects: hunting vs gathering preference, price decisions risk_tolerance: float = 0.5 # Sensitivity to good/bad deals (0.5 = not picky, 1.5 = very price conscious) price_sensitivity: float = 1.0 # Activity biases - how much the agent prefers each activity # Higher values = more likely to choose this activity # These create "profession tendencies" hunt_preference: float = 1.0 # Preference for hunting gather_preference: float = 1.0 # Preference for gathering woodcut_preference: float = 1.0 # Preference for wood trade_preference: float = 1.0 # Preference for trading/market # How social/market-oriented the agent is # High = frequent market visits, buys more from others # Low = self-sufficient, prefers to produce own resources market_affinity: float = 0.5 def to_dict(self) -> dict: return { "wealth_desire": round(self.wealth_desire, 2), "hoarding_rate": round(self.hoarding_rate, 2), "risk_tolerance": round(self.risk_tolerance, 2), "price_sensitivity": round(self.price_sensitivity, 2), "hunt_preference": round(self.hunt_preference, 2), "gather_preference": round(self.gather_preference, 2), "woodcut_preference": round(self.woodcut_preference, 2), "trade_preference": round(self.trade_preference, 2), "market_affinity": round(self.market_affinity, 2), } @dataclass class Skills: """Skills that improve with practice. Each skill affects the outcome of related actions. Skills increase slowly through practice (use it or lose it). """ # Combat/hunting skill - affects hunt success rate hunting: float = 1.0 # Foraging skill - affects gather output quantity gathering: float = 1.0 # Woodcutting skill - affects wood output woodcutting: float = 1.0 # Trading skill - affects prices (buy lower, sell higher) trading: float = 1.0 # Crafting skill - affects craft quality/success crafting: float = 1.0 # Skill improvement rate per action IMPROVEMENT_RATE: float = 0.02 # Skill decay rate per turn (use it or lose it, gentle decay) DECAY_RATE: float = 0.001 # Maximum skill level MAX_SKILL: float = 2.0 # Minimum skill level MIN_SKILL: float = 0.5 def improve(self, skill_name: str, amount: Optional[float] = None) -> None: """Improve a skill through practice.""" if amount is None: amount = self.IMPROVEMENT_RATE if hasattr(self, skill_name): current = getattr(self, skill_name) new_value = min(self.MAX_SKILL, current + amount) setattr(self, skill_name, new_value) def decay_all(self) -> None: """Apply gentle decay to all skills (use it or lose it).""" for skill_name in ['hunting', 'gathering', 'woodcutting', 'trading', 'crafting']: current = getattr(self, skill_name) new_value = max(self.MIN_SKILL, current - self.DECAY_RATE) setattr(self, skill_name, new_value) def get_primary_skill(self) -> tuple[str, float]: """Get the agent's highest skill and its name.""" skills = { 'hunting': self.hunting, 'gathering': self.gathering, 'woodcutting': self.woodcutting, 'trading': self.trading, 'crafting': self.crafting, } best_skill = max(skills, key=skills.get) return best_skill, skills[best_skill] def to_dict(self) -> dict: return { "hunting": round(self.hunting, 3), "gathering": round(self.gathering, 3), "woodcutting": round(self.woodcutting, 3), "trading": round(self.trading, 3), "crafting": round(self.crafting, 3), } def generate_random_personality(archetype: Optional[str] = None) -> PersonalityTraits: """Generate random personality traits. If archetype is specified, traits will be biased towards that profession: - "hunter": High risk tolerance, high hunt preference - "gatherer": Low risk tolerance, high gather preference - "trader": High wealth desire, high market affinity, high trade preference - "hoarder": High hoarding rate, low market affinity - None: Fully random Returns a PersonalityTraits instance with randomized values. """ # Start with base random values traits = PersonalityTraits( wealth_desire=random.uniform(0.1, 0.9), hoarding_rate=random.uniform(0.2, 0.8), risk_tolerance=random.uniform(0.2, 0.8), price_sensitivity=random.uniform(0.6, 1.4), hunt_preference=random.uniform(0.5, 1.5), gather_preference=random.uniform(0.5, 1.5), woodcut_preference=random.uniform(0.5, 1.5), trade_preference=random.uniform(0.5, 1.5), market_affinity=random.uniform(0.2, 0.8), ) # Apply archetype biases if archetype == "hunter": traits.hunt_preference = random.uniform(1.3, 2.0) traits.risk_tolerance = random.uniform(0.6, 0.9) traits.gather_preference = random.uniform(0.3, 0.7) elif archetype == "gatherer": traits.gather_preference = random.uniform(1.3, 2.0) traits.risk_tolerance = random.uniform(0.2, 0.5) traits.hunt_preference = random.uniform(0.3, 0.7) elif archetype == "trader": traits.trade_preference = random.uniform(1.5, 2.5) traits.market_affinity = random.uniform(0.7, 0.95) traits.wealth_desire = random.uniform(0.6, 0.95) traits.price_sensitivity = random.uniform(1.1, 1.6) traits.hoarding_rate = random.uniform(0.1, 0.4) # Traders sell! # Traders don't hunt/gather much traits.hunt_preference = random.uniform(0.2, 0.5) traits.gather_preference = random.uniform(0.2, 0.5) elif archetype == "hoarder": traits.hoarding_rate = random.uniform(0.7, 0.95) traits.market_affinity = random.uniform(0.1, 0.4) traits.trade_preference = random.uniform(0.3, 0.7) elif archetype == "woodcutter": traits.woodcut_preference = random.uniform(1.3, 2.0) traits.gather_preference = random.uniform(0.5, 0.8) return traits def generate_random_skills(personality: PersonalityTraits) -> Skills: """Generate starting skills influenced by personality. Agents with strong preferences start with slightly better skills in those areas (natural talent). """ # Base skill level with small random variation base = 1.0 variance = 0.15 skills = Skills( hunting=base + random.uniform(-variance, variance) + (personality.hunt_preference - 1.0) * 0.1, gathering=base + random.uniform(-variance, variance) + (personality.gather_preference - 1.0) * 0.1, woodcutting=base + random.uniform(-variance, variance) + (personality.woodcut_preference - 1.0) * 0.1, trading=base + random.uniform(-variance, variance) + (personality.trade_preference - 1.0) * 0.1, crafting=base + random.uniform(-variance, variance), ) # Clamp all skills to valid range skills.hunting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.hunting)) skills.gathering = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.gathering)) skills.woodcutting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.woodcutting)) skills.trading = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.trading)) skills.crafting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.crafting)) return skills def determine_profession(personality: PersonalityTraits, skills: Skills) -> ProfessionType: """Determine an agent's emergent profession based on traits and skills. This is for display/statistics - it doesn't affect behavior directly. The behavior is determined by the traits and skills themselves. """ # Calculate profession scores scores = { ProfessionType.HUNTER: personality.hunt_preference * skills.hunting * 1.2, ProfessionType.GATHERER: personality.gather_preference * skills.gathering, ProfessionType.WOODCUTTER: personality.woodcut_preference * skills.woodcutting, ProfessionType.TRADER: personality.trade_preference * skills.trading * personality.market_affinity * 1.5, } # Find the best match best_profession = max(scores, key=scores.get) best_score = scores[best_profession] # If no clear winner (all scores similar), they're a generalist second_best = sorted(scores.values(), reverse=True)[1] if best_score < second_best * 1.2: return ProfessionType.GENERALIST return best_profession def get_action_skill_modifier(skill_value: float) -> float: """Convert skill value to action modifier. Skill 0.5 = 0.75x effectiveness Skill 1.0 = 1.0x effectiveness Skill 1.5 = 1.25x effectiveness Skill 2.0 = 1.5x effectiveness This creates meaningful but not overpowering differences. """ # Linear scaling: (skill - 1.0) * 0.5 + 1.0 # So skill 0.5 -> 0.75, skill 1.0 -> 1.0, skill 2.0 -> 1.5 return max(0.5, min(1.5, 0.5 * skill_value + 0.5)) def get_trade_price_modifier(skill_value: float, is_buying: bool) -> float: """Get price modifier for trading based on skill. Higher trading skill = better deals: - When buying: lower prices (modifier < 1) - When selling: higher prices (modifier > 1) Skill 1.0 = no modifier Skill 2.0 = 15% better deals """ modifier = (skill_value - 1.0) * 0.15 if is_buying: return max(0.85, 1.0 - modifier) # Lower is better for buying else: return min(1.15, 1.0 + modifier) # Higher is better for selling