[upd] Add birth and age-based death and modifiers

This commit is contained in:
Снесарев Максим 2026-01-19 22:55:26 +03:00
parent 25bd13e001
commit d190e3efe5
9 changed files with 1163 additions and 195 deletions

View File

@ -55,9 +55,31 @@ class AgentResponse(BaseModel):
inventory: list[ResourceSchema]
money: int
is_alive: bool
is_corpse: bool = False
can_act: bool
current_action: AgentActionSchema
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 ==============
@ -119,6 +141,7 @@ class StatisticsSchema(BaseModel):
living_agents: int
total_agents_spawned: int
total_agents_died: int
total_births: int = 0
total_money_in_circulation: int
professions: dict[str, int]
# Wealth inequality metrics
@ -127,6 +150,16 @@ class StatisticsSchema(BaseModel):
richest_agent: int = 0
poorest_agent: int = 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):
@ -142,10 +175,12 @@ class TurnLogSchema(BaseModel):
turn: int
agent_actions: list[ActionLogSchema]
deaths: list[str]
births: list[str] = []
trades: list[dict]
resources_produced: dict[str, int] = {}
resources_consumed: dict[str, int] = {}
resources_spoiled: dict[str, int] = {}
day_events: dict = {}
class ResourceStatsSchema(BaseModel):

View File

@ -151,6 +151,105 @@ class AIConfig:
reactive_fallback: bool = True
@dataclass
class AgeConfig:
"""Configuration for the age and lifecycle system.
Age affects skills, energy costs, and creates birth/death cycles.
Age is measured in "years" where 1 year = 1 simulation day.
Population is controlled by economy:
- Birth rate scales with village prosperity (food availability)
- Parents transfer wealth to children at birth and death
"""
# Starting age range for initial agents
min_start_age: int = 18
max_start_age: int = 35
# Age category thresholds
young_age_threshold: int = 25 # Below this = young
prime_age_start: int = 25 # Prime age begins
prime_age_end: int = 50 # Prime age ends
old_age_threshold: int = 50 # Above this = old
# Lifespan
base_max_age: int = 75 # Base maximum age
max_age_variance: int = 10 # ± variance for max age
age_per_day: int = 1 # How many "years" per sim day
# Birth mechanics - economy controlled
birth_cooldown_days: int = 20 # Days after birth before can birth again
min_birth_age: int = 20 # Minimum age to give birth
max_birth_age: int = 45 # Maximum age to give birth
birth_base_chance: float = 0.02 # Base chance of birth per day
birth_prosperity_multiplier: float = 3.0 # Max multiplier based on food abundance
birth_food_requirement: int = 60 # Min hunger to attempt birth
birth_energy_requirement: int = 25 # Min energy to attempt birth
# Wealth transfer
birth_wealth_transfer: float = 0.25 # Parent gives 25% wealth to child at birth
inheritance_enabled: bool = True # Children inherit from dead parents
child_start_age: int = 18 # Age children start at (adult)
# Age modifiers for YOUNG agents (learning phase)
young_skill_multiplier: float = 0.8 # Skills are 80% effective
young_learning_multiplier: float = 1.4 # Learn 40% faster
young_energy_cost_multiplier: float = 0.85 # 15% less energy cost
# Age modifiers for PRIME agents (peak performance)
prime_skill_multiplier: float = 1.0
prime_learning_multiplier: float = 1.0
prime_energy_cost_multiplier: float = 1.0
# Age modifiers for OLD agents (wisdom but frailty)
old_skill_multiplier: float = 1.15 # Skills 15% more effective (wisdom)
old_learning_multiplier: float = 0.6 # Learn 40% slower
old_energy_cost_multiplier: float = 1.2 # 20% more energy cost
old_max_energy_multiplier: float = 0.75 # 25% less max energy
old_decay_multiplier: float = 1.15 # 15% faster stat decay
@dataclass
class StorageConfig:
"""Configuration for resource storage limits.
Limits the total resources that can exist in the village economy.
"""
# Village-wide storage limits per resource type
village_meat_limit: int = 100
village_berries_limit: int = 150
village_water_limit: int = 200
village_wood_limit: int = 200
village_hide_limit: int = 80
village_clothes_limit: int = 50
# Market limits
market_order_limit_per_agent: int = 5 # Max active orders per agent
market_total_order_limit: int = 500 # Max total market orders
@dataclass
class SinksConfig:
"""Configuration for resource sinks (ways resources leave the economy).
These create pressure to keep producing resources rather than hoarding.
"""
# Daily decay of village storage (percentage)
daily_village_decay_rate: float = 0.02 # 2% of stored resources decay daily
# Money tax (redistributed or removed)
daily_tax_rate: float = 0.01 # 1% wealth tax per day
# Random events
random_event_chance: float = 0.05 # 5% chance of event per day
fire_event_resource_loss: float = 0.1 # 10% resources lost in fire
theft_event_money_loss: float = 0.05 # 5% money stolen
# Maintenance costs
clothes_maintenance_per_day: int = 1 # Clothes degrade 1 durability/day
fire_wood_cost_per_night: int = 1 # Wood consumed to stay warm at night
@dataclass
class SimulationConfig:
"""Master configuration containing all sub-configs."""
@ -161,6 +260,9 @@ class SimulationConfig:
market: MarketConfig = field(default_factory=MarketConfig)
economy: EconomyConfig = field(default_factory=EconomyConfig)
ai: AIConfig = field(default_factory=AIConfig)
age: AgeConfig = field(default_factory=AgeConfig)
storage: StorageConfig = field(default_factory=StorageConfig)
sinks: SinksConfig = field(default_factory=SinksConfig)
# Simulation control
auto_step_interval: float = 1.0 # Seconds between auto steps
@ -175,6 +277,9 @@ class SimulationConfig:
"world": asdict(self.world),
"market": asdict(self.market),
"economy": asdict(self.economy),
"age": asdict(self.age),
"storage": asdict(self.storage),
"sinks": asdict(self.sinks),
"auto_step_interval": self.auto_step_interval,
}
@ -189,6 +294,9 @@ class SimulationConfig:
world=WorldConfig(**data.get("world", {})),
market=MarketConfig(**data.get("market", {})),
economy=EconomyConfig(**data.get("economy", {})),
age=AgeConfig(**data.get("age", {})),
storage=StorageConfig(**data.get("storage", {})),
sinks=SinksConfig(**data.get("sinks", {})),
auto_step_interval=data.get("auto_step_interval", 1.0),
)

