298 lines
11 KiB
Python
298 lines
11 KiB
Python
"""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
|
|
|