[upd] Add birth and age-based death and modifiers
This commit is contained in:
parent
25bd13e001
commit
d190e3efe5
@ -55,9 +55,31 @@ class AgentResponse(BaseModel):
|
|||||||
inventory: list[ResourceSchema]
|
inventory: list[ResourceSchema]
|
||||||
money: int
|
money: int
|
||||||
is_alive: bool
|
is_alive: bool
|
||||||
|
is_corpse: bool = False
|
||||||
can_act: bool
|
can_act: bool
|
||||||
current_action: AgentActionSchema
|
current_action: AgentActionSchema
|
||||||
last_action_result: str
|
last_action_result: str
|
||||||
|
death_turn: int = -1
|
||||||
|
death_reason: str = ""
|
||||||
|
# Age system
|
||||||
|
age: int = 25
|
||||||
|
max_age: int = 70
|
||||||
|
age_category: str = "prime"
|
||||||
|
birth_day: int = 0
|
||||||
|
generation: int = 0
|
||||||
|
parent_ids: list[str] = []
|
||||||
|
children_count: int = 0
|
||||||
|
# Age modifiers
|
||||||
|
skill_modifier: float = 1.0
|
||||||
|
energy_cost_modifier: float = 1.0
|
||||||
|
learning_modifier: float = 1.0
|
||||||
|
# Personality and skills
|
||||||
|
personality: dict = {}
|
||||||
|
skills: dict = {}
|
||||||
|
actions_performed: dict = {}
|
||||||
|
total_trades: int = 0
|
||||||
|
total_money_earned: int = 0
|
||||||
|
action_history: list = []
|
||||||
|
|
||||||
|
|
||||||
# ============== Market Schemas ==============
|
# ============== Market Schemas ==============
|
||||||
@ -119,6 +141,7 @@ class StatisticsSchema(BaseModel):
|
|||||||
living_agents: int
|
living_agents: int
|
||||||
total_agents_spawned: int
|
total_agents_spawned: int
|
||||||
total_agents_died: int
|
total_agents_died: int
|
||||||
|
total_births: int = 0
|
||||||
total_money_in_circulation: int
|
total_money_in_circulation: int
|
||||||
professions: dict[str, int]
|
professions: dict[str, int]
|
||||||
# Wealth inequality metrics
|
# Wealth inequality metrics
|
||||||
@ -127,6 +150,16 @@ class StatisticsSchema(BaseModel):
|
|||||||
richest_agent: int = 0
|
richest_agent: int = 0
|
||||||
poorest_agent: int = 0
|
poorest_agent: int = 0
|
||||||
gini_coefficient: float = 0.0
|
gini_coefficient: float = 0.0
|
||||||
|
# Age demographics
|
||||||
|
age_distribution: dict[str, int] = {}
|
||||||
|
avg_age: float = 0.0
|
||||||
|
oldest_agent: int = 0
|
||||||
|
youngest_agent: int = 0
|
||||||
|
generations: dict[int, int] = {}
|
||||||
|
# Death statistics
|
||||||
|
deaths_by_cause: dict[str, int] = {}
|
||||||
|
# Village storage
|
||||||
|
village_storage: dict[str, int] = {}
|
||||||
|
|
||||||
|
|
||||||
class ActionLogSchema(BaseModel):
|
class ActionLogSchema(BaseModel):
|
||||||
@ -142,10 +175,12 @@ class TurnLogSchema(BaseModel):
|
|||||||
turn: int
|
turn: int
|
||||||
agent_actions: list[ActionLogSchema]
|
agent_actions: list[ActionLogSchema]
|
||||||
deaths: list[str]
|
deaths: list[str]
|
||||||
|
births: list[str] = []
|
||||||
trades: list[dict]
|
trades: list[dict]
|
||||||
resources_produced: dict[str, int] = {}
|
resources_produced: dict[str, int] = {}
|
||||||
resources_consumed: dict[str, int] = {}
|
resources_consumed: dict[str, int] = {}
|
||||||
resources_spoiled: dict[str, int] = {}
|
resources_spoiled: dict[str, int] = {}
|
||||||
|
day_events: dict = {}
|
||||||
|
|
||||||
|
|
||||||
class ResourceStatsSchema(BaseModel):
|
class ResourceStatsSchema(BaseModel):
|
||||||
|
|||||||
@ -151,6 +151,105 @@ class AIConfig:
|
|||||||
reactive_fallback: bool = True
|
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
|
@dataclass
|
||||||
class SimulationConfig:
|
class SimulationConfig:
|
||||||
"""Master configuration containing all sub-configs."""
|
"""Master configuration containing all sub-configs."""
|
||||||
@ -161,6 +260,9 @@ class SimulationConfig:
|
|||||||
market: MarketConfig = field(default_factory=MarketConfig)
|
market: MarketConfig = field(default_factory=MarketConfig)
|
||||||
economy: EconomyConfig = field(default_factory=EconomyConfig)
|
economy: EconomyConfig = field(default_factory=EconomyConfig)
|
||||||
ai: AIConfig = field(default_factory=AIConfig)
|
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
|
# Simulation control
|
||||||
auto_step_interval: float = 1.0 # Seconds between auto steps
|
auto_step_interval: float = 1.0 # Seconds between auto steps
|
||||||
@ -175,6 +277,9 @@ class SimulationConfig:
|
|||||||
"world": asdict(self.world),
|
"world": asdict(self.world),
|
||||||
"market": asdict(self.market),
|
"market": asdict(self.market),
|
||||||
"economy": asdict(self.economy),
|
"economy": asdict(self.economy),
|
||||||
|
"age": asdict(self.age),
|
||||||
|
"storage": asdict(self.storage),
|
||||||
|
"sinks": asdict(self.sinks),
|
||||||
"auto_step_interval": self.auto_step_interval,
|
"auto_step_interval": self.auto_step_interval,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +294,9 @@ class SimulationConfig:
|
|||||||
world=WorldConfig(**data.get("world", {})),
|
world=WorldConfig(**data.get("world", {})),
|
||||||
market=MarketConfig(**data.get("market", {})),
|
market=MarketConfig(**data.get("market", {})),
|
||||||
economy=EconomyConfig(**data.get("economy", {})),
|
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),
|
auto_step_interval=data.get("auto_step_interval", 1.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -30,21 +30,26 @@ class TurnLog:
|
|||||||
turn: int
|
turn: int
|
||||||
agent_actions: list[dict] = field(default_factory=list)
|
agent_actions: list[dict] = field(default_factory=list)
|
||||||
deaths: list[str] = field(default_factory=list)
|
deaths: list[str] = field(default_factory=list)
|
||||||
|
births: list[str] = field(default_factory=list)
|
||||||
trades: list[dict] = field(default_factory=list)
|
trades: list[dict] = field(default_factory=list)
|
||||||
# Resource tracking for this turn
|
# Resource tracking for this turn
|
||||||
resources_produced: dict = field(default_factory=dict)
|
resources_produced: dict = field(default_factory=dict)
|
||||||
resources_consumed: dict = field(default_factory=dict)
|
resources_consumed: dict = field(default_factory=dict)
|
||||||
resources_spoiled: dict = field(default_factory=dict)
|
resources_spoiled: dict = field(default_factory=dict)
|
||||||
|
# New day events
|
||||||
|
day_events: dict = field(default_factory=dict)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"turn": self.turn,
|
"turn": self.turn,
|
||||||
"agent_actions": self.agent_actions,
|
"agent_actions": self.agent_actions,
|
||||||
"deaths": self.deaths,
|
"deaths": self.deaths,
|
||||||
|
"births": self.births,
|
||||||
"trades": self.trades,
|
"trades": self.trades,
|
||||||
"resources_produced": self.resources_produced,
|
"resources_produced": self.resources_produced,
|
||||||
"resources_consumed": self.resources_consumed,
|
"resources_consumed": self.resources_consumed,
|
||||||
"resources_spoiled": self.resources_spoiled,
|
"resources_spoiled": self.resources_spoiled,
|
||||||
|
"day_events": self.day_events,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -281,10 +286,15 @@ class GameEngine:
|
|||||||
# End turn logging
|
# End turn logging
|
||||||
self.logger.end_turn()
|
self.logger.end_turn()
|
||||||
|
|
||||||
# 8. Advance time
|
# 8. Advance time (returns True if new day started)
|
||||||
self.world.advance_time()
|
new_day = self.world.advance_time()
|
||||||
|
|
||||||
# 9. Check win/lose conditions (count only truly living agents, not corpses)
|
# 9. Process new day events (aging, births, sinks)
|
||||||
|
if new_day:
|
||||||
|
day_events = self._process_new_day(turn_log)
|
||||||
|
turn_log.day_events = day_events
|
||||||
|
|
||||||
|
# 10. Check win/lose conditions (count only truly living agents, not corpses)
|
||||||
if len(self.world.get_living_agents()) == 0:
|
if len(self.world.get_living_agents()) == 0:
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
self.logger.close()
|
self.logger.close()
|
||||||
@ -293,16 +303,32 @@ class GameEngine:
|
|||||||
return turn_log
|
return turn_log
|
||||||
|
|
||||||
def _mark_dead_agents(self, current_turn: int) -> list[Agent]:
|
def _mark_dead_agents(self, current_turn: int) -> list[Agent]:
|
||||||
"""Mark agents who just died as corpses. Returns list of newly dead agents."""
|
"""Mark agents who just died as corpses. Returns list of newly dead agents.
|
||||||
|
|
||||||
|
Also processes inheritance - distributing wealth to children.
|
||||||
|
"""
|
||||||
newly_dead = []
|
newly_dead = []
|
||||||
for agent in self.world.agents:
|
for agent in self.world.agents:
|
||||||
if not agent.is_alive() and not agent.is_corpse():
|
if not agent.is_alive() and not agent.is_corpse():
|
||||||
# Agent just died this turn
|
# Determine cause of death
|
||||||
|
if agent.is_too_old():
|
||||||
|
cause = "age"
|
||||||
|
else:
|
||||||
cause = agent.stats.get_critical_stat() or "unknown"
|
cause = agent.stats.get_critical_stat() or "unknown"
|
||||||
|
|
||||||
|
# Process inheritance BEFORE marking dead (while inventory still accessible)
|
||||||
|
inheritance = self.world.process_inheritance(agent)
|
||||||
|
if inheritance.get("beneficiaries"):
|
||||||
|
self.logger.log_event("inheritance", inheritance)
|
||||||
|
|
||||||
agent.mark_dead(current_turn, cause)
|
agent.mark_dead(current_turn, cause)
|
||||||
# Clear their action to show death state
|
# Clear their action to show death state
|
||||||
agent.current_action.action_type = "dead"
|
agent.current_action.action_type = "dead"
|
||||||
agent.current_action.message = f"Died: {cause}"
|
agent.current_action.message = f"Died: {cause}"
|
||||||
|
|
||||||
|
# Record death statistics
|
||||||
|
self.world.record_death(agent, cause)
|
||||||
|
|
||||||
newly_dead.append(agent)
|
newly_dead.append(agent)
|
||||||
return newly_dead
|
return newly_dead
|
||||||
|
|
||||||
@ -316,10 +342,148 @@ class GameEngine:
|
|||||||
|
|
||||||
for agent in to_remove:
|
for agent in to_remove:
|
||||||
self.world.agents.remove(agent)
|
self.world.agents.remove(agent)
|
||||||
self.world.total_agents_died += 1
|
# Note: death was already recorded in _mark_dead_agents
|
||||||
|
|
||||||
return to_remove
|
return to_remove
|
||||||
|
|
||||||
|
def _process_new_day(self, turn_log: TurnLog) -> dict:
|
||||||
|
"""Process all new-day events: aging, births, resource sinks.
|
||||||
|
|
||||||
|
Called when a new simulation day starts.
|
||||||
|
"""
|
||||||
|
events = {
|
||||||
|
"day": self.world.current_day,
|
||||||
|
"births": [],
|
||||||
|
"age_deaths": [],
|
||||||
|
"taxes_collected": 0,
|
||||||
|
"storage_decay": {},
|
||||||
|
"random_events": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
sinks_config = get_config().sinks
|
||||||
|
age_config = get_config().age
|
||||||
|
|
||||||
|
# 1. Age all living agents
|
||||||
|
for agent in self.world.get_living_agents():
|
||||||
|
agent.age_one_day()
|
||||||
|
|
||||||
|
# 2. Check for age-related deaths (after aging)
|
||||||
|
current_turn = self.world.current_turn
|
||||||
|
for agent in self.world.agents:
|
||||||
|
if not agent.is_corpse() and agent.is_too_old() and not agent.is_alive():
|
||||||
|
# Will be caught by _mark_dead_agents in the next turn
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 3. Process potential births
|
||||||
|
for agent in list(self.world.get_living_agents()): # Copy list since we modify it
|
||||||
|
if agent.can_give_birth(self.world.current_day):
|
||||||
|
child = self.world.spawn_child(agent)
|
||||||
|
if child:
|
||||||
|
birth_info = {
|
||||||
|
"parent_id": agent.id,
|
||||||
|
"parent_name": agent.name,
|
||||||
|
"child_id": child.id,
|
||||||
|
"child_name": child.name,
|
||||||
|
}
|
||||||
|
events["births"].append(birth_info)
|
||||||
|
turn_log.births.append(child.name)
|
||||||
|
self.logger.log_event("birth", birth_info)
|
||||||
|
|
||||||
|
# 4. Apply daily money tax (wealth redistribution/removal)
|
||||||
|
if sinks_config.daily_tax_rate > 0:
|
||||||
|
total_taxes = 0
|
||||||
|
for agent in self.world.get_living_agents():
|
||||||
|
tax = int(agent.money * sinks_config.daily_tax_rate)
|
||||||
|
if tax > 0:
|
||||||
|
agent.money -= tax
|
||||||
|
total_taxes += tax
|
||||||
|
events["taxes_collected"] = total_taxes
|
||||||
|
|
||||||
|
# 5. Apply village storage decay (resources spoil over time)
|
||||||
|
if sinks_config.daily_village_decay_rate > 0:
|
||||||
|
decay_rate = sinks_config.daily_village_decay_rate
|
||||||
|
for agent in self.world.get_living_agents():
|
||||||
|
for resource in agent.inventory[:]: # Copy list to allow modification
|
||||||
|
# Random chance for each resource to decay
|
||||||
|
if random.random() < decay_rate:
|
||||||
|
decay_amount = max(1, int(resource.quantity * decay_rate))
|
||||||
|
resource.quantity -= decay_amount
|
||||||
|
|
||||||
|
res_type = resource.type.value
|
||||||
|
events["storage_decay"][res_type] = events["storage_decay"].get(res_type, 0) + decay_amount
|
||||||
|
|
||||||
|
# Track as spoiled
|
||||||
|
turn_log.resources_spoiled[res_type] = turn_log.resources_spoiled.get(res_type, 0) + decay_amount
|
||||||
|
self.resource_stats["spoiled"][res_type] = self.resource_stats["spoiled"].get(res_type, 0) + decay_amount
|
||||||
|
|
||||||
|
if resource.quantity <= 0:
|
||||||
|
agent.inventory.remove(resource)
|
||||||
|
|
||||||
|
# 6. Random events (fires, theft, etc.)
|
||||||
|
if random.random() < sinks_config.random_event_chance:
|
||||||
|
event = self._generate_random_event(events, turn_log)
|
||||||
|
if event:
|
||||||
|
events["random_events"].append(event)
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def _generate_random_event(self, events: dict, turn_log: TurnLog) -> Optional[dict]:
|
||||||
|
"""Generate a random village event (disaster, theft, etc.)."""
|
||||||
|
sinks_config = get_config().sinks
|
||||||
|
living_agents = self.world.get_living_agents()
|
||||||
|
|
||||||
|
if not living_agents:
|
||||||
|
return None
|
||||||
|
|
||||||
|
event_types = ["fire", "theft", "blessing"]
|
||||||
|
event_type = random.choice(event_types)
|
||||||
|
|
||||||
|
event_info = {"type": event_type, "affected": []}
|
||||||
|
|
||||||
|
if event_type == "fire":
|
||||||
|
# Fire destroys some resources from random agents
|
||||||
|
num_affected = max(1, len(living_agents) // 5) # 20% of agents affected
|
||||||
|
affected_agents = random.sample(living_agents, min(num_affected, len(living_agents)))
|
||||||
|
|
||||||
|
for agent in affected_agents:
|
||||||
|
for resource in agent.inventory[:]:
|
||||||
|
loss = int(resource.quantity * sinks_config.fire_event_resource_loss)
|
||||||
|
if loss > 0:
|
||||||
|
resource.quantity -= loss
|
||||||
|
res_type = resource.type.value
|
||||||
|
turn_log.resources_spoiled[res_type] = turn_log.resources_spoiled.get(res_type, 0) + loss
|
||||||
|
|
||||||
|
if resource.quantity <= 0:
|
||||||
|
agent.inventory.remove(resource)
|
||||||
|
|
||||||
|
event_info["affected"].append(agent.name)
|
||||||
|
|
||||||
|
elif event_type == "theft":
|
||||||
|
# Some money is stolen from wealthy agents
|
||||||
|
wealthy_agents = [a for a in living_agents if a.money > 1000]
|
||||||
|
if wealthy_agents:
|
||||||
|
victim = random.choice(wealthy_agents)
|
||||||
|
stolen = int(victim.money * sinks_config.theft_event_money_loss)
|
||||||
|
victim.money -= stolen
|
||||||
|
event_info["affected"].append(victim.name)
|
||||||
|
event_info["amount_stolen"] = stolen
|
||||||
|
|
||||||
|
elif event_type == "blessing":
|
||||||
|
# Good harvest - some agents get bonus resources
|
||||||
|
lucky_agent = random.choice(living_agents)
|
||||||
|
from backend.domain.resources import Resource, ResourceType
|
||||||
|
bonus_type = random.choice([ResourceType.BERRIES, ResourceType.WOOD])
|
||||||
|
bonus = Resource(type=bonus_type, quantity=random.randint(2, 5), created_turn=self.world.current_turn)
|
||||||
|
lucky_agent.add_to_inventory(bonus)
|
||||||
|
event_info["affected"].append(lucky_agent.name)
|
||||||
|
event_info["bonus"] = f"+{bonus.quantity} {bonus_type.value}"
|
||||||
|
|
||||||
|
if event_info["affected"]:
|
||||||
|
self.logger.log_event("random_event", event_info)
|
||||||
|
return event_info
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def _execute_action(self, agent: Agent, decision: AIDecision) -> Optional[ActionResult]:
|
def _execute_action(self, agent: Agent, decision: AIDecision) -> Optional[ActionResult]:
|
||||||
"""Execute an action for an agent."""
|
"""Execute an action for an agent."""
|
||||||
action = decision.action
|
action = decision.action
|
||||||
@ -398,9 +562,17 @@ class GameEngine:
|
|||||||
- Gathering skill affects gather output
|
- Gathering skill affects gather output
|
||||||
- Woodcutting skill affects wood output
|
- Woodcutting skill affects wood output
|
||||||
- Skills improve with use
|
- Skills improve with use
|
||||||
|
|
||||||
|
Age modifies:
|
||||||
|
- Energy costs (young use less, old use more)
|
||||||
|
- Skill effectiveness (young less effective, old more effective "wisdom")
|
||||||
|
- Learning rate (young learn faster, old learn slower)
|
||||||
"""
|
"""
|
||||||
# Check energy
|
# Calculate age-modified energy cost
|
||||||
energy_cost = abs(config.energy_cost)
|
base_energy_cost = abs(config.energy_cost)
|
||||||
|
energy_cost_modifier = agent.get_energy_cost_modifier()
|
||||||
|
energy_cost = max(1, int(base_energy_cost * energy_cost_modifier))
|
||||||
|
|
||||||
if not agent.spend_energy(energy_cost):
|
if not agent.spend_energy(energy_cost):
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
action_type=action,
|
action_type=action,
|
||||||
@ -428,16 +600,20 @@ class GameEngine:
|
|||||||
# Get relevant skill for this action
|
# Get relevant skill for this action
|
||||||
skill_name = self._get_skill_for_action(action)
|
skill_name = self._get_skill_for_action(action)
|
||||||
skill_value = getattr(agent.skills, skill_name, 1.0) if skill_name else 1.0
|
skill_value = getattr(agent.skills, skill_name, 1.0) if skill_name else 1.0
|
||||||
skill_modifier = get_action_skill_modifier(skill_value)
|
|
||||||
|
|
||||||
# Check success chance (modified by skill)
|
# Apply age-based skill modifier (young less effective, old more effective)
|
||||||
|
age_skill_modifier = agent.get_skill_modifier()
|
||||||
|
skill_modifier = get_action_skill_modifier(skill_value) * age_skill_modifier
|
||||||
|
|
||||||
|
# Check success chance (modified by skill and age)
|
||||||
# Higher skill = higher effective success chance
|
# Higher skill = higher effective success chance
|
||||||
effective_success_chance = min(0.98, config.success_chance * skill_modifier)
|
effective_success_chance = min(0.98, config.success_chance * skill_modifier)
|
||||||
if random.random() > effective_success_chance:
|
if random.random() > effective_success_chance:
|
||||||
# Record action attempt (skill still improves on failure, just less)
|
# Record action attempt (skill still improves on failure, just less)
|
||||||
agent.record_action(action.value)
|
agent.record_action(action.value)
|
||||||
if skill_name:
|
if skill_name:
|
||||||
agent.skills.improve(skill_name, 0.005) # Small improvement on failure
|
learning_modifier = agent.get_learning_modifier()
|
||||||
|
agent.skills.improve(skill_name, 0.005, learning_modifier) # Small improvement on failure
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
action_type=action,
|
action_type=action,
|
||||||
success=False,
|
success=False,
|
||||||
@ -445,14 +621,21 @@ class GameEngine:
|
|||||||
message="Action failed",
|
message="Action failed",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate output (modified by skill for quantity)
|
# Generate output (modified by skill and age for quantity)
|
||||||
resources_gained = []
|
resources_gained = []
|
||||||
|
|
||||||
if config.output_resource:
|
if config.output_resource:
|
||||||
|
# Check storage limit before producing
|
||||||
|
res_type = config.output_resource.value
|
||||||
|
storage_available = self.world.get_storage_available(res_type)
|
||||||
|
|
||||||
# Skill affects output quantity
|
# Skill affects output quantity
|
||||||
base_quantity = random.randint(config.min_output, config.max_output)
|
base_quantity = random.randint(config.min_output, config.max_output)
|
||||||
quantity = max(config.min_output, int(base_quantity * skill_modifier))
|
quantity = max(config.min_output, int(base_quantity * skill_modifier))
|
||||||
|
|
||||||
|
# Limit by storage
|
||||||
|
quantity = min(quantity, storage_available)
|
||||||
|
|
||||||
if quantity > 0:
|
if quantity > 0:
|
||||||
resource = Resource(
|
resource = Resource(
|
||||||
type=config.output_resource,
|
type=config.output_resource,
|
||||||
@ -467,10 +650,15 @@ class GameEngine:
|
|||||||
created_turn=self.world.current_turn,
|
created_turn=self.world.current_turn,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Secondary output (e.g., hide from hunting) - also affected by skill
|
# Secondary output (e.g., hide from hunting) - also affected by skill and storage
|
||||||
if config.secondary_output:
|
if config.secondary_output:
|
||||||
|
res_type = config.secondary_output.value
|
||||||
|
storage_available = self.world.get_storage_available(res_type)
|
||||||
|
|
||||||
base_quantity = random.randint(config.secondary_min, config.secondary_max)
|
base_quantity = random.randint(config.secondary_min, config.secondary_max)
|
||||||
quantity = max(0, int(base_quantity * skill_modifier))
|
quantity = max(0, int(base_quantity * skill_modifier))
|
||||||
|
quantity = min(quantity, storage_available)
|
||||||
|
|
||||||
if quantity > 0:
|
if quantity > 0:
|
||||||
resource = Resource(
|
resource = Resource(
|
||||||
type=config.secondary_output,
|
type=config.secondary_output,
|
||||||
@ -485,10 +673,11 @@ class GameEngine:
|
|||||||
created_turn=self.world.current_turn,
|
created_turn=self.world.current_turn,
|
||||||
))
|
))
|
||||||
|
|
||||||
# Record action and improve skill
|
# Record action and improve skill (modified by age learning rate)
|
||||||
agent.record_action(action.value)
|
agent.record_action(action.value)
|
||||||
if skill_name:
|
if skill_name:
|
||||||
agent.skills.improve(skill_name, 0.015) # Skill improves with successful use
|
learning_modifier = agent.get_learning_modifier()
|
||||||
|
agent.skills.improve(skill_name, 0.015, learning_modifier) # Skill improves with successful use
|
||||||
|
|
||||||
# Build success message with details
|
# Build success message with details
|
||||||
gained_str = ", ".join(f"+{r.quantity} {r.type.value}" for r in resources_gained)
|
gained_str = ", ".join(f"+{r.quantity} {r.type.value}" for r in resources_gained)
|
||||||
@ -569,13 +758,15 @@ class GameEngine:
|
|||||||
if seller:
|
if seller:
|
||||||
seller.money += result.total_paid
|
seller.money += result.total_paid
|
||||||
seller.record_trade(result.total_paid)
|
seller.record_trade(result.total_paid)
|
||||||
seller.skills.improve("trading", 0.02) # Seller skill improves
|
seller.skills.improve("trading", 0.02, seller.get_learning_modifier()) # Seller skill improves
|
||||||
|
|
||||||
agent.spend_energy(abs(config.energy_cost))
|
# Age-modified energy cost for trading
|
||||||
|
energy_cost = max(1, int(abs(config.energy_cost) * agent.get_energy_cost_modifier()))
|
||||||
|
agent.spend_energy(energy_cost)
|
||||||
|
|
||||||
# Record buyer's trade and improve skill
|
# Record buyer's trade and improve skill (with age learning modifier)
|
||||||
agent.record_action("trade")
|
agent.record_action("trade")
|
||||||
agent.skills.improve("trading", 0.01) # Buyer skill improves less
|
agent.skills.improve("trading", 0.01, agent.get_learning_modifier()) # Buyer skill improves less
|
||||||
|
|
||||||
return ActionResult(
|
return ActionResult(
|
||||||
action_type=ActionType.TRADE,
|
action_type=ActionType.TRADE,
|
||||||
|
|||||||
@ -210,6 +210,22 @@ class SimulationLogger:
|
|||||||
self._current_entry.deaths.append({"name": agent_name, "cause": cause})
|
self._current_entry.deaths.append({"name": agent_name, "cause": cause})
|
||||||
self.logger.info(f" DEATH: {agent_name} died from {cause}")
|
self.logger.info(f" DEATH: {agent_name} died from {cause}")
|
||||||
|
|
||||||
|
def log_event(self, event_type: str, event_data: dict) -> None:
|
||||||
|
"""Log a general event (births, random events, etc.)."""
|
||||||
|
if self._current_entry is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
if event_type == "birth":
|
||||||
|
self.logger.info(
|
||||||
|
f" BIRTH: {event_data.get('child_name', '?')} born to {event_data.get('parent_name', '?')}"
|
||||||
|
)
|
||||||
|
elif event_type == "random_event":
|
||||||
|
self.logger.info(
|
||||||
|
f" EVENT: {event_data.get('type', '?')} affecting {event_data.get('affected', [])}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.logger.debug(f" Event [{event_type}]: {event_data}")
|
||||||
|
|
||||||
def log_statistics(self, stats: dict) -> None:
|
def log_statistics(self, stats: dict) -> None:
|
||||||
"""Log end-of-turn statistics."""
|
"""Log end-of-turn statistics."""
|
||||||
if self._current_entry is None:
|
if self._current_entry is None:
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
The world spawns diverse agents with varied personality traits,
|
The world spawns diverse agents with varied personality traits,
|
||||||
skills, and starting conditions to create emergent professions
|
skills, and starting conditions to create emergent professions
|
||||||
and class inequality.
|
and class inequality.
|
||||||
|
|
||||||
|
Now includes age-based lifecycle with birth and death by old age.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import random
|
import random
|
||||||
@ -15,6 +17,7 @@ from backend.domain.personality import (
|
|||||||
PersonalityTraits, Skills,
|
PersonalityTraits, Skills,
|
||||||
generate_random_personality, generate_random_skills
|
generate_random_personality, generate_random_skills
|
||||||
)
|
)
|
||||||
|
from backend.domain.resources import ResourceType
|
||||||
|
|
||||||
|
|
||||||
class TimeOfDay(Enum):
|
class TimeOfDay(Enum):
|
||||||
@ -68,6 +71,21 @@ class World:
|
|||||||
# Statistics
|
# Statistics
|
||||||
total_agents_spawned: int = 0
|
total_agents_spawned: int = 0
|
||||||
total_agents_died: int = 0
|
total_agents_died: int = 0
|
||||||
|
total_births: int = 0
|
||||||
|
total_deaths_by_age: int = 0
|
||||||
|
total_deaths_by_starvation: int = 0
|
||||||
|
total_deaths_by_thirst: int = 0
|
||||||
|
total_deaths_by_cold: int = 0
|
||||||
|
|
||||||
|
# Village-wide storage tracking (for storage limits)
|
||||||
|
village_storage: dict = field(default_factory=lambda: {
|
||||||
|
"meat": 0,
|
||||||
|
"berries": 0,
|
||||||
|
"water": 0,
|
||||||
|
"wood": 0,
|
||||||
|
"hide": 0,
|
||||||
|
"clothes": 0,
|
||||||
|
})
|
||||||
|
|
||||||
def spawn_agent(
|
def spawn_agent(
|
||||||
self,
|
self,
|
||||||
@ -76,6 +94,9 @@ class World:
|
|||||||
position: Optional[Position] = None,
|
position: Optional[Position] = None,
|
||||||
archetype: Optional[str] = None,
|
archetype: Optional[str] = None,
|
||||||
starting_money: Optional[int] = None,
|
starting_money: Optional[int] = None,
|
||||||
|
age: Optional[int] = None,
|
||||||
|
generation: int = 0,
|
||||||
|
parent_ids: Optional[list[str]] = None,
|
||||||
) -> Agent:
|
) -> Agent:
|
||||||
"""Spawn a new agent in the world with unique personality.
|
"""Spawn a new agent in the world with unique personality.
|
||||||
|
|
||||||
@ -85,6 +106,9 @@ class World:
|
|||||||
position: Starting position (random if None)
|
position: Starting position (random if None)
|
||||||
archetype: Personality archetype ("hunter", "gatherer", "trader", etc.)
|
archetype: Personality archetype ("hunter", "gatherer", "trader", etc.)
|
||||||
starting_money: Starting money (random with inequality if None)
|
starting_money: Starting money (random with inequality if None)
|
||||||
|
age: Starting age (random within config range if None)
|
||||||
|
generation: 0 for initial spawn, 1+ for born in simulation
|
||||||
|
parent_ids: IDs of parent agents (for lineage tracking)
|
||||||
"""
|
"""
|
||||||
if position is None:
|
if position is None:
|
||||||
position = Position(
|
position = Position(
|
||||||
@ -92,14 +116,21 @@ class World:
|
|||||||
y=random.randint(0, self.config.height - 1),
|
y=random.randint(0, self.config.height - 1),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Generate unique personality and skills
|
# Get age config for age calculation
|
||||||
|
from backend.config import get_config
|
||||||
|
age_config = get_config().age
|
||||||
|
|
||||||
|
# Calculate starting age
|
||||||
|
if age is None:
|
||||||
|
age = random.randint(age_config.min_start_age, age_config.max_start_age)
|
||||||
|
|
||||||
|
# Generate unique personality and skills (skills influenced by age)
|
||||||
personality = generate_random_personality(archetype)
|
personality = generate_random_personality(archetype)
|
||||||
skills = generate_random_skills(personality)
|
skills = generate_random_skills(personality, age=age)
|
||||||
|
|
||||||
# Variable starting money for class inequality
|
# Variable starting money for class inequality
|
||||||
# Some agents start with more, some with less
|
# Some agents start with more, some with less
|
||||||
if starting_money is None:
|
if starting_money is None:
|
||||||
from backend.config import get_config
|
|
||||||
base_money = get_config().world.starting_money
|
base_money = get_config().world.starting_money
|
||||||
# Random multiplier: 0.3x to 2.0x base money
|
# Random multiplier: 0.3x to 2.0x base money
|
||||||
# This creates natural class inequality
|
# This creates natural class inequality
|
||||||
@ -116,12 +147,185 @@ class World:
|
|||||||
personality=personality,
|
personality=personality,
|
||||||
skills=skills,
|
skills=skills,
|
||||||
money=starting_money,
|
money=starting_money,
|
||||||
|
age=age,
|
||||||
|
birth_day=self.current_day,
|
||||||
|
generation=generation,
|
||||||
|
parent_ids=parent_ids or [],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.agents.append(agent)
|
self.agents.append(agent)
|
||||||
self.total_agents_spawned += 1
|
self.total_agents_spawned += 1
|
||||||
return agent
|
return agent
|
||||||
|
|
||||||
|
def spawn_child(self, parent: Agent) -> Optional[Agent]:
|
||||||
|
"""Spawn a new agent as a child of an existing agent.
|
||||||
|
|
||||||
|
Birth chance is controlled by village prosperity (food abundance).
|
||||||
|
Parent transfers wealth to child at birth.
|
||||||
|
|
||||||
|
Returns the new agent or None if birth conditions not met.
|
||||||
|
"""
|
||||||
|
from backend.config import get_config
|
||||||
|
age_config = get_config().age
|
||||||
|
|
||||||
|
# Check birth eligibility
|
||||||
|
if not parent.can_give_birth(self.current_day):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Calculate economy-based birth chance
|
||||||
|
# More food in village = higher birth rate
|
||||||
|
# But even in hard times, some births occur (base chance always applies)
|
||||||
|
prosperity = self.calculate_prosperity()
|
||||||
|
|
||||||
|
# Prosperity boosts birth rate: base_chance * (1 + prosperity * multiplier)
|
||||||
|
# At prosperity=0: birth_chance = base_chance
|
||||||
|
# At prosperity=1: birth_chance = base_chance * (1 + multiplier)
|
||||||
|
birth_chance = age_config.birth_base_chance * (1 + prosperity * age_config.birth_prosperity_multiplier)
|
||||||
|
birth_chance = min(0.20, birth_chance) # Cap at 20%
|
||||||
|
|
||||||
|
if random.random() > birth_chance:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Birth happens! Child spawns near parent
|
||||||
|
child_pos = Position(
|
||||||
|
x=parent.position.x + random.uniform(-1, 1),
|
||||||
|
y=parent.position.y + random.uniform(-1, 1),
|
||||||
|
)
|
||||||
|
# Clamp to world bounds
|
||||||
|
child_pos.x = max(0, min(self.config.width - 1, child_pos.x))
|
||||||
|
child_pos.y = max(0, min(self.config.height - 1, child_pos.y))
|
||||||
|
|
||||||
|
# Child inherits some personality traits from parent with mutation
|
||||||
|
child_archetype = None # Random, not determined by parent
|
||||||
|
|
||||||
|
# Wealth transfer: parent gives portion of their wealth to child
|
||||||
|
wealth_transfer = age_config.birth_wealth_transfer
|
||||||
|
child_money = int(parent.money * wealth_transfer)
|
||||||
|
parent.money -= child_money
|
||||||
|
|
||||||
|
# Ensure child has minimum viable wealth
|
||||||
|
min_child_money = int(get_config().world.starting_money * 0.3)
|
||||||
|
child_money = max(child_money, min_child_money)
|
||||||
|
|
||||||
|
# Child starts at configured age (adult)
|
||||||
|
child_age = age_config.child_start_age
|
||||||
|
|
||||||
|
child = self.spawn_agent(
|
||||||
|
name=f"Child_{self.total_agents_spawned + 1}",
|
||||||
|
position=child_pos,
|
||||||
|
archetype=child_archetype,
|
||||||
|
starting_money=child_money,
|
||||||
|
age=child_age,
|
||||||
|
generation=parent.generation + 1,
|
||||||
|
parent_ids=[parent.id],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parent also transfers some food to child
|
||||||
|
self._transfer_resources_to_child(parent, child)
|
||||||
|
|
||||||
|
# Record birth for parent
|
||||||
|
parent.record_birth(self.current_day, child.id)
|
||||||
|
|
||||||
|
self.total_births += 1
|
||||||
|
return child
|
||||||
|
|
||||||
|
def _transfer_resources_to_child(self, parent: Agent, child: Agent) -> None:
|
||||||
|
"""Transfer some resources from parent to child at birth."""
|
||||||
|
# Transfer 1 of each food type parent has (if available)
|
||||||
|
for res_type in [ResourceType.MEAT, ResourceType.BERRIES, ResourceType.WATER]:
|
||||||
|
if parent.has_resource(res_type, 1):
|
||||||
|
parent.remove_from_inventory(res_type, 1)
|
||||||
|
from backend.domain.resources import Resource
|
||||||
|
child.add_to_inventory(Resource(
|
||||||
|
type=res_type,
|
||||||
|
quantity=1,
|
||||||
|
created_turn=self.current_turn,
|
||||||
|
))
|
||||||
|
|
||||||
|
def calculate_prosperity(self) -> float:
|
||||||
|
"""Calculate village prosperity (0.0 to 1.0) based on food abundance.
|
||||||
|
|
||||||
|
Higher prosperity = more births allowed.
|
||||||
|
This creates population cycles tied to resource availability.
|
||||||
|
"""
|
||||||
|
self.update_village_storage()
|
||||||
|
from backend.config import get_config
|
||||||
|
storage_config = get_config().storage
|
||||||
|
|
||||||
|
# Calculate how full each food storage is
|
||||||
|
meat_ratio = self.village_storage.get("meat", 0) / max(1, storage_config.village_meat_limit)
|
||||||
|
berries_ratio = self.village_storage.get("berries", 0) / max(1, storage_config.village_berries_limit)
|
||||||
|
water_ratio = self.village_storage.get("water", 0) / max(1, storage_config.village_water_limit)
|
||||||
|
|
||||||
|
# Average food abundance (weighted: meat most valuable)
|
||||||
|
prosperity = (meat_ratio * 0.4 + berries_ratio * 0.3 + water_ratio * 0.3)
|
||||||
|
return min(1.0, max(0.0, prosperity))
|
||||||
|
|
||||||
|
def process_inheritance(self, dead_agent: Agent) -> dict:
|
||||||
|
"""Process inheritance when an agent dies.
|
||||||
|
|
||||||
|
Wealth and resources are distributed to living children.
|
||||||
|
If no children, wealth is distributed to random villagers (estate tax).
|
||||||
|
|
||||||
|
Returns dict with inheritance details.
|
||||||
|
"""
|
||||||
|
from backend.config import get_config
|
||||||
|
age_config = get_config().age
|
||||||
|
|
||||||
|
if not age_config.inheritance_enabled:
|
||||||
|
return {"enabled": False}
|
||||||
|
|
||||||
|
inheritance_info = {
|
||||||
|
"enabled": True,
|
||||||
|
"deceased": dead_agent.name,
|
||||||
|
"total_money": dead_agent.money,
|
||||||
|
"total_resources": sum(r.quantity for r in dead_agent.inventory),
|
||||||
|
"beneficiaries": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find living children
|
||||||
|
living_children = []
|
||||||
|
for child_id in dead_agent.children_ids:
|
||||||
|
child = self.get_agent(child_id)
|
||||||
|
if child and child.is_alive():
|
||||||
|
living_children.append(child)
|
||||||
|
|
||||||
|
if living_children:
|
||||||
|
# Distribute equally among children
|
||||||
|
money_per_child = dead_agent.money // len(living_children)
|
||||||
|
for child in living_children:
|
||||||
|
child.money += money_per_child
|
||||||
|
inheritance_info["beneficiaries"].append({
|
||||||
|
"name": child.name,
|
||||||
|
"money": money_per_child,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Distribute resources (round-robin)
|
||||||
|
for i, resource in enumerate(dead_agent.inventory):
|
||||||
|
recipient = living_children[i % len(living_children)]
|
||||||
|
recipient.add_to_inventory(resource)
|
||||||
|
else:
|
||||||
|
# No children - distribute to random villagers (estate tax effect)
|
||||||
|
living = self.get_living_agents()
|
||||||
|
if living:
|
||||||
|
# Give money to poorest villagers
|
||||||
|
poorest = sorted(living, key=lambda a: a.money)[:3]
|
||||||
|
if poorest:
|
||||||
|
money_each = dead_agent.money // len(poorest)
|
||||||
|
for villager in poorest:
|
||||||
|
villager.money += money_each
|
||||||
|
inheritance_info["beneficiaries"].append({
|
||||||
|
"name": villager.name,
|
||||||
|
"money": money_each,
|
||||||
|
"relation": "community"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Clear dead agent's inventory (already distributed or lost)
|
||||||
|
dead_agent.inventory.clear()
|
||||||
|
dead_agent.money = 0
|
||||||
|
|
||||||
|
return inheritance_info
|
||||||
|
|
||||||
def get_agent(self, agent_id: str) -> Optional[Agent]:
|
def get_agent(self, agent_id: str) -> Optional[Agent]:
|
||||||
"""Get an agent by ID."""
|
"""Get an agent by ID."""
|
||||||
for agent in self.agents:
|
for agent in self.agents:
|
||||||
@ -137,16 +341,21 @@ class World:
|
|||||||
# Don't actually remove here - let the engine handle corpse visualization
|
# Don't actually remove here - let the engine handle corpse visualization
|
||||||
return dead_agents
|
return dead_agents
|
||||||
|
|
||||||
def advance_time(self) -> None:
|
def advance_time(self) -> bool:
|
||||||
"""Advance the simulation time by one step."""
|
"""Advance the simulation time by one step.
|
||||||
|
|
||||||
|
Returns True if a new day started (for age/birth processing).
|
||||||
|
"""
|
||||||
self.current_turn += 1
|
self.current_turn += 1
|
||||||
self.step_in_day += 1
|
self.step_in_day += 1
|
||||||
|
|
||||||
total_steps = self.config.day_steps + self.config.night_steps
|
total_steps = self.config.day_steps + self.config.night_steps
|
||||||
|
new_day = False
|
||||||
|
|
||||||
if self.step_in_day > total_steps:
|
if self.step_in_day > total_steps:
|
||||||
self.step_in_day = 1
|
self.step_in_day = 1
|
||||||
self.current_day += 1
|
self.current_day += 1
|
||||||
|
new_day = True
|
||||||
|
|
||||||
# Determine time of day
|
# Determine time of day
|
||||||
if self.step_in_day <= self.config.day_steps:
|
if self.step_in_day <= self.config.day_steps:
|
||||||
@ -154,6 +363,91 @@ class World:
|
|||||||
else:
|
else:
|
||||||
self.time_of_day = TimeOfDay.NIGHT
|
self.time_of_day = TimeOfDay.NIGHT
|
||||||
|
|
||||||
|
return new_day
|
||||||
|
|
||||||
|
def process_new_day(self) -> dict:
|
||||||
|
"""Process all new-day events: aging, births, sinks.
|
||||||
|
|
||||||
|
Returns a dict with events that happened.
|
||||||
|
"""
|
||||||
|
events = {
|
||||||
|
"aged_agents": [],
|
||||||
|
"births": [],
|
||||||
|
"age_deaths": [],
|
||||||
|
"storage_decay": {},
|
||||||
|
"taxes_collected": 0,
|
||||||
|
"random_events": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
# Age all living agents
|
||||||
|
for agent in self.get_living_agents():
|
||||||
|
agent.age_one_day()
|
||||||
|
events["aged_agents"].append(agent.id)
|
||||||
|
|
||||||
|
# Check for births (only from living agents after aging)
|
||||||
|
for agent in self.get_living_agents():
|
||||||
|
if agent.can_give_birth(self.current_day):
|
||||||
|
child = self.spawn_child(agent)
|
||||||
|
if child:
|
||||||
|
events["births"].append({
|
||||||
|
"parent_id": agent.id,
|
||||||
|
"child_id": child.id,
|
||||||
|
"child_name": child.name,
|
||||||
|
})
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def update_village_storage(self) -> None:
|
||||||
|
"""Update the village-wide storage tracking."""
|
||||||
|
# Reset counts
|
||||||
|
for key in self.village_storage:
|
||||||
|
self.village_storage[key] = 0
|
||||||
|
|
||||||
|
# Count all resources in agent inventories
|
||||||
|
for agent in self.get_living_agents():
|
||||||
|
for resource in agent.inventory:
|
||||||
|
res_type = resource.type.value
|
||||||
|
if res_type in self.village_storage:
|
||||||
|
self.village_storage[res_type] += resource.quantity
|
||||||
|
|
||||||
|
def get_storage_limit(self, resource_type: str) -> int:
|
||||||
|
"""Get the storage limit for a resource type."""
|
||||||
|
from backend.config import get_config
|
||||||
|
storage_config = get_config().storage
|
||||||
|
|
||||||
|
limit_map = {
|
||||||
|
"meat": storage_config.village_meat_limit,
|
||||||
|
"berries": storage_config.village_berries_limit,
|
||||||
|
"water": storage_config.village_water_limit,
|
||||||
|
"wood": storage_config.village_wood_limit,
|
||||||
|
"hide": storage_config.village_hide_limit,
|
||||||
|
"clothes": storage_config.village_clothes_limit,
|
||||||
|
}
|
||||||
|
return limit_map.get(resource_type, 999999)
|
||||||
|
|
||||||
|
def get_storage_available(self, resource_type: str) -> int:
|
||||||
|
"""Get how much more of a resource can be stored village-wide."""
|
||||||
|
self.update_village_storage()
|
||||||
|
limit = self.get_storage_limit(resource_type)
|
||||||
|
current = self.village_storage.get(resource_type, 0)
|
||||||
|
return max(0, limit - current)
|
||||||
|
|
||||||
|
def is_storage_full(self, resource_type: str) -> bool:
|
||||||
|
"""Check if village storage for a resource type is full."""
|
||||||
|
return self.get_storage_available(resource_type) <= 0
|
||||||
|
|
||||||
|
def record_death(self, agent: Agent, reason: str) -> None:
|
||||||
|
"""Record a death and update statistics."""
|
||||||
|
self.total_agents_died += 1
|
||||||
|
if reason == "age":
|
||||||
|
self.total_deaths_by_age += 1
|
||||||
|
elif reason == "hunger":
|
||||||
|
self.total_deaths_by_starvation += 1
|
||||||
|
elif reason == "thirst":
|
||||||
|
self.total_deaths_by_thirst += 1
|
||||||
|
elif reason == "heat":
|
||||||
|
self.total_deaths_by_cold += 1
|
||||||
|
|
||||||
def is_night(self) -> bool:
|
def is_night(self) -> bool:
|
||||||
"""Check if it's currently night."""
|
"""Check if it's currently night."""
|
||||||
return self.time_of_day == TimeOfDay.NIGHT
|
return self.time_of_day == TimeOfDay.NIGHT
|
||||||
@ -163,7 +457,7 @@ class World:
|
|||||||
return [a for a in self.agents if a.is_alive() and not a.is_corpse()]
|
return [a for a in self.agents if a.is_alive() and not a.is_corpse()]
|
||||||
|
|
||||||
def get_statistics(self) -> dict:
|
def get_statistics(self) -> dict:
|
||||||
"""Get current world statistics including wealth distribution."""
|
"""Get current world statistics including wealth distribution and demographics."""
|
||||||
living = self.get_living_agents()
|
living = self.get_living_agents()
|
||||||
total_money = sum(a.money for a in living)
|
total_money = sum(a.money for a in living)
|
||||||
|
|
||||||
@ -174,6 +468,21 @@ class World:
|
|||||||
prof = agent.profession.value
|
prof = agent.profession.value
|
||||||
profession_counts[prof] = profession_counts.get(prof, 0) + 1
|
profession_counts[prof] = profession_counts.get(prof, 0) + 1
|
||||||
|
|
||||||
|
# Age demographics
|
||||||
|
age_distribution = {"young": 0, "prime": 0, "old": 0}
|
||||||
|
ages = []
|
||||||
|
generations = {}
|
||||||
|
for agent in living:
|
||||||
|
category = agent.get_age_category()
|
||||||
|
age_distribution[category] = age_distribution.get(category, 0) + 1
|
||||||
|
ages.append(agent.age)
|
||||||
|
gen = agent.generation
|
||||||
|
generations[gen] = generations.get(gen, 0) + 1
|
||||||
|
|
||||||
|
avg_age = sum(ages) / len(ages) if ages else 0
|
||||||
|
oldest_age = max(ages) if ages else 0
|
||||||
|
youngest_age = min(ages) if ages else 0
|
||||||
|
|
||||||
# Calculate wealth inequality metrics
|
# Calculate wealth inequality metrics
|
||||||
if living:
|
if living:
|
||||||
moneys = sorted([a.money for a in living])
|
moneys = sorted([a.money for a in living])
|
||||||
@ -192,6 +501,9 @@ class World:
|
|||||||
else:
|
else:
|
||||||
avg_money = median_money = richest = poorest = gini = 0
|
avg_money = median_money = richest = poorest = gini = 0
|
||||||
|
|
||||||
|
# Update village storage
|
||||||
|
self.update_village_storage()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"current_turn": self.current_turn,
|
"current_turn": self.current_turn,
|
||||||
"current_day": self.current_day,
|
"current_day": self.current_day,
|
||||||
@ -200,6 +512,7 @@ class World:
|
|||||||
"living_agents": len(living),
|
"living_agents": len(living),
|
||||||
"total_agents_spawned": self.total_agents_spawned,
|
"total_agents_spawned": self.total_agents_spawned,
|
||||||
"total_agents_died": self.total_agents_died,
|
"total_agents_died": self.total_agents_died,
|
||||||
|
"total_births": self.total_births,
|
||||||
"total_money_in_circulation": total_money,
|
"total_money_in_circulation": total_money,
|
||||||
"professions": profession_counts,
|
"professions": profession_counts,
|
||||||
# Wealth inequality metrics
|
# Wealth inequality metrics
|
||||||
@ -208,6 +521,21 @@ class World:
|
|||||||
"richest_agent": richest,
|
"richest_agent": richest,
|
||||||
"poorest_agent": poorest,
|
"poorest_agent": poorest,
|
||||||
"gini_coefficient": round(gini, 3),
|
"gini_coefficient": round(gini, 3),
|
||||||
|
# Age demographics
|
||||||
|
"age_distribution": age_distribution,
|
||||||
|
"avg_age": round(avg_age, 1),
|
||||||
|
"oldest_agent": oldest_age,
|
||||||
|
"youngest_agent": youngest_age,
|
||||||
|
"generations": generations,
|
||||||
|
# Death statistics
|
||||||
|
"deaths_by_cause": {
|
||||||
|
"age": self.total_deaths_by_age,
|
||||||
|
"starvation": self.total_deaths_by_starvation,
|
||||||
|
"thirst": self.total_deaths_by_thirst,
|
||||||
|
"cold": self.total_deaths_by_cold,
|
||||||
|
},
|
||||||
|
# Village storage
|
||||||
|
"village_storage": self.village_storage.copy(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_state_snapshot(self) -> dict:
|
def get_state_snapshot(self) -> dict:
|
||||||
|
|||||||
@ -25,6 +25,12 @@ def _get_agent_stats_config():
|
|||||||
return get_config().agent_stats
|
return get_config().agent_stats
|
||||||
|
|
||||||
|
|
||||||
|
def _get_age_config():
|
||||||
|
"""Get age configuration from global config."""
|
||||||
|
from backend.config import get_config
|
||||||
|
return get_config().age
|
||||||
|
|
||||||
|
|
||||||
class Profession(Enum):
|
class Profession(Enum):
|
||||||
"""Agent professions - now derived from personality and skills."""
|
"""Agent professions - now derived from personality and skills."""
|
||||||
VILLAGER = "villager"
|
VILLAGER = "villager"
|
||||||
@ -96,14 +102,24 @@ class AgentStats:
|
|||||||
# Critical threshold - loaded from config
|
# Critical threshold - loaded from config
|
||||||
CRITICAL_THRESHOLD: float = field(default=0.25)
|
CRITICAL_THRESHOLD: float = field(default=0.25)
|
||||||
|
|
||||||
def apply_passive_decay(self, has_clothes: bool = False) -> None:
|
def apply_passive_decay(self, has_clothes: bool = False, decay_modifier: float = 1.0) -> None:
|
||||||
"""Apply passive stat decay each turn."""
|
"""Apply passive stat decay each turn.
|
||||||
self.energy = max(0, self.energy - self.ENERGY_DECAY)
|
|
||||||
self.hunger = max(0, self.hunger - self.HUNGER_DECAY)
|
Args:
|
||||||
self.thirst = max(0, self.thirst - self.THIRST_DECAY)
|
has_clothes: Whether agent has clothes (reduces heat decay)
|
||||||
|
decay_modifier: Age-based modifier (old agents decay faster)
|
||||||
|
"""
|
||||||
|
energy_decay = int(self.ENERGY_DECAY * decay_modifier)
|
||||||
|
hunger_decay = int(self.HUNGER_DECAY * decay_modifier)
|
||||||
|
thirst_decay = int(self.THIRST_DECAY * decay_modifier)
|
||||||
|
|
||||||
|
self.energy = max(0, self.energy - energy_decay)
|
||||||
|
self.hunger = max(0, self.hunger - hunger_decay)
|
||||||
|
self.thirst = max(0, self.thirst - thirst_decay)
|
||||||
|
|
||||||
# Clothes reduce heat loss by 50%
|
# Clothes reduce heat loss by 50%
|
||||||
heat_decay = self.HEAT_DECAY // 2 if has_clothes else self.HEAT_DECAY
|
heat_decay = int(self.HEAT_DECAY * decay_modifier)
|
||||||
|
heat_decay = heat_decay // 2 if has_clothes else heat_decay
|
||||||
self.heat = max(0, self.heat - heat_decay)
|
self.heat = max(0, self.heat - heat_decay)
|
||||||
|
|
||||||
def is_critical(self) -> bool:
|
def is_critical(self) -> bool:
|
||||||
@ -217,6 +233,11 @@ class Agent:
|
|||||||
Stats, inventory slots, and starting money are loaded from config.json.
|
Stats, inventory slots, and starting money are loaded from config.json.
|
||||||
Each agent now has unique personality traits and skills that create
|
Each agent now has unique personality traits and skills that create
|
||||||
emergent behaviors and professions.
|
emergent behaviors and professions.
|
||||||
|
|
||||||
|
Age affects skills, energy costs, and survival:
|
||||||
|
- Young (< 25): Learning faster, lower skill effectiveness, less energy cost
|
||||||
|
- Prime (25-45): Peak performance
|
||||||
|
- Old (> 45): Higher skill effectiveness (wisdom), but higher energy costs
|
||||||
"""
|
"""
|
||||||
id: str = field(default_factory=lambda: str(uuid4())[:8])
|
id: str = field(default_factory=lambda: str(uuid4())[:8])
|
||||||
name: str = ""
|
name: str = ""
|
||||||
@ -230,6 +251,15 @@ class Agent:
|
|||||||
personality: PersonalityTraits = field(default_factory=PersonalityTraits)
|
personality: PersonalityTraits = field(default_factory=PersonalityTraits)
|
||||||
skills: Skills = field(default_factory=Skills)
|
skills: Skills = field(default_factory=Skills)
|
||||||
|
|
||||||
|
# Age system - age is in "years" where 1 year = 1 simulation day
|
||||||
|
age: int = field(default=-1) # -1 signals to use random start age
|
||||||
|
max_age: int = field(default=-1) # -1 signals to calculate from config
|
||||||
|
birth_day: int = 0 # Day this agent was born (0 = initial spawn)
|
||||||
|
last_birth_day: int = -1000 # Last day this agent gave birth (for cooldown)
|
||||||
|
parent_ids: list[str] = field(default_factory=list) # IDs of parents (for lineage)
|
||||||
|
children_ids: list[str] = field(default_factory=list) # IDs of children
|
||||||
|
generation: int = 0 # 0 = initial spawn, 1+ = born in simulation
|
||||||
|
|
||||||
# Movement and action tracking
|
# Movement and action tracking
|
||||||
home_position: Position = field(default_factory=Position)
|
home_position: Position = field(default_factory=Position)
|
||||||
current_action: AgentAction = field(default_factory=AgentAction)
|
current_action: AgentAction = field(default_factory=AgentAction)
|
||||||
@ -267,6 +297,21 @@ class Agent:
|
|||||||
if self.INVENTORY_SLOTS == -1:
|
if self.INVENTORY_SLOTS == -1:
|
||||||
self.INVENTORY_SLOTS = config.inventory_slots
|
self.INVENTORY_SLOTS = config.inventory_slots
|
||||||
|
|
||||||
|
# Initialize age system
|
||||||
|
age_config = _get_age_config()
|
||||||
|
if self.age == -1:
|
||||||
|
# Random starting age within configured range
|
||||||
|
self.age = random.randint(age_config.min_start_age, age_config.max_start_age)
|
||||||
|
if self.max_age == -1:
|
||||||
|
# Calculate max age with variance
|
||||||
|
variance = random.randint(-age_config.max_age_variance, age_config.max_age_variance)
|
||||||
|
self.max_age = age_config.base_max_age + variance
|
||||||
|
|
||||||
|
# Apply age-based max energy adjustment for old agents
|
||||||
|
if self.get_age_category() == "old":
|
||||||
|
self.stats.MAX_ENERGY = int(self.stats.MAX_ENERGY * age_config.old_max_energy_multiplier)
|
||||||
|
self.stats.energy = min(self.stats.energy, self.stats.MAX_ENERGY)
|
||||||
|
|
||||||
# Update profession based on personality and skills
|
# Update profession based on personality and skills
|
||||||
self._update_profession()
|
self._update_profession()
|
||||||
|
|
||||||
@ -282,6 +327,111 @@ class Agent:
|
|||||||
}
|
}
|
||||||
self.profession = profession_map.get(prof_type, Profession.VILLAGER)
|
self.profession = profession_map.get(prof_type, Profession.VILLAGER)
|
||||||
|
|
||||||
|
def get_age_category(self) -> str:
|
||||||
|
"""Get the agent's age category: 'young', 'prime', or 'old'."""
|
||||||
|
age_config = _get_age_config()
|
||||||
|
if self.age < age_config.young_age_threshold:
|
||||||
|
return "young"
|
||||||
|
elif self.age <= age_config.old_age_threshold:
|
||||||
|
return "prime"
|
||||||
|
else:
|
||||||
|
return "old"
|
||||||
|
|
||||||
|
def get_skill_modifier(self) -> float:
|
||||||
|
"""Get skill effectiveness modifier based on age.
|
||||||
|
|
||||||
|
Young agents are less effective but learn faster.
|
||||||
|
Old agents are more effective (wisdom) but learn slower.
|
||||||
|
"""
|
||||||
|
age_config = _get_age_config()
|
||||||
|
category = self.get_age_category()
|
||||||
|
if category == "young":
|
||||||
|
return age_config.young_skill_multiplier
|
||||||
|
elif category == "prime":
|
||||||
|
return age_config.prime_skill_multiplier
|
||||||
|
else:
|
||||||
|
return age_config.old_skill_multiplier
|
||||||
|
|
||||||
|
def get_learning_modifier(self) -> float:
|
||||||
|
"""Get learning rate modifier based on age."""
|
||||||
|
age_config = _get_age_config()
|
||||||
|
category = self.get_age_category()
|
||||||
|
if category == "young":
|
||||||
|
return age_config.young_learning_multiplier
|
||||||
|
elif category == "prime":
|
||||||
|
return age_config.prime_learning_multiplier
|
||||||
|
else:
|
||||||
|
return age_config.old_learning_multiplier
|
||||||
|
|
||||||
|
def get_energy_cost_modifier(self) -> float:
|
||||||
|
"""Get energy cost modifier based on age.
|
||||||
|
|
||||||
|
Young agents use less energy.
|
||||||
|
Old agents use more energy.
|
||||||
|
"""
|
||||||
|
age_config = _get_age_config()
|
||||||
|
category = self.get_age_category()
|
||||||
|
if category == "young":
|
||||||
|
return age_config.young_energy_cost_multiplier
|
||||||
|
elif category == "prime":
|
||||||
|
return age_config.prime_energy_cost_multiplier
|
||||||
|
else:
|
||||||
|
return age_config.old_energy_cost_multiplier
|
||||||
|
|
||||||
|
def get_decay_modifier(self) -> float:
|
||||||
|
"""Get stat decay modifier based on age.
|
||||||
|
|
||||||
|
Old agents decay faster (frailer).
|
||||||
|
"""
|
||||||
|
age_config = _get_age_config()
|
||||||
|
if self.get_age_category() == "old":
|
||||||
|
return age_config.old_decay_multiplier
|
||||||
|
return 1.0
|
||||||
|
|
||||||
|
def age_one_day(self) -> None:
|
||||||
|
"""Age the agent by one day (called at day transition)."""
|
||||||
|
age_config = _get_age_config()
|
||||||
|
self.age += age_config.age_per_day
|
||||||
|
|
||||||
|
# Check if agent just became old - reduce max energy
|
||||||
|
if self.age == age_config.old_age_threshold + 1:
|
||||||
|
self.stats.MAX_ENERGY = int(self.stats.MAX_ENERGY * age_config.old_max_energy_multiplier)
|
||||||
|
self.stats.energy = min(self.stats.energy, self.stats.MAX_ENERGY)
|
||||||
|
|
||||||
|
def is_too_old(self) -> bool:
|
||||||
|
"""Check if agent has exceeded their maximum age."""
|
||||||
|
return self.age >= self.max_age
|
||||||
|
|
||||||
|
def can_give_birth(self, current_day: int) -> bool:
|
||||||
|
"""Check if agent is eligible to give birth."""
|
||||||
|
age_config = _get_age_config()
|
||||||
|
|
||||||
|
# Age check
|
||||||
|
if self.age < age_config.min_birth_age or self.age > age_config.max_birth_age:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Cooldown check
|
||||||
|
days_since_birth = current_day - self.last_birth_day
|
||||||
|
if days_since_birth < age_config.birth_cooldown_days:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Resource check
|
||||||
|
if self.stats.hunger < age_config.birth_food_requirement:
|
||||||
|
return False
|
||||||
|
if self.stats.energy < age_config.birth_energy_requirement:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def record_birth(self, current_day: int, child_id: str) -> None:
|
||||||
|
"""Record that this agent gave birth."""
|
||||||
|
self.last_birth_day = current_day
|
||||||
|
self.children_ids.append(child_id)
|
||||||
|
|
||||||
|
# Birth is exhausting - reduce stats
|
||||||
|
self.stats.energy = max(0, self.stats.energy - 20)
|
||||||
|
self.stats.hunger = max(0, self.stats.hunger - 30)
|
||||||
|
|
||||||
def record_action(self, action_type: str) -> None:
|
def record_action(self, action_type: str) -> None:
|
||||||
"""Record an action for profession tracking."""
|
"""Record an action for profession tracking."""
|
||||||
if action_type in self.actions_performed:
|
if action_type in self.actions_performed:
|
||||||
@ -308,11 +458,13 @@ class Agent:
|
|||||||
|
|
||||||
def is_alive(self) -> bool:
|
def is_alive(self) -> bool:
|
||||||
"""Check if the agent is still alive."""
|
"""Check if the agent is still alive."""
|
||||||
return (
|
# Death by needs
|
||||||
self.stats.hunger > 0 and
|
if self.stats.hunger <= 0 or self.stats.thirst <= 0 or self.stats.heat <= 0:
|
||||||
self.stats.thirst > 0 and
|
return False
|
||||||
self.stats.heat > 0
|
# Death by old age
|
||||||
)
|
if self.is_too_old():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
def is_corpse(self) -> bool:
|
def is_corpse(self) -> bool:
|
||||||
"""Check if this agent is a corpse (died but still visible)."""
|
"""Check if this agent is a corpse (died but still visible)."""
|
||||||
@ -493,8 +645,9 @@ class Agent:
|
|||||||
return expired
|
return expired
|
||||||
|
|
||||||
def apply_passive_decay(self) -> None:
|
def apply_passive_decay(self) -> None:
|
||||||
"""Apply passive stat decay for this turn."""
|
"""Apply passive stat decay for this turn, modified by age."""
|
||||||
self.stats.apply_passive_decay(has_clothes=self.has_clothes())
|
decay_modifier = self.get_decay_modifier()
|
||||||
|
self.stats.apply_passive_decay(has_clothes=self.has_clothes(), decay_modifier=decay_modifier)
|
||||||
|
|
||||||
def mark_dead(self, turn: int, reason: str) -> None:
|
def mark_dead(self, turn: int, reason: str) -> None:
|
||||||
"""Mark this agent as dead."""
|
"""Mark this agent as dead."""
|
||||||
@ -522,6 +675,18 @@ class Agent:
|
|||||||
"last_action_result": self.last_action_result,
|
"last_action_result": self.last_action_result,
|
||||||
"death_turn": self.death_turn,
|
"death_turn": self.death_turn,
|
||||||
"death_reason": self.death_reason,
|
"death_reason": self.death_reason,
|
||||||
|
# Age system
|
||||||
|
"age": self.age,
|
||||||
|
"max_age": self.max_age,
|
||||||
|
"age_category": self.get_age_category(),
|
||||||
|
"birth_day": self.birth_day,
|
||||||
|
"generation": self.generation,
|
||||||
|
"parent_ids": self.parent_ids.copy(),
|
||||||
|
"children_count": len(self.children_ids),
|
||||||
|
# Age modifiers (for UI display)
|
||||||
|
"skill_modifier": round(self.get_skill_modifier(), 2),
|
||||||
|
"energy_cost_modifier": round(self.get_energy_cost_modifier(), 2),
|
||||||
|
"learning_modifier": round(self.get_learning_modifier(), 2),
|
||||||
# New fields for agent diversity
|
# New fields for agent diversity
|
||||||
"personality": self.personality.to_dict(),
|
"personality": self.personality.to_dict(),
|
||||||
"skills": self.skills.to_dict(),
|
"skills": self.skills.to_dict(),
|
||||||
|
|||||||
@ -112,11 +112,20 @@ class Skills:
|
|||||||
# Minimum skill level
|
# Minimum skill level
|
||||||
MIN_SKILL: float = 0.5
|
MIN_SKILL: float = 0.5
|
||||||
|
|
||||||
def improve(self, skill_name: str, amount: Optional[float] = None) -> None:
|
def improve(self, skill_name: str, amount: Optional[float] = None, learning_modifier: float = 1.0) -> None:
|
||||||
"""Improve a skill through practice."""
|
"""Improve a skill through practice.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
skill_name: Name of the skill to improve
|
||||||
|
amount: Base improvement amount (defaults to IMPROVEMENT_RATE)
|
||||||
|
learning_modifier: Age-based modifier (young learn faster, old learn slower)
|
||||||
|
"""
|
||||||
if amount is None:
|
if amount is None:
|
||||||
amount = self.IMPROVEMENT_RATE
|
amount = self.IMPROVEMENT_RATE
|
||||||
|
|
||||||
|
# Apply learning modifier (young agents learn faster)
|
||||||
|
amount = amount * learning_modifier
|
||||||
|
|
||||||
if hasattr(self, skill_name):
|
if hasattr(self, skill_name):
|
||||||
current = getattr(self, skill_name)
|
current = getattr(self, skill_name)
|
||||||
new_value = min(self.MAX_SKILL, current + amount)
|
new_value = min(self.MAX_SKILL, current + amount)
|
||||||
@ -209,22 +218,35 @@ def generate_random_personality(archetype: Optional[str] = None) -> PersonalityT
|
|||||||
return traits
|
return traits
|
||||||
|
|
||||||
|
|
||||||
def generate_random_skills(personality: PersonalityTraits) -> Skills:
|
def generate_random_skills(personality: PersonalityTraits, age: Optional[int] = None) -> Skills:
|
||||||
"""Generate starting skills influenced by personality.
|
"""Generate starting skills influenced by personality and age.
|
||||||
|
|
||||||
Agents with strong preferences start with slightly better skills
|
Agents with strong preferences start with slightly better skills
|
||||||
in those areas (natural talent).
|
in those areas (natural talent).
|
||||||
|
|
||||||
|
Older agents start with higher skills (life experience).
|
||||||
"""
|
"""
|
||||||
# Base skill level with small random variation
|
# Base skill level with small random variation
|
||||||
base = 1.0
|
base = 1.0
|
||||||
variance = 0.15
|
variance = 0.15
|
||||||
|
|
||||||
|
# Age bonus: older agents have more experience
|
||||||
|
age_bonus = 0.0
|
||||||
|
if age is not None:
|
||||||
|
# Young agents (< 25): no bonus
|
||||||
|
# Prime agents (25-45): small bonus
|
||||||
|
# Old agents (> 45): larger bonus (wisdom)
|
||||||
|
if age >= 45:
|
||||||
|
age_bonus = 0.3 + random.uniform(0, 0.2)
|
||||||
|
elif age >= 25:
|
||||||
|
age_bonus = (age - 25) * 0.01 + random.uniform(0, 0.1)
|
||||||
|
|
||||||
skills = Skills(
|
skills = Skills(
|
||||||
hunting=base + random.uniform(-variance, variance) + (personality.hunt_preference - 1.0) * 0.1,
|
hunting=base + random.uniform(-variance, variance) + (personality.hunt_preference - 1.0) * 0.1 + age_bonus,
|
||||||
gathering=base + random.uniform(-variance, variance) + (personality.gather_preference - 1.0) * 0.1,
|
gathering=base + random.uniform(-variance, variance) + (personality.gather_preference - 1.0) * 0.1 + age_bonus,
|
||||||
woodcutting=base + random.uniform(-variance, variance) + (personality.woodcut_preference - 1.0) * 0.1,
|
woodcutting=base + random.uniform(-variance, variance) + (personality.woodcut_preference - 1.0) * 0.1 + age_bonus,
|
||||||
trading=base + random.uniform(-variance, variance) + (personality.trade_preference - 1.0) * 0.1,
|
trading=base + random.uniform(-variance, variance) + (personality.trade_preference - 1.0) * 0.1 + age_bonus,
|
||||||
crafting=base + random.uniform(-variance, variance),
|
crafting=base + random.uniform(-variance, variance) + age_bonus,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clamp all skills to valid range
|
# Clamp all skills to valid range
|
||||||
|
|||||||
91
config.json
91
config.json
@ -11,47 +11,98 @@
|
|||||||
"max_thirst": 100,
|
"max_thirst": 100,
|
||||||
"max_heat": 100,
|
"max_heat": 100,
|
||||||
"start_energy": 50,
|
"start_energy": 50,
|
||||||
"start_hunger": 70,
|
"start_hunger": 80,
|
||||||
"start_thirst": 75,
|
"start_thirst": 85,
|
||||||
"start_heat": 100,
|
"start_heat": 100,
|
||||||
"energy_decay": 1,
|
"energy_decay": 1,
|
||||||
"hunger_decay": 2,
|
"hunger_decay": 2,
|
||||||
"thirst_decay": 3,
|
"thirst_decay": 2,
|
||||||
"heat_decay": 3,
|
"heat_decay": 2,
|
||||||
"critical_threshold": 0.25,
|
"critical_threshold": 0.25,
|
||||||
"low_energy_threshold": 12
|
"low_energy_threshold": 12
|
||||||
},
|
},
|
||||||
|
"age": {
|
||||||
|
"min_start_age": 18,
|
||||||
|
"max_start_age": 28,
|
||||||
|
"young_age_threshold": 25,
|
||||||
|
"prime_age_start": 25,
|
||||||
|
"prime_age_end": 50,
|
||||||
|
"old_age_threshold": 50,
|
||||||
|
"base_max_age": 75,
|
||||||
|
"max_age_variance": 8,
|
||||||
|
"age_per_day": 1,
|
||||||
|
"birth_cooldown_days": 8,
|
||||||
|
"min_birth_age": 20,
|
||||||
|
"max_birth_age": 50,
|
||||||
|
"birth_base_chance": 0.06,
|
||||||
|
"birth_prosperity_multiplier": 2.5,
|
||||||
|
"birth_food_requirement": 40,
|
||||||
|
"birth_energy_requirement": 15,
|
||||||
|
"birth_wealth_transfer": 0.15,
|
||||||
|
"inheritance_enabled": true,
|
||||||
|
"child_start_age": 18,
|
||||||
|
"young_skill_multiplier": 0.85,
|
||||||
|
"young_learning_multiplier": 1.3,
|
||||||
|
"young_energy_cost_multiplier": 0.9,
|
||||||
|
"prime_skill_multiplier": 1.0,
|
||||||
|
"prime_learning_multiplier": 1.0,
|
||||||
|
"prime_energy_cost_multiplier": 1.0,
|
||||||
|
"old_skill_multiplier": 1.1,
|
||||||
|
"old_learning_multiplier": 0.7,
|
||||||
|
"old_energy_cost_multiplier": 1.15,
|
||||||
|
"old_max_energy_multiplier": 0.8,
|
||||||
|
"old_decay_multiplier": 1.1
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"village_meat_limit": 200,
|
||||||
|
"village_berries_limit": 300,
|
||||||
|
"village_water_limit": 400,
|
||||||
|
"village_wood_limit": 400,
|
||||||
|
"village_hide_limit": 150,
|
||||||
|
"village_clothes_limit": 100,
|
||||||
|
"market_order_limit_per_agent": 5,
|
||||||
|
"market_total_order_limit": 500
|
||||||
|
},
|
||||||
|
"sinks": {
|
||||||
|
"daily_village_decay_rate": 0.01,
|
||||||
|
"daily_tax_rate": 0.005,
|
||||||
|
"random_event_chance": 0.02,
|
||||||
|
"fire_event_resource_loss": 0.05,
|
||||||
|
"theft_event_money_loss": 0.03,
|
||||||
|
"clothes_maintenance_per_day": 1,
|
||||||
|
"fire_wood_cost_per_night": 1
|
||||||
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"meat_decay": 10,
|
"meat_decay": 12,
|
||||||
"berries_decay": 6,
|
"berries_decay": 8,
|
||||||
"clothes_decay": 20,
|
"clothes_decay": 30,
|
||||||
"meat_hunger": 45,
|
"meat_hunger": 45,
|
||||||
"meat_energy": 15,
|
"meat_energy": 15,
|
||||||
"berries_hunger": 8,
|
"berries_hunger": 10,
|
||||||
"berries_thirst": 2,
|
"berries_thirst": 3,
|
||||||
"water_thirst": 50,
|
"water_thirst": 50,
|
||||||
"fire_heat": 20
|
"fire_heat": 25
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"sleep_energy": 55,
|
"sleep_energy": 55,
|
||||||
"rest_energy": 12,
|
"rest_energy": 12,
|
||||||
"hunt_energy": -5,
|
"hunt_energy": -5,
|
||||||
"gather_energy": -4,
|
"gather_energy": -3,
|
||||||
"chop_wood_energy": -6,
|
"chop_wood_energy": -5,
|
||||||
"get_water_energy": -2,
|
"get_water_energy": -2,
|
||||||
"weave_energy": -6,
|
"weave_energy": -5,
|
||||||
"build_fire_energy": -4,
|
"build_fire_energy": -3,
|
||||||
"trade_energy": -1,
|
"trade_energy": -1,
|
||||||
"hunt_success": 0.85,
|
"hunt_success": 0.85,
|
||||||
"chop_wood_success": 0.9,
|
"chop_wood_success": 0.9,
|
||||||
"hunt_meat_min": 2,
|
"hunt_meat_min": 2,
|
||||||
"hunt_meat_max": 4,
|
"hunt_meat_max": 5,
|
||||||
"hunt_hide_min": 0,
|
"hunt_hide_min": 0,
|
||||||
"hunt_hide_max": 2,
|
"hunt_hide_max": 2,
|
||||||
"gather_min": 2,
|
"gather_min": 3,
|
||||||
"gather_max": 4,
|
"gather_max": 5,
|
||||||
"chop_wood_min": 1,
|
"chop_wood_min": 2,
|
||||||
"chop_wood_max": 3
|
"chop_wood_max": 4
|
||||||
},
|
},
|
||||||
"world": {
|
"world": {
|
||||||
"width": 25,
|
"width": 25,
|
||||||
@ -59,7 +110,7 @@
|
|||||||
"initial_agents": 25,
|
"initial_agents": 25,
|
||||||
"day_steps": 10,
|
"day_steps": 10,
|
||||||
"night_steps": 1,
|
"night_steps": 1,
|
||||||
"inventory_slots": 12,
|
"inventory_slots": 15,
|
||||||
"starting_money": 8000
|
"starting_money": 8000
|
||||||
},
|
},
|
||||||
"market": {
|
"market": {
|
||||||
|
|||||||
@ -11,47 +11,98 @@
|
|||||||
"max_thirst": 100,
|
"max_thirst": 100,
|
||||||
"max_heat": 100,
|
"max_heat": 100,
|
||||||
"start_energy": 50,
|
"start_energy": 50,
|
||||||
"start_hunger": 70,
|
"start_hunger": 80,
|
||||||
"start_thirst": 75,
|
"start_thirst": 85,
|
||||||
"start_heat": 100,
|
"start_heat": 100,
|
||||||
"energy_decay": 1,
|
"energy_decay": 1,
|
||||||
"hunger_decay": 2,
|
"hunger_decay": 2,
|
||||||
"thirst_decay": 3,
|
"thirst_decay": 2,
|
||||||
"heat_decay": 3,
|
"heat_decay": 2,
|
||||||
"critical_threshold": 0.25,
|
"critical_threshold": 0.25,
|
||||||
"low_energy_threshold": 12
|
"low_energy_threshold": 12
|
||||||
},
|
},
|
||||||
|
"age": {
|
||||||
|
"min_start_age": 18,
|
||||||
|
"max_start_age": 28,
|
||||||
|
"young_age_threshold": 25,
|
||||||
|
"prime_age_start": 25,
|
||||||
|
"prime_age_end": 50,
|
||||||
|
"old_age_threshold": 50,
|
||||||
|
"base_max_age": 75,
|
||||||
|
"max_age_variance": 8,
|
||||||
|
"age_per_day": 1,
|
||||||
|
"birth_cooldown_days": 8,
|
||||||
|
"min_birth_age": 20,
|
||||||
|
"max_birth_age": 50,
|
||||||
|
"birth_base_chance": 0.06,
|
||||||
|
"birth_prosperity_multiplier": 2.5,
|
||||||
|
"birth_food_requirement": 40,
|
||||||
|
"birth_energy_requirement": 15,
|
||||||
|
"birth_wealth_transfer": 0.15,
|
||||||
|
"inheritance_enabled": true,
|
||||||
|
"child_start_age": 18,
|
||||||
|
"young_skill_multiplier": 0.85,
|
||||||
|
"young_learning_multiplier": 1.3,
|
||||||
|
"young_energy_cost_multiplier": 0.9,
|
||||||
|
"prime_skill_multiplier": 1.0,
|
||||||
|
"prime_learning_multiplier": 1.0,
|
||||||
|
"prime_energy_cost_multiplier": 1.0,
|
||||||
|
"old_skill_multiplier": 1.1,
|
||||||
|
"old_learning_multiplier": 0.7,
|
||||||
|
"old_energy_cost_multiplier": 1.15,
|
||||||
|
"old_max_energy_multiplier": 0.8,
|
||||||
|
"old_decay_multiplier": 1.1
|
||||||
|
},
|
||||||
|
"storage": {
|
||||||
|
"village_meat_limit": 200,
|
||||||
|
"village_berries_limit": 300,
|
||||||
|
"village_water_limit": 400,
|
||||||
|
"village_wood_limit": 400,
|
||||||
|
"village_hide_limit": 150,
|
||||||
|
"village_clothes_limit": 100,
|
||||||
|
"market_order_limit_per_agent": 5,
|
||||||
|
"market_total_order_limit": 500
|
||||||
|
},
|
||||||
|
"sinks": {
|
||||||
|
"daily_village_decay_rate": 0.01,
|
||||||
|
"daily_tax_rate": 0.005,
|
||||||
|
"random_event_chance": 0.02,
|
||||||
|
"fire_event_resource_loss": 0.05,
|
||||||
|
"theft_event_money_loss": 0.03,
|
||||||
|
"clothes_maintenance_per_day": 1,
|
||||||
|
"fire_wood_cost_per_night": 1
|
||||||
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
"meat_decay": 10,
|
"meat_decay": 12,
|
||||||
"berries_decay": 6,
|
"berries_decay": 8,
|
||||||
"clothes_decay": 20,
|
"clothes_decay": 30,
|
||||||
"meat_hunger": 45,
|
"meat_hunger": 45,
|
||||||
"meat_energy": 15,
|
"meat_energy": 15,
|
||||||
"berries_hunger": 8,
|
"berries_hunger": 10,
|
||||||
"berries_thirst": 2,
|
"berries_thirst": 3,
|
||||||
"water_thirst": 50,
|
"water_thirst": 50,
|
||||||
"fire_heat": 20
|
"fire_heat": 25
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"sleep_energy": 55,
|
"sleep_energy": 55,
|
||||||
"rest_energy": 12,
|
"rest_energy": 12,
|
||||||
"hunt_energy": -5,
|
"hunt_energy": -5,
|
||||||
"gather_energy": -4,
|
"gather_energy": -3,
|
||||||
"chop_wood_energy": -6,
|
"chop_wood_energy": -5,
|
||||||
"get_water_energy": -2,
|
"get_water_energy": -2,
|
||||||
"weave_energy": -6,
|
"weave_energy": -5,
|
||||||
"build_fire_energy": -4,
|
"build_fire_energy": -3,
|
||||||
"trade_energy": -1,
|
"trade_energy": -1,
|
||||||
"hunt_success": 0.85,
|
"hunt_success": 0.85,
|
||||||
"chop_wood_success": 0.9,
|
"chop_wood_success": 0.9,
|
||||||
"hunt_meat_min": 2,
|
"hunt_meat_min": 2,
|
||||||
"hunt_meat_max": 4,
|
"hunt_meat_max": 5,
|
||||||
"hunt_hide_min": 0,
|
"hunt_hide_min": 0,
|
||||||
"hunt_hide_max": 2,
|
"hunt_hide_max": 2,
|
||||||
"gather_min": 2,
|
"gather_min": 3,
|
||||||
"gather_max": 4,
|
"gather_max": 5,
|
||||||
"chop_wood_min": 1,
|
"chop_wood_min": 2,
|
||||||
"chop_wood_max": 3
|
"chop_wood_max": 4
|
||||||
},
|
},
|
||||||
"world": {
|
"world": {
|
||||||
"width": 25,
|
"width": 25,
|
||||||
@ -59,7 +110,7 @@
|
|||||||
"initial_agents": 25,
|
"initial_agents": 25,
|
||||||
"day_steps": 10,
|
"day_steps": 10,
|
||||||
"night_steps": 1,
|
"night_steps": 1,
|
||||||
"inventory_slots": 12,
|
"inventory_slots": 15,
|
||||||
"starting_money": 80
|
"starting_money": 80
|
||||||
},
|
},
|
||||||
"market": {
|
"market": {
|
||||||
@ -69,6 +120,7 @@
|
|||||||
},
|
},
|
||||||
"economy": {
|
"economy": {
|
||||||
"energy_to_money_ratio": 1.5,
|
"energy_to_money_ratio": 1.5,
|
||||||
|
"min_price": 1,
|
||||||
"wealth_desire": 0.35,
|
"wealth_desire": 0.35,
|
||||||
"buy_efficiency_threshold": 0.75,
|
"buy_efficiency_threshold": 0.75,
|
||||||
"min_wealth_target": 50,
|
"min_wealth_target": 50,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user