View File

@ -30,21 +30,26 @@ class TurnLog:
turn: int
agent_actions: list[dict] = 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)
# Resource tracking for this turn
resources_produced: dict = field(default_factory=dict)
resources_consumed: 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:
return {
"turn": self.turn,
"agent_actions": self.agent_actions,
"deaths": self.deaths,
"births": self.births,
"trades": self.trades,
"resources_produced": self.resources_produced,
"resources_consumed": self.resources_consumed,
"resources_spoiled": self.resources_spoiled,
"day_events": self.day_events,
}
@ -281,10 +286,15 @@ class GameEngine:
# End turn logging
self.logger.end_turn()
# 8. Advance time
self.world.advance_time()
# 8. Advance time (returns True if new day started)
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:
self.is_running = False
self.logger.close()
@ -293,16 +303,32 @@ class GameEngine:
return turn_log
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 = []
for agent in self.world.agents:
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"
# 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)
# Clear their action to show death state
agent.current_action.action_type = "dead"
agent.current_action.message = f"Died: {cause}"
# Record death statistics
self.world.record_death(agent, cause)
newly_dead.append(agent)
return newly_dead
@ -316,10 +342,148 @@ class GameEngine:
for agent in to_remove:
self.world.agents.remove(agent)
self.world.total_agents_died += 1
# Note: death was already recorded in _mark_dead_agents
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]:
"""Execute an action for an agent."""
action = decision.action
@ -398,9 +562,17 @@ class GameEngine:
- Gathering skill affects gather output
- Woodcutting skill affects wood output
- 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
energy_cost = abs(config.energy_cost)
# Calculate age-modified 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):
return ActionResult(
action_type=action,
@ -428,16 +600,20 @@ class GameEngine:
# Get relevant skill for this 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_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
effective_success_chance = min(0.98, config.success_chance * skill_modifier)
if random.random() > effective_success_chance:
# Record action attempt (skill still improves on failure, just less)
agent.record_action(action.value)
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(
action_type=action,
success=False,
@ -445,14 +621,21 @@ class GameEngine:
message="Action failed",
)
# Generate output (modified by skill for quantity)
# Generate output (modified by skill and age for quantity)
resources_gained = []
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
base_quantity = random.randint(config.min_output, config.max_output)
quantity = max(config.min_output, int(base_quantity * skill_modifier))
# Limit by storage
quantity = min(quantity, storage_available)
if quantity > 0:
resource = Resource(
type=config.output_resource,
@ -467,10 +650,15 @@ class GameEngine:
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:
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)
quantity = max(0, int(base_quantity * skill_modifier))
quantity = min(quantity, storage_available)
if quantity > 0:
resource = Resource(
type=config.secondary_output,
@ -485,10 +673,11 @@ class GameEngine:
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)
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
gained_str = ", ".join(f"+{r.quantity} {r.type.value}" for r in resources_gained)
@ -569,13 +758,15 @@ class GameEngine:
if seller:
seller.money += 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.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(
action_type=ActionType.TRADE,

View File

@ -210,6 +210,22 @@ class SimulationLogger:
self._current_entry.deaths.append({"name": agent_name, "cause": 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:
"""Log end-of-turn statistics."""
if self._current_entry is None:

View File

@ -3,6 +3,8 @@
The world spawns diverse agents with varied personality traits,
skills, and starting conditions to create emergent professions
and class inequality.
Now includes age-based lifecycle with birth and death by old age.
"""
import random
@ -15,6 +17,7 @@ from backend.domain.personality import (
PersonalityTraits, Skills,
generate_random_personality, generate_random_skills
)
from backend.domain.resources import ResourceType
class TimeOfDay(Enum):
@ -68,6 +71,21 @@ class World:
# Statistics
total_agents_spawned: 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(
self,
@ -76,6 +94,9 @@ class World:
position: Optional[Position] = None,
archetype: Optional[str] = None,
starting_money: Optional[int] = None,
age: Optional[int] = None,
generation: int = 0,
parent_ids: Optional[list[str]] = None,
) -> Agent:
"""Spawn a new agent in the world with unique personality.
@ -85,6 +106,9 @@ class World:
position: Starting position (random if None)
archetype: Personality archetype ("hunter", "gatherer", "trader", etc.)
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:
position = Position(
@ -92,14 +116,21 @@ class World:
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)
skills = generate_random_skills(personality)
skills = generate_random_skills(personality, age=age)
# Variable starting money for class inequality
# Some agents start with more, some with less
if starting_money is None:
from backend.config import get_config
base_money = get_config().world.starting_money
# Random multiplier: 0.3x to 2.0x base money
# This creates natural class inequality
@ -116,12 +147,185 @@ class World:
personality=personality,
skills=skills,
money=starting_money,
age=age,
birth_day=self.current_day,
generation=generation,
parent_ids=parent_ids or [],
)
self.agents.append(agent)
self.total_agents_spawned += 1
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]:
"""Get an agent by ID."""
for agent in self.agents:
@ -137,16 +341,21 @@ class World:
# Don't actually remove here - let the engine handle corpse visualization
return dead_agents
def advance_time(self) -> None:
"""Advance the simulation time by one step."""
def advance_time(self) -> bool:
"""Advance the simulation time by one step.
Returns True if a new day started (for age/birth processing).
"""
self.current_turn += 1
self.step_in_day += 1
total_steps = self.config.day_steps + self.config.night_steps
new_day = False
if self.step_in_day > total_steps:
self.step_in_day = 1
self.current_day += 1
new_day = True
# Determine time of day
if self.step_in_day <= self.config.day_steps:
@ -154,6 +363,91 @@ class World:
else:
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:
"""Check if it's currently 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()]
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()
total_money = sum(a.money for a in living)
@ -174,6 +468,21 @@ class World:
prof = agent.profession.value
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
if living:
moneys = sorted([a.money for a in living])
@ -192,6 +501,9 @@ class World:
else:
avg_money = median_money = richest = poorest = gini = 0
# Update village storage
self.update_village_storage()
return {
"current_turn": self.current_turn,
"current_day": self.current_day,
@ -200,6 +512,7 @@ class World:
"living_agents": len(living),
"total_agents_spawned": self.total_agents_spawned,
"total_agents_died": self.total_agents_died,
"total_births": self.total_births,
"total_money_in_circulation": total_money,
"professions": profession_counts,
# Wealth inequality metrics
@ -208,6 +521,21 @@ class World:
"richest_agent": richest,
"poorest_agent": poorest,
"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:

View File

@ -25,6 +25,12 @@ def _get_agent_stats_config():
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):
"""Agent professions - now derived from personality and skills."""
VILLAGER = "villager"
@ -96,14 +102,24 @@ class AgentStats:
# Critical threshold - loaded from config
CRITICAL_THRESHOLD: float = field(default=0.25)
def apply_passive_decay(self, has_clothes: bool = False) -> None:
"""Apply passive stat decay each turn."""
self.energy = max(0, self.energy - self.ENERGY_DECAY)
self.hunger = max(0, self.hunger - self.HUNGER_DECAY)
self.thirst = max(0, self.thirst - self.THIRST_DECAY)
def apply_passive_decay(self, has_clothes: bool = False, decay_modifier: float = 1.0) -> None:
"""Apply passive stat decay each turn.
Args:
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%
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)
def is_critical(self) -> bool:
@ -217,6 +233,11 @@ class Agent:
Stats, inventory slots, and starting money are loaded from config.json.
Each agent now has unique personality traits and skills that create
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])
name: str = ""
@ -230,6 +251,15 @@ class Agent:
personality: PersonalityTraits = field(default_factory=PersonalityTraits)
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
home_position: Position = field(default_factory=Position)
current_action: AgentAction = field(default_factory=AgentAction)
@ -267,6 +297,21 @@ class Agent:
if self.INVENTORY_SLOTS == -1:
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
self._update_profession()
@ -282,6 +327,111 @@ class Agent:
}
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:
"""Record an action for profession tracking."""
if action_type in self.actions_performed:
@ -308,11 +458,13 @@ class Agent:
def is_alive(self) -> bool:
"""Check if the agent is still alive."""
return (
self.stats.hunger > 0 and
self.stats.thirst > 0 and
self.stats.heat > 0
)
# Death by needs
if self.stats.hunger <= 0 or self.stats.thirst <= 0 or self.stats.heat <= 0:
return False
# Death by old age
if self.is_too_old():
return False
return True
def is_corpse(self) -> bool:
"""Check if this agent is a corpse (died but still visible)."""
@ -493,8 +645,9 @@ class Agent:
return expired
def apply_passive_decay(self) -> None:
"""Apply passive stat decay for this turn."""
self.stats.apply_passive_decay(has_clothes=self.has_clothes())
"""Apply passive stat decay for this turn, modified by age."""
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:
"""Mark this agent as dead."""
@ -522,6 +675,18 @@ class Agent:
"last_action_result": self.last_action_result,
"death_turn": self.death_turn,
"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
"personality": self.personality.to_dict(),
"skills": self.skills.to_dict(),

