[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
|
||||||
cause = agent.stats.get_critical_stat() or "unknown"
|
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)
|
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,
|
||||||
|
|||||||
@ -39,7 +39,7 @@ class TurnLogEntry:
|
|||||||
trades: list = field(default_factory=list)
|
trades: list = field(default_factory=list)
|
||||||
deaths: list = field(default_factory=list)
|
deaths: list = field(default_factory=list)
|
||||||
statistics: dict = field(default_factory=dict)
|
statistics: dict = field(default_factory=dict)
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
"""Convert to dictionary for JSON serialization."""
|
"""Convert to dictionary for JSON serialization."""
|
||||||
return {
|
return {
|
||||||
@ -59,24 +59,24 @@ class TurnLogEntry:
|
|||||||
|
|
||||||
class SimulationLogger:
|
class SimulationLogger:
|
||||||
"""Logger that dumps detailed simulation data to files."""
|
"""Logger that dumps detailed simulation data to files."""
|
||||||
|
|
||||||
def __init__(self, log_dir: str = "logs"):
|
def __init__(self, log_dir: str = "logs"):
|
||||||
self.log_dir = Path(log_dir)
|
self.log_dir = Path(log_dir)
|
||||||
self.log_dir.mkdir(exist_ok=True)
|
self.log_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
# Create session-specific log file
|
# Create session-specific log file
|
||||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
self.session_file = self.log_dir / f"sim_{timestamp}.jsonl"
|
self.session_file = self.log_dir / f"sim_{timestamp}.jsonl"
|
||||||
self.summary_file = self.log_dir / f"sim_{timestamp}_summary.txt"
|
self.summary_file = self.log_dir / f"sim_{timestamp}_summary.txt"
|
||||||
|
|
||||||
# File handles
|
# File handles
|
||||||
self._json_file: Optional[TextIO] = None
|
self._json_file: Optional[TextIO] = None
|
||||||
self._summary_file: Optional[TextIO] = None
|
self._summary_file: Optional[TextIO] = None
|
||||||
|
|
||||||
# Also set up standard Python logging
|
# Also set up standard Python logging
|
||||||
self.logger = logging.getLogger("simulation")
|
self.logger = logging.getLogger("simulation")
|
||||||
self.logger.setLevel(logging.DEBUG)
|
self.logger.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
# File handler for detailed logs
|
# File handler for detailed logs
|
||||||
file_handler = logging.FileHandler(self.log_dir / f"sim_{timestamp}.log")
|
file_handler = logging.FileHandler(self.log_dir / f"sim_{timestamp}.log")
|
||||||
file_handler.setLevel(logging.DEBUG)
|
file_handler.setLevel(logging.DEBUG)
|
||||||
@ -84,7 +84,7 @@ class SimulationLogger:
|
|||||||
"%(asctime)s | %(levelname)s | %(message)s"
|
"%(asctime)s | %(levelname)s | %(message)s"
|
||||||
))
|
))
|
||||||
self.logger.addHandler(file_handler)
|
self.logger.addHandler(file_handler)
|
||||||
|
|
||||||
# Console handler for important events
|
# Console handler for important events
|
||||||
console_handler = logging.StreamHandler()
|
console_handler = logging.StreamHandler()
|
||||||
console_handler.setLevel(logging.INFO)
|
console_handler.setLevel(logging.INFO)
|
||||||
@ -92,25 +92,25 @@ class SimulationLogger:
|
|||||||
"%(asctime)s | %(message)s", datefmt="%H:%M:%S"
|
"%(asctime)s | %(message)s", datefmt="%H:%M:%S"
|
||||||
))
|
))
|
||||||
self.logger.addHandler(console_handler)
|
self.logger.addHandler(console_handler)
|
||||||
|
|
||||||
self._entries: list[TurnLogEntry] = []
|
self._entries: list[TurnLogEntry] = []
|
||||||
self._current_entry: Optional[TurnLogEntry] = None
|
self._current_entry: Optional[TurnLogEntry] = None
|
||||||
|
|
||||||
def start_session(self, config: dict) -> None:
|
def start_session(self, config: dict) -> None:
|
||||||
"""Start a new logging session."""
|
"""Start a new logging session."""
|
||||||
self._json_file = open(self.session_file, "w")
|
self._json_file = open(self.session_file, "w")
|
||||||
self._summary_file = open(self.summary_file, "w")
|
self._summary_file = open(self.summary_file, "w")
|
||||||
|
|
||||||
# Write config as first line
|
# Write config as first line
|
||||||
self._json_file.write(json.dumps({"type": "config", "data": config}) + "\n")
|
self._json_file.write(json.dumps({"type": "config", "data": config}) + "\n")
|
||||||
self._json_file.flush()
|
self._json_file.flush()
|
||||||
|
|
||||||
self._summary_file.write(f"Simulation Session Started: {datetime.now()}\n")
|
self._summary_file.write(f"Simulation Session Started: {datetime.now()}\n")
|
||||||
self._summary_file.write("=" * 60 + "\n\n")
|
self._summary_file.write("=" * 60 + "\n\n")
|
||||||
self._summary_file.flush()
|
self._summary_file.flush()
|
||||||
|
|
||||||
self.logger.info(f"Logging session started: {self.session_file}")
|
self.logger.info(f"Logging session started: {self.session_file}")
|
||||||
|
|
||||||
def start_turn(self, turn: int, day: int, step_in_day: int, time_of_day: str) -> None:
|
def start_turn(self, turn: int, day: int, step_in_day: int, time_of_day: str) -> None:
|
||||||
"""Start logging a new turn."""
|
"""Start logging a new turn."""
|
||||||
self._current_entry = TurnLogEntry(
|
self._current_entry = TurnLogEntry(
|
||||||
@ -121,7 +121,7 @@ class SimulationLogger:
|
|||||||
timestamp=datetime.now().isoformat(),
|
timestamp=datetime.now().isoformat(),
|
||||||
)
|
)
|
||||||
self.logger.debug(f"Turn {turn} started (Day {day}, Step {step_in_day}, {time_of_day})")
|
self.logger.debug(f"Turn {turn} started (Day {day}, Step {step_in_day}, {time_of_day})")
|
||||||
|
|
||||||
def log_agent_before(
|
def log_agent_before(
|
||||||
self,
|
self,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
@ -135,7 +135,7 @@ class SimulationLogger:
|
|||||||
"""Log agent state before action."""
|
"""Log agent state before action."""
|
||||||
if self._current_entry is None:
|
if self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Create placeholder entry
|
# Create placeholder entry
|
||||||
entry = AgentLogEntry(
|
entry = AgentLogEntry(
|
||||||
agent_id=agent_id,
|
agent_id=agent_id,
|
||||||
@ -152,12 +152,12 @@ class SimulationLogger:
|
|||||||
money_after=money,
|
money_after=money,
|
||||||
)
|
)
|
||||||
self._current_entry.agent_entries.append(entry)
|
self._current_entry.agent_entries.append(entry)
|
||||||
|
|
||||||
def log_agent_decision(self, agent_id: str, decision: dict) -> None:
|
def log_agent_decision(self, agent_id: str, decision: dict) -> None:
|
||||||
"""Log agent's AI decision."""
|
"""Log agent's AI decision."""
|
||||||
if self._current_entry is None:
|
if self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
for entry in self._current_entry.agent_entries:
|
for entry in self._current_entry.agent_entries:
|
||||||
if entry.agent_id == agent_id:
|
if entry.agent_id == agent_id:
|
||||||
entry.decision = decision.copy()
|
entry.decision = decision.copy()
|
||||||
@ -166,7 +166,7 @@ class SimulationLogger:
|
|||||||
f"- {decision.get('reason', '')}"
|
f"- {decision.get('reason', '')}"
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
def log_agent_after(
|
def log_agent_after(
|
||||||
self,
|
self,
|
||||||
agent_id: str,
|
agent_id: str,
|
||||||
@ -179,7 +179,7 @@ class SimulationLogger:
|
|||||||
"""Log agent state after action."""
|
"""Log agent state after action."""
|
||||||
if self._current_entry is None:
|
if self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
for entry in self._current_entry.agent_entries:
|
for entry in self._current_entry.agent_entries:
|
||||||
if entry.agent_id == agent_id:
|
if entry.agent_id == agent_id:
|
||||||
entry.stats_after = stats.copy()
|
entry.stats_after = stats.copy()
|
||||||
@ -188,55 +188,71 @@ class SimulationLogger:
|
|||||||
entry.position = position.copy()
|
entry.position = position.copy()
|
||||||
entry.action_result = action_result.copy()
|
entry.action_result = action_result.copy()
|
||||||
break
|
break
|
||||||
|
|
||||||
def log_market_state(self, orders_before: list, orders_after: list) -> None:
|
def log_market_state(self, orders_before: list, orders_after: list) -> None:
|
||||||
"""Log market state."""
|
"""Log market state."""
|
||||||
if self._current_entry is None:
|
if self._current_entry is None:
|
||||||
return
|
return
|
||||||
self._current_entry.market_orders_before = orders_before
|
self._current_entry.market_orders_before = orders_before
|
||||||
self._current_entry.market_orders_after = orders_after
|
self._current_entry.market_orders_after = orders_after
|
||||||
|
|
||||||
def log_trade(self, trade: dict) -> None:
|
def log_trade(self, trade: dict) -> None:
|
||||||
"""Log a trade transaction."""
|
"""Log a trade transaction."""
|
||||||
if self._current_entry is None:
|
if self._current_entry is None:
|
||||||
return
|
return
|
||||||
self._current_entry.trades.append(trade)
|
self._current_entry.trades.append(trade)
|
||||||
self.logger.debug(f" Trade: {trade.get('message', 'Unknown trade')}")
|
self.logger.debug(f" Trade: {trade.get('message', 'Unknown trade')}")
|
||||||
|
|
||||||
def log_death(self, agent_name: str, cause: str) -> None:
|
def log_death(self, agent_name: str, cause: str) -> None:
|
||||||
"""Log an agent death."""
|
"""Log an agent death."""
|
||||||
if self._current_entry is None:
|
if self._current_entry is None:
|
||||||
return
|
return
|
||||||
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:
|
||||||
return
|
return
|
||||||
self._current_entry.statistics = stats.copy()
|
self._current_entry.statistics = stats.copy()
|
||||||
|
|
||||||
def end_turn(self) -> None:
|
def end_turn(self) -> None:
|
||||||
"""Finish logging the current turn and write to file."""
|
"""Finish logging the current turn and write to file."""
|
||||||
if self._current_entry is None:
|
if self._current_entry is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._entries.append(self._current_entry)
|
self._entries.append(self._current_entry)
|
||||||
|
|
||||||
# Write to JSON lines file
|
# Write to JSON lines file
|
||||||
if self._json_file:
|
if self._json_file:
|
||||||
self._json_file.write(
|
self._json_file.write(
|
||||||
json.dumps({"type": "turn", "data": self._current_entry.to_dict()}) + "\n"
|
json.dumps({"type": "turn", "data": self._current_entry.to_dict()}) + "\n"
|
||||||
)
|
)
|
||||||
self._json_file.flush()
|
self._json_file.flush()
|
||||||
|
|
||||||
# Write summary
|
# Write summary
|
||||||
if self._summary_file:
|
if self._summary_file:
|
||||||
entry = self._current_entry
|
entry = self._current_entry
|
||||||
self._summary_file.write(
|
self._summary_file.write(
|
||||||
f"Turn {entry.turn} | Day {entry.day} Step {entry.step_in_day} ({entry.time_of_day})\n"
|
f"Turn {entry.turn} | Day {entry.day} Step {entry.step_in_day} ({entry.time_of_day})\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
for agent in entry.agent_entries:
|
for agent in entry.agent_entries:
|
||||||
action = agent.decision.get("action", "?")
|
action = agent.decision.get("action", "?")
|
||||||
result = "✓" if agent.action_result.get("success", False) else "✗"
|
result = "✓" if agent.action_result.get("success", False) else "✗"
|
||||||
@ -247,17 +263,17 @@ class SimulationLogger:
|
|||||||
f"T:{agent.stats_after.get('thirst', '?')} "
|
f"T:{agent.stats_after.get('thirst', '?')} "
|
||||||
f"${agent.money_after}\n"
|
f"${agent.money_after}\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
if entry.deaths:
|
if entry.deaths:
|
||||||
for death in entry.deaths:
|
for death in entry.deaths:
|
||||||
self._summary_file.write(f" 💀 {death['name']} died: {death['cause']}\n")
|
self._summary_file.write(f" 💀 {death['name']} died: {death['cause']}\n")
|
||||||
|
|
||||||
self._summary_file.write("\n")
|
self._summary_file.write("\n")
|
||||||
self._summary_file.flush()
|
self._summary_file.flush()
|
||||||
|
|
||||||
self.logger.debug(f"Turn {self._current_entry.turn} completed")
|
self.logger.debug(f"Turn {self._current_entry.turn} completed")
|
||||||
self._current_entry = None
|
self._current_entry = None
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Close log files."""
|
"""Close log files."""
|
||||||
if self._json_file:
|
if self._json_file:
|
||||||
@ -268,7 +284,7 @@ class SimulationLogger:
|
|||||||
self._summary_file.close()
|
self._summary_file.close()
|
||||||
self._summary_file = None
|
self._summary_file = None
|
||||||
self.logger.info("Logging session closed")
|
self.logger.info("Logging session closed")
|
||||||
|
|
||||||
def get_entries(self) -> list[TurnLogEntry]:
|
def get_entries(self) -> list[TurnLogEntry]:
|
||||||
"""Get all logged entries."""
|
"""Get all logged entries."""
|
||||||
return self._entries.copy()
|
return self._entries.copy()
|
||||||
|
|||||||
@ -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):
|
||||||
@ -32,7 +35,7 @@ def _get_world_config_from_file():
|
|||||||
@dataclass
|
@dataclass
|
||||||
class WorldConfig:
|
class WorldConfig:
|
||||||
"""Configuration for the world.
|
"""Configuration for the world.
|
||||||
|
|
||||||
Default values are loaded from config.json via create_world_config().
|
Default values are loaded from config.json via create_world_config().
|
||||||
These hardcoded defaults are only fallbacks.
|
These hardcoded defaults are only fallbacks.
|
||||||
"""
|
"""
|
||||||
@ -64,11 +67,26 @@ class World:
|
|||||||
current_day: int = 1
|
current_day: int = 1
|
||||||
step_in_day: int = 0
|
step_in_day: int = 0
|
||||||
time_of_day: TimeOfDay = TimeOfDay.DAY
|
time_of_day: TimeOfDay = TimeOfDay.DAY
|
||||||
|
|
||||||
# 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,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
@ -76,30 +94,43 @@ 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.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name: Agent name (auto-generated if None)
|
name: Agent name (auto-generated if None)
|
||||||
profession: Deprecated, now derived from personality
|
profession: Deprecated, now derived from personality
|
||||||
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(
|
||||||
x=random.randint(0, self.config.width - 1),
|
x=random.randint(0, self.config.width - 1),
|
||||||
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
|
||||||
@ -108,7 +139,7 @@ class World:
|
|||||||
if personality.trade_preference > 1.3:
|
if personality.trade_preference > 1.3:
|
||||||
money_multiplier *= 1.5
|
money_multiplier *= 1.5
|
||||||
starting_money = int(base_money * money_multiplier)
|
starting_money = int(base_money * money_multiplier)
|
||||||
|
|
||||||
agent = Agent(
|
agent = Agent(
|
||||||
name=name or f"Villager_{self.total_agents_spawned + 1}",
|
name=name or f"Villager_{self.total_agents_spawned + 1}",
|
||||||
profession=Profession.VILLAGER, # Will be updated based on personality
|
profession=Profession.VILLAGER, # Will be updated based on personality
|
||||||
@ -116,19 +147,192 @@ 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:
|
||||||
if agent.id == agent_id:
|
if agent.id == agent_id:
|
||||||
return agent
|
return agent
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def remove_dead_agents(self) -> list[Agent]:
|
def remove_dead_agents(self) -> list[Agent]:
|
||||||
"""Remove all dead agents from the world. Returns list of removed agents.
|
"""Remove all dead agents from the world. Returns list of removed agents.
|
||||||
Note: This is now handled by the engine's corpse system for visualization.
|
Note: This is now handled by the engine's corpse system for visualization.
|
||||||
@ -136,44 +340,149 @@ class World:
|
|||||||
dead_agents = [a for a in self.agents if not a.is_alive() and not a.is_corpse()]
|
dead_agents = [a for a in self.agents if not a.is_alive() and not a.is_corpse()]
|
||||||
# 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:
|
||||||
self.time_of_day = TimeOfDay.DAY
|
self.time_of_day = TimeOfDay.DAY
|
||||||
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
|
||||||
|
|
||||||
def get_living_agents(self) -> list[Agent]:
|
def get_living_agents(self) -> list[Agent]:
|
||||||
"""Get all living agents (excludes corpses)."""
|
"""Get all living agents (excludes corpses)."""
|
||||||
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)
|
||||||
|
|
||||||
# Count emergent professions (updated based on current skills)
|
# Count emergent professions (updated based on current skills)
|
||||||
profession_counts = {}
|
profession_counts = {}
|
||||||
for agent in living:
|
for agent in living:
|
||||||
agent._update_profession() # Update based on current state
|
agent._update_profession() # Update based on current state
|
||||||
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])
|
||||||
@ -181,7 +490,7 @@ class World:
|
|||||||
median_money = moneys[len(moneys) // 2]
|
median_money = moneys[len(moneys) // 2]
|
||||||
richest = moneys[-1] if moneys else 0
|
richest = moneys[-1] if moneys else 0
|
||||||
poorest = moneys[0] if moneys else 0
|
poorest = moneys[0] if moneys else 0
|
||||||
|
|
||||||
# Gini coefficient for inequality (0 = perfect equality, 1 = max inequality)
|
# Gini coefficient for inequality (0 = perfect equality, 1 = max inequality)
|
||||||
n = len(moneys)
|
n = len(moneys)
|
||||||
if n > 1 and total_money > 0:
|
if n > 1 and total_money > 0:
|
||||||
@ -191,7 +500,10 @@ class World:
|
|||||||
gini = 0
|
gini = 0
|
||||||
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,8 +521,23 @@ 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:
|
||||||
"""Get a full snapshot of the world state for API."""
|
"""Get a full snapshot of the world state for API."""
|
||||||
return {
|
return {
|
||||||
@ -221,10 +549,10 @@ class World:
|
|||||||
"agents": [a.to_dict() for a in self.agents],
|
"agents": [a.to_dict() for a in self.agents],
|
||||||
"statistics": self.get_statistics(),
|
"statistics": self.get_statistics(),
|
||||||
}
|
}
|
||||||
|
|
||||||
def initialize(self) -> None:
|
def initialize(self) -> None:
|
||||||
"""Initialize the world with diverse starting agents.
|
"""Initialize the world with diverse starting agents.
|
||||||
|
|
||||||
Creates a mix of agent archetypes to seed profession diversity:
|
Creates a mix of agent archetypes to seed profession diversity:
|
||||||
- Some hunters (risk-takers who hunt)
|
- Some hunters (risk-takers who hunt)
|
||||||
- Some gatherers (cautious resource collectors)
|
- Some gatherers (cautious resource collectors)
|
||||||
@ -232,7 +560,7 @@ class World:
|
|||||||
- Some generalists (balanced approach)
|
- Some generalists (balanced approach)
|
||||||
"""
|
"""
|
||||||
n = self.config.initial_agents
|
n = self.config.initial_agents
|
||||||
|
|
||||||
# Distribute archetypes for diversity
|
# Distribute archetypes for diversity
|
||||||
# ~15% hunters, ~15% gatherers, ~15% traders, ~10% woodcutters, 45% random
|
# ~15% hunters, ~15% gatherers, ~15% traders, ~10% woodcutters, 45% random
|
||||||
archetypes = (
|
archetypes = (
|
||||||
@ -241,14 +569,14 @@ class World:
|
|||||||
["trader"] * max(1, n // 7) +
|
["trader"] * max(1, n // 7) +
|
||||||
["woodcutter"] * max(1, n // 10)
|
["woodcutter"] * max(1, n // 10)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fill remaining slots with random (no archetype)
|
# Fill remaining slots with random (no archetype)
|
||||||
while len(archetypes) < n:
|
while len(archetypes) < n:
|
||||||
archetypes.append(None)
|
archetypes.append(None)
|
||||||
|
|
||||||
# Shuffle to randomize positions
|
# Shuffle to randomize positions
|
||||||
random.shuffle(archetypes)
|
random.shuffle(archetypes)
|
||||||
|
|
||||||
for archetype in archetypes:
|
for archetype in archetypes:
|
||||||
self.spawn_agent(archetype=archetype)
|
self.spawn_agent(archetype=archetype)
|
||||||
|
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -34,23 +34,23 @@ class ProfessionType(Enum):
|
|||||||
@dataclass
|
@dataclass
|
||||||
class PersonalityTraits:
|
class PersonalityTraits:
|
||||||
"""Unique personality traits that affect agent behavior.
|
"""Unique personality traits that affect agent behavior.
|
||||||
|
|
||||||
These are set at birth and don't change during the agent's life.
|
These are set at birth and don't change during the agent's life.
|
||||||
They create natural diversity in the population.
|
They create natural diversity in the population.
|
||||||
"""
|
"""
|
||||||
# How much the agent values accumulating wealth (0.1 = minimal, 0.9 = greedy)
|
# How much the agent values accumulating wealth (0.1 = minimal, 0.9 = greedy)
|
||||||
wealth_desire: float = 0.3
|
wealth_desire: float = 0.3
|
||||||
|
|
||||||
# How much the agent hoards resources vs trades them (0.1 = trades freely, 0.9 = hoards)
|
# How much the agent hoards resources vs trades them (0.1 = trades freely, 0.9 = hoards)
|
||||||
hoarding_rate: float = 0.5
|
hoarding_rate: float = 0.5
|
||||||
|
|
||||||
# Willingness to take risks (0.1 = very cautious, 0.9 = risk-taker)
|
# Willingness to take risks (0.1 = very cautious, 0.9 = risk-taker)
|
||||||
# Affects: hunting vs gathering preference, price decisions
|
# Affects: hunting vs gathering preference, price decisions
|
||||||
risk_tolerance: float = 0.5
|
risk_tolerance: float = 0.5
|
||||||
|
|
||||||
# Sensitivity to good/bad deals (0.5 = not picky, 1.5 = very price conscious)
|
# Sensitivity to good/bad deals (0.5 = not picky, 1.5 = very price conscious)
|
||||||
price_sensitivity: float = 1.0
|
price_sensitivity: float = 1.0
|
||||||
|
|
||||||
# Activity biases - how much the agent prefers each activity
|
# Activity biases - how much the agent prefers each activity
|
||||||
# Higher values = more likely to choose this activity
|
# Higher values = more likely to choose this activity
|
||||||
# These create "profession tendencies"
|
# These create "profession tendencies"
|
||||||
@ -58,12 +58,12 @@ class PersonalityTraits:
|
|||||||
gather_preference: float = 1.0 # Preference for gathering
|
gather_preference: float = 1.0 # Preference for gathering
|
||||||
woodcut_preference: float = 1.0 # Preference for wood
|
woodcut_preference: float = 1.0 # Preference for wood
|
||||||
trade_preference: float = 1.0 # Preference for trading/market
|
trade_preference: float = 1.0 # Preference for trading/market
|
||||||
|
|
||||||
# How social/market-oriented the agent is
|
# How social/market-oriented the agent is
|
||||||
# High = frequent market visits, buys more from others
|
# High = frequent market visits, buys more from others
|
||||||
# Low = self-sufficient, prefers to produce own resources
|
# Low = self-sufficient, prefers to produce own resources
|
||||||
market_affinity: float = 0.5
|
market_affinity: float = 0.5
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"wealth_desire": round(self.wealth_desire, 2),
|
"wealth_desire": round(self.wealth_desire, 2),
|
||||||
@ -81,54 +81,63 @@ class PersonalityTraits:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class Skills:
|
class Skills:
|
||||||
"""Skills that improve with practice.
|
"""Skills that improve with practice.
|
||||||
|
|
||||||
Each skill affects the outcome of related actions.
|
Each skill affects the outcome of related actions.
|
||||||
Skills increase slowly through practice (use it or lose it).
|
Skills increase slowly through practice (use it or lose it).
|
||||||
"""
|
"""
|
||||||
# Combat/hunting skill - affects hunt success rate
|
# Combat/hunting skill - affects hunt success rate
|
||||||
hunting: float = 1.0
|
hunting: float = 1.0
|
||||||
|
|
||||||
# Foraging skill - affects gather output quantity
|
# Foraging skill - affects gather output quantity
|
||||||
gathering: float = 1.0
|
gathering: float = 1.0
|
||||||
|
|
||||||
# Woodcutting skill - affects wood output
|
# Woodcutting skill - affects wood output
|
||||||
woodcutting: float = 1.0
|
woodcutting: float = 1.0
|
||||||
|
|
||||||
# Trading skill - affects prices (buy lower, sell higher)
|
# Trading skill - affects prices (buy lower, sell higher)
|
||||||
trading: float = 1.0
|
trading: float = 1.0
|
||||||
|
|
||||||
# Crafting skill - affects craft quality/success
|
# Crafting skill - affects craft quality/success
|
||||||
crafting: float = 1.0
|
crafting: float = 1.0
|
||||||
|
|
||||||
# Skill improvement rate per action
|
# Skill improvement rate per action
|
||||||
IMPROVEMENT_RATE: float = 0.02
|
IMPROVEMENT_RATE: float = 0.02
|
||||||
|
|
||||||
# Skill decay rate per turn (use it or lose it, gentle decay)
|
# Skill decay rate per turn (use it or lose it, gentle decay)
|
||||||
DECAY_RATE: float = 0.001
|
DECAY_RATE: float = 0.001
|
||||||
|
|
||||||
# Maximum skill level
|
# Maximum skill level
|
||||||
MAX_SKILL: float = 2.0
|
MAX_SKILL: float = 2.0
|
||||||
|
|
||||||
# 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)
|
||||||
setattr(self, skill_name, new_value)
|
setattr(self, skill_name, new_value)
|
||||||
|
|
||||||
def decay_all(self) -> None:
|
def decay_all(self) -> None:
|
||||||
"""Apply gentle decay to all skills (use it or lose it)."""
|
"""Apply gentle decay to all skills (use it or lose it)."""
|
||||||
for skill_name in ['hunting', 'gathering', 'woodcutting', 'trading', 'crafting']:
|
for skill_name in ['hunting', 'gathering', 'woodcutting', 'trading', 'crafting']:
|
||||||
current = getattr(self, skill_name)
|
current = getattr(self, skill_name)
|
||||||
new_value = max(self.MIN_SKILL, current - self.DECAY_RATE)
|
new_value = max(self.MIN_SKILL, current - self.DECAY_RATE)
|
||||||
setattr(self, skill_name, new_value)
|
setattr(self, skill_name, new_value)
|
||||||
|
|
||||||
def get_primary_skill(self) -> tuple[str, float]:
|
def get_primary_skill(self) -> tuple[str, float]:
|
||||||
"""Get the agent's highest skill and its name."""
|
"""Get the agent's highest skill and its name."""
|
||||||
skills = {
|
skills = {
|
||||||
@ -140,7 +149,7 @@ class Skills:
|
|||||||
}
|
}
|
||||||
best_skill = max(skills, key=skills.get)
|
best_skill = max(skills, key=skills.get)
|
||||||
return best_skill, skills[best_skill]
|
return best_skill, skills[best_skill]
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
"hunting": round(self.hunting, 3),
|
"hunting": round(self.hunting, 3),
|
||||||
@ -153,14 +162,14 @@ class Skills:
|
|||||||
|
|
||||||
def generate_random_personality(archetype: Optional[str] = None) -> PersonalityTraits:
|
def generate_random_personality(archetype: Optional[str] = None) -> PersonalityTraits:
|
||||||
"""Generate random personality traits.
|
"""Generate random personality traits.
|
||||||
|
|
||||||
If archetype is specified, traits will be biased towards that profession:
|
If archetype is specified, traits will be biased towards that profession:
|
||||||
- "hunter": High risk tolerance, high hunt preference
|
- "hunter": High risk tolerance, high hunt preference
|
||||||
- "gatherer": Low risk tolerance, high gather preference
|
- "gatherer": Low risk tolerance, high gather preference
|
||||||
- "trader": High wealth desire, high market affinity, high trade preference
|
- "trader": High wealth desire, high market affinity, high trade preference
|
||||||
- "hoarder": High hoarding rate, low market affinity
|
- "hoarder": High hoarding rate, low market affinity
|
||||||
- None: Fully random
|
- None: Fully random
|
||||||
|
|
||||||
Returns a PersonalityTraits instance with randomized values.
|
Returns a PersonalityTraits instance with randomized values.
|
||||||
"""
|
"""
|
||||||
# Start with base random values
|
# Start with base random values
|
||||||
@ -175,18 +184,18 @@ def generate_random_personality(archetype: Optional[str] = None) -> PersonalityT
|
|||||||
trade_preference=random.uniform(0.5, 1.5),
|
trade_preference=random.uniform(0.5, 1.5),
|
||||||
market_affinity=random.uniform(0.2, 0.8),
|
market_affinity=random.uniform(0.2, 0.8),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Apply archetype biases
|
# Apply archetype biases
|
||||||
if archetype == "hunter":
|
if archetype == "hunter":
|
||||||
traits.hunt_preference = random.uniform(1.3, 2.0)
|
traits.hunt_preference = random.uniform(1.3, 2.0)
|
||||||
traits.risk_tolerance = random.uniform(0.6, 0.9)
|
traits.risk_tolerance = random.uniform(0.6, 0.9)
|
||||||
traits.gather_preference = random.uniform(0.3, 0.7)
|
traits.gather_preference = random.uniform(0.3, 0.7)
|
||||||
|
|
||||||
elif archetype == "gatherer":
|
elif archetype == "gatherer":
|
||||||
traits.gather_preference = random.uniform(1.3, 2.0)
|
traits.gather_preference = random.uniform(1.3, 2.0)
|
||||||
traits.risk_tolerance = random.uniform(0.2, 0.5)
|
traits.risk_tolerance = random.uniform(0.2, 0.5)
|
||||||
traits.hunt_preference = random.uniform(0.3, 0.7)
|
traits.hunt_preference = random.uniform(0.3, 0.7)
|
||||||
|
|
||||||
elif archetype == "trader":
|
elif archetype == "trader":
|
||||||
traits.trade_preference = random.uniform(1.5, 2.5)
|
traits.trade_preference = random.uniform(1.5, 2.5)
|
||||||
traits.market_affinity = random.uniform(0.7, 0.95)
|
traits.market_affinity = random.uniform(0.7, 0.95)
|
||||||
@ -196,50 +205,63 @@ def generate_random_personality(archetype: Optional[str] = None) -> PersonalityT
|
|||||||
# Traders don't hunt/gather much
|
# Traders don't hunt/gather much
|
||||||
traits.hunt_preference = random.uniform(0.2, 0.5)
|
traits.hunt_preference = random.uniform(0.2, 0.5)
|
||||||
traits.gather_preference = random.uniform(0.2, 0.5)
|
traits.gather_preference = random.uniform(0.2, 0.5)
|
||||||
|
|
||||||
elif archetype == "hoarder":
|
elif archetype == "hoarder":
|
||||||
traits.hoarding_rate = random.uniform(0.7, 0.95)
|
traits.hoarding_rate = random.uniform(0.7, 0.95)
|
||||||
traits.market_affinity = random.uniform(0.1, 0.4)
|
traits.market_affinity = random.uniform(0.1, 0.4)
|
||||||
traits.trade_preference = random.uniform(0.3, 0.7)
|
traits.trade_preference = random.uniform(0.3, 0.7)
|
||||||
|
|
||||||
elif archetype == "woodcutter":
|
elif archetype == "woodcutter":
|
||||||
traits.woodcut_preference = random.uniform(1.3, 2.0)
|
traits.woodcut_preference = random.uniform(1.3, 2.0)
|
||||||
traits.gather_preference = random.uniform(0.5, 0.8)
|
traits.gather_preference = random.uniform(0.5, 0.8)
|
||||||
|
|
||||||
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
|
||||||
skills.hunting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.hunting))
|
skills.hunting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.hunting))
|
||||||
skills.gathering = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.gathering))
|
skills.gathering = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.gathering))
|
||||||
skills.woodcutting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.woodcutting))
|
skills.woodcutting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.woodcutting))
|
||||||
skills.trading = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.trading))
|
skills.trading = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.trading))
|
||||||
skills.crafting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.crafting))
|
skills.crafting = max(skills.MIN_SKILL, min(skills.MAX_SKILL, skills.crafting))
|
||||||
|
|
||||||
return skills
|
return skills
|
||||||
|
|
||||||
|
|
||||||
def determine_profession(personality: PersonalityTraits, skills: Skills) -> ProfessionType:
|
def determine_profession(personality: PersonalityTraits, skills: Skills) -> ProfessionType:
|
||||||
"""Determine an agent's emergent profession based on traits and skills.
|
"""Determine an agent's emergent profession based on traits and skills.
|
||||||
|
|
||||||
This is for display/statistics - it doesn't affect behavior directly.
|
This is for display/statistics - it doesn't affect behavior directly.
|
||||||
The behavior is determined by the traits and skills themselves.
|
The behavior is determined by the traits and skills themselves.
|
||||||
"""
|
"""
|
||||||
@ -250,27 +272,27 @@ def determine_profession(personality: PersonalityTraits, skills: Skills) -> Prof
|
|||||||
ProfessionType.WOODCUTTER: personality.woodcut_preference * skills.woodcutting,
|
ProfessionType.WOODCUTTER: personality.woodcut_preference * skills.woodcutting,
|
||||||
ProfessionType.TRADER: personality.trade_preference * skills.trading * personality.market_affinity * 1.5,
|
ProfessionType.TRADER: personality.trade_preference * skills.trading * personality.market_affinity * 1.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
# Find the best match
|
# Find the best match
|
||||||
best_profession = max(scores, key=scores.get)
|
best_profession = max(scores, key=scores.get)
|
||||||
best_score = scores[best_profession]
|
best_score = scores[best_profession]
|
||||||
|
|
||||||
# If no clear winner (all scores similar), they're a generalist
|
# If no clear winner (all scores similar), they're a generalist
|
||||||
second_best = sorted(scores.values(), reverse=True)[1]
|
second_best = sorted(scores.values(), reverse=True)[1]
|
||||||
if best_score < second_best * 1.2:
|
if best_score < second_best * 1.2:
|
||||||
return ProfessionType.GENERALIST
|
return ProfessionType.GENERALIST
|
||||||
|
|
||||||
return best_profession
|
return best_profession
|
||||||
|
|
||||||
|
|
||||||
def get_action_skill_modifier(skill_value: float) -> float:
|
def get_action_skill_modifier(skill_value: float) -> float:
|
||||||
"""Convert skill value to action modifier.
|
"""Convert skill value to action modifier.
|
||||||
|
|
||||||
Skill 0.5 = 0.75x effectiveness
|
Skill 0.5 = 0.75x effectiveness
|
||||||
Skill 1.0 = 1.0x effectiveness
|
Skill 1.0 = 1.0x effectiveness
|
||||||
Skill 1.5 = 1.25x effectiveness
|
Skill 1.5 = 1.25x effectiveness
|
||||||
Skill 2.0 = 1.5x effectiveness
|
Skill 2.0 = 1.5x effectiveness
|
||||||
|
|
||||||
This creates meaningful but not overpowering differences.
|
This creates meaningful but not overpowering differences.
|
||||||
"""
|
"""
|
||||||
# Linear scaling: (skill - 1.0) * 0.5 + 1.0
|
# Linear scaling: (skill - 1.0) * 0.5 + 1.0
|
||||||
@ -280,16 +302,16 @@ def get_action_skill_modifier(skill_value: float) -> float:
|
|||||||
|
|
||||||
def get_trade_price_modifier(skill_value: float, is_buying: bool) -> float:
|
def get_trade_price_modifier(skill_value: float, is_buying: bool) -> float:
|
||||||
"""Get price modifier for trading based on skill.
|
"""Get price modifier for trading based on skill.
|
||||||
|
|
||||||
Higher trading skill = better deals:
|
Higher trading skill = better deals:
|
||||||
- When buying: lower prices (modifier < 1)
|
- When buying: lower prices (modifier < 1)
|
||||||
- When selling: higher prices (modifier > 1)
|
- When selling: higher prices (modifier > 1)
|
||||||
|
|
||||||
Skill 1.0 = no modifier
|
Skill 1.0 = no modifier
|
||||||
Skill 2.0 = 15% better deals
|
Skill 2.0 = 15% better deals
|
||||||
"""
|
"""
|
||||||
modifier = (skill_value - 1.0) * 0.15
|
modifier = (skill_value - 1.0) * 0.15
|
||||||
|
|
||||||
if is_buying:
|
if is_buying:
|
||||||
return max(0.85, 1.0 - modifier) # Lower is better for buying
|
return max(0.85, 1.0 - modifier) # Lower is better for buying
|
||||||
else:
|
else:
|
||||||
|
|||||||
93
config.json
93
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": {
|
||||||
@ -77,4 +128,4 @@
|
|||||||
"min_price_discount": 0.4
|
"min_price_discount": 0.4
|
||||||
},
|
},
|
||||||
"auto_step_interval": 0.15
|
"auto_step_interval": 0.15
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
@ -76,4 +128,4 @@
|
|||||||
"min_price_discount": 0.4
|
"min_price_discount": 0.4
|
||||||
},
|
},
|
||||||
"auto_step_interval": 0.15
|
"auto_step_interval": 0.15
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user