View File

@ -112,11 +112,20 @@ class Skills:
# Minimum skill level
MIN_SKILL: float = 0.5
def improve(self, skill_name: str, amount: Optional[float] = None) -> None:
"""Improve a skill through practice."""
def improve(self, skill_name: str, amount: Optional[float] = None, learning_modifier: float = 1.0) -> None:
"""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:
amount = self.IMPROVEMENT_RATE
# Apply learning modifier (young agents learn faster)
amount = amount * learning_modifier
if hasattr(self, skill_name):
current = getattr(self, skill_name)
new_value = min(self.MAX_SKILL, current + amount)
@ -209,22 +218,35 @@ def generate_random_personality(archetype: Optional[str] = None) -> PersonalityT
return traits
def generate_random_skills(personality: PersonalityTraits) -> Skills:
"""Generate starting skills influenced by personality.
def generate_random_skills(personality: PersonalityTraits, age: Optional[int] = None) -> Skills:
"""Generate starting skills influenced by personality and age.
Agents with strong preferences start with slightly better skills
in those areas (natural talent).
Older agents start with higher skills (life experience).
"""
# Base skill level with small random variation
base = 1.0
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(
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),
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 + age_bonus,
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 + age_bonus,
crafting=base + random.uniform(-variance, variance) + age_bonus,
)
# Clamp all skills to valid range

View File

@ -11,47 +11,98 @@
"max_thirst": 100,
"max_heat": 100,
"start_energy": 50,
"start_hunger": 70,
"start_thirst": 75,
"start_hunger": 80,
"start_thirst": 85,
"start_heat": 100,
"energy_decay": 1,
"hunger_decay": 2,
"thirst_decay": 3,
"heat_decay": 3,
"thirst_decay": 2,
"heat_decay": 2,
"critical_threshold": 0.25,
"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": {
"meat_decay": 10,
"berries_decay": 6,
"clothes_decay": 20,
"meat_decay": 12,
"berries_decay": 8,
"clothes_decay": 30,
"meat_hunger": 45,
"meat_energy": 15,
"berries_hunger": 8,
"berries_thirst": 2,
"berries_hunger": 10,
"berries_thirst": 3,
"water_thirst": 50,
"fire_heat": 20
"fire_heat": 25
},
"actions": {
"sleep_energy": 55,
"rest_energy": 12,
"hunt_energy": -5,
"gather_energy": -4,
"chop_wood_energy": -6,
"gather_energy": -3,
"chop_wood_energy": -5,
"get_water_energy": -2,
"weave_energy": -6,
"build_fire_energy": -4,
"weave_energy": -5,
"build_fire_energy": -3,
"trade_energy": -1,
"hunt_success": 0.85,
"chop_wood_success": 0.9,
"hunt_meat_min": 2,
"hunt_meat_max": 4,
"hunt_meat_max": 5,
"hunt_hide_min": 0,
"hunt_hide_max": 2,
"gather_min": 2,
"gather_max": 4,
"chop_wood_min": 1,
"chop_wood_max": 3
"gather_min": 3,
"gather_max": 5,
"chop_wood_min": 2,
"chop_wood_max": 4
},
"world": {
"width": 25,
@ -59,7 +110,7 @@
"initial_agents": 25,
"day_steps": 10,
"night_steps": 1,
"inventory_slots": 12,
"inventory_slots": 15,
"starting_money": 8000
},
"market": {

View File

@ -11,47 +11,98 @@
"max_thirst": 100,
"max_heat": 100,
"start_energy": 50,
"start_hunger": 70,
"start_thirst": 75,
"start_hunger": 80,
"start_thirst": 85,
"start_heat": 100,
"energy_decay": 1,
"hunger_decay": 2,
"thirst_decay": 3,
"heat_decay": 3,
"thirst_decay": 2,
"heat_decay": 2,
"critical_threshold": 0.25,
"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": {
"meat_decay": 10,
"berries_decay": 6,
"clothes_decay": 20,
"meat_decay": 12,
"berries_decay": 8,
"clothes_decay": 30,
"meat_hunger": 45,
"meat_energy": 15,
"berries_hunger": 8,
"berries_thirst": 2,
"berries_hunger": 10,
"berries_thirst": 3,
"water_thirst": 50,
"fire_heat": 20
"fire_heat": 25
},
"actions": {
"sleep_energy": 55,
"rest_energy": 12,
"hunt_energy": -5,
"gather_energy": -4,
"chop_wood_energy": -6,
"gather_energy": -3,
"chop_wood_energy": -5,
"get_water_energy": -2,
"weave_energy": -6,
"build_fire_energy": -4,
"weave_energy": -5,
"build_fire_energy": -3,
"trade_energy": -1,
"hunt_success": 0.85,
"chop_wood_success": 0.9,
"hunt_meat_min": 2,
"hunt_meat_max": 4,
"hunt_meat_max": 5,
"hunt_hide_min": 0,
"hunt_hide_max": 2,
"gather_min": 2,
"gather_max": 4,
"chop_wood_min": 1,
"chop_wood_max": 3
"gather_min": 3,
"gather_max": 5,
"chop_wood_min": 2,
"chop_wood_max": 4
},
"world": {
"width": 25,
@ -59,7 +110,7 @@
"initial_agents": 25,
"day_steps": 10,
"night_steps": 1,
"inventory_slots": 12,
"inventory_slots": 15,
"starting_money": 80
},
"market": {
@ -69,6 +120,7 @@
},
"economy": {
"energy_to_money_ratio": 1.5,
"min_price": 1,
"wealth_desire": 0.35,
"buy_efficiency_threshold": 0.75,
"min_wealth_target": 50,