"""World container for the Village Simulation. The world spawns diverse agents with varied personality traits, skills, and starting conditions to create emergent professions and class inequality. NEW: World now supports religion and faction systems for realistic social dynamics including religious diversity and geopolitical factions. """ import random from dataclasses import dataclass, field from enum import Enum from typing import Optional from backend.domain.agent import Agent, Position, Profession from backend.domain.personality import ( PersonalityTraits, Skills, generate_random_personality, generate_random_skills ) from backend.domain.religion import ( ReligiousBeliefs, ReligionType, generate_random_religion ) from backend.domain.diplomacy import ( AgentDiplomacy, FactionType, FactionRelations, generate_random_faction, reset_faction_relations, get_faction_relations ) class TimeOfDay(Enum): """Current time of day in the simulation.""" DAY = "day" NIGHT = "night" def _get_world_config_from_file(): """Load world configuration from config.json.""" from backend.config import get_config return get_config().world @dataclass class WorldConfig: """Configuration for the world.""" width: int = 25 height: int = 25 initial_agents: int = 25 day_steps: int = 10 night_steps: int = 1 oil_fields_count: int = 3 # NEW temple_count: int = 2 # NEW def create_world_config() -> WorldConfig: """Factory function to create WorldConfig from config.json.""" cfg = _get_world_config_from_file() return WorldConfig( width=cfg.width, height=cfg.height, initial_agents=cfg.initial_agents, day_steps=cfg.day_steps, night_steps=cfg.night_steps, oil_fields_count=getattr(cfg, 'oil_fields_count', 3), temple_count=getattr(cfg, 'temple_count', 2), ) @dataclass class WorldLocation: """A special location in the world.""" name: str position: Position location_type: str # "oil_field", "temple", "market", etc. faction: Optional[FactionType] = None religion: Optional[ReligionType] = None @dataclass class World: """Container for all entities in the simulation.""" config: WorldConfig = field(default_factory=create_world_config) agents: list[Agent] = field(default_factory=list) current_turn: int = 0 current_day: int = 1 step_in_day: int = 0 time_of_day: TimeOfDay = TimeOfDay.DAY # Special locations oil_fields: list[WorldLocation] = field(default_factory=list) temples: list[WorldLocation] = field(default_factory=list) # Faction relations faction_relations: FactionRelations = field(default_factory=FactionRelations) # Statistics total_agents_spawned: int = 0 total_agents_died: int = 0 total_wars: int = 0 total_peace_treaties: int = 0 total_conversions: int = 0 def _generate_locations(self) -> None: """Generate special locations in the world.""" # Generate oil fields (right side of map - "resource-rich" area) self.oil_fields = [] for i in range(self.config.oil_fields_count): x = self.config.width * random.uniform(0.75, 0.95) y = self.config.height * (i + 1) / (self.config.oil_fields_count + 1) self.oil_fields.append(WorldLocation( name=f"Oil Field {i + 1}", position=Position(x, y), location_type="oil_field", faction=random.choice([FactionType.MOUNTAINEER, FactionType.NORTHLANDS]), )) # Generate temples (scattered across map) self.temples = [] religions = [r for r in ReligionType if r != ReligionType.ATHEIST] for i in range(self.config.temple_count): x = self.config.width * random.uniform(0.3, 0.7) y = self.config.height * (i + 1) / (self.config.temple_count + 1) self.temples.append(WorldLocation( name=f"Temple of {religions[i % len(religions)].value.title()}", position=Position(x, y), location_type="temple", religion=religions[i % len(religions)], )) def spawn_agent( self, name: Optional[str] = None, profession: Optional[Profession] = None, position: Optional[Position] = None, archetype: Optional[str] = None, starting_money: Optional[int] = None, religion: Optional[ReligiousBeliefs] = None, faction: Optional[AgentDiplomacy] = None, ) -> Agent: """Spawn a new agent in the world with unique personality. Args: name: Agent name (auto-generated if None) profession: Deprecated, now derived from personality position: Starting position (random if None) archetype: Personality archetype ("hunter", "gatherer", "trader", etc.) starting_money: Starting money (random with inequality if None) religion: Religious beliefs (random if None) faction: Faction membership (random if None) """ if position is None: position = Position( x=random.randint(0, self.config.width - 1), y=random.randint(0, self.config.height - 1), ) # Generate unique personality and skills personality = generate_random_personality(archetype) skills = generate_random_skills(personality) # Generate religion if not provided if religion is None: religion = generate_random_religion(archetype) # Generate faction if not provided if faction is None: faction = generate_random_faction(archetype) # Variable starting money for class inequality if starting_money is None: from backend.config import get_config base_money = get_config().world.starting_money money_multiplier = random.uniform(0.3, 2.0) # Traders start with more money (their capital) if personality.trade_preference > 1.3: money_multiplier *= 1.5 # Oil-controlling factions have wealth bonus if faction.faction == FactionType.MOUNTAINEER: money_multiplier *= 1.3 starting_money = int(base_money * money_multiplier) agent = Agent( name=name or f"Villager_{self.total_agents_spawned + 1}", profession=Profession.VILLAGER, position=position, personality=personality, skills=skills, religion=religion, diplomacy=faction, money=starting_money, ) self.agents.append(agent) self.total_agents_spawned += 1 return agent def get_agent(self, agent_id: str) -> Optional[Agent]: """Get an agent by ID.""" for agent in self.agents: if agent.id == agent_id: return agent return None def get_agents_by_faction(self, faction: FactionType) -> list[Agent]: """Get all living agents in a faction.""" return [ a for a in self.agents if a.is_alive() and not a.is_corpse() and a.diplomacy.faction == faction ] def get_agents_by_religion(self, religion: ReligionType) -> list[Agent]: """Get all living agents of a religion.""" return [ a for a in self.agents if a.is_alive() and not a.is_corpse() and a.religion.religion == religion ] def get_nearby_agents(self, agent: Agent, radius: float = 3.0) -> list[Agent]: """Get living agents near a given agent.""" nearby = [] for other in self.agents: if other.id == agent.id: continue if not other.is_alive() or other.is_corpse(): continue if agent.position.distance_to(other.position) <= radius: nearby.append(other) return nearby def remove_dead_agents(self) -> list[Agent]: """Remove all dead agents from the world.""" dead_agents = [a for a in self.agents if not a.is_alive() and not a.is_corpse()] return dead_agents def advance_time(self) -> None: """Advance the simulation time by one step.""" self.current_turn += 1 self.step_in_day += 1 total_steps = self.config.day_steps + self.config.night_steps if self.step_in_day > total_steps: self.step_in_day = 1 self.current_day += 1 if self.step_in_day <= self.config.day_steps: self.time_of_day = TimeOfDay.DAY else: self.time_of_day = TimeOfDay.NIGHT # Update faction relations each turn self.faction_relations.update_turn(self.current_turn) def is_night(self) -> bool: """Check if it's currently night.""" return self.time_of_day == TimeOfDay.NIGHT def get_living_agents(self) -> list[Agent]: """Get all living agents (excludes corpses).""" return [a for a in self.agents if a.is_alive() and not a.is_corpse()] def get_statistics(self) -> dict: """Get current world statistics including wealth distribution.""" living = self.get_living_agents() total_money = sum(a.money for a in living) # Count emergent professions profession_counts = {} for agent in living: agent._update_profession() prof = agent.profession.value profession_counts[prof] = profession_counts.get(prof, 0) + 1 # Count religions religion_counts = {} for agent in living: rel = agent.religion.religion.value religion_counts[rel] = religion_counts.get(rel, 0) + 1 # Count factions faction_counts = {} for agent in living: fac = agent.diplomacy.faction.value faction_counts[fac] = faction_counts.get(fac, 0) + 1 # Calculate wealth inequality metrics if living: moneys = sorted([a.money for a in living]) avg_money = total_money / len(living) median_money = moneys[len(moneys) // 2] richest = moneys[-1] if moneys else 0 poorest = moneys[0] if moneys else 0 # Gini coefficient n = len(moneys) if n > 1 and total_money > 0: sum_of_diffs = sum(abs(m1 - m2) for m1 in moneys for m2 in moneys) gini = sum_of_diffs / (2 * n * total_money) else: gini = 0 # Average faith avg_faith = sum(a.stats.faith for a in living) / len(living) else: avg_money = median_money = richest = poorest = gini = avg_faith = 0 # War status active_wars = len(self.faction_relations.active_wars) return { "current_turn": self.current_turn, "current_day": self.current_day, "step_in_day": self.step_in_day, "time_of_day": self.time_of_day.value, "living_agents": len(living), "total_agents_spawned": self.total_agents_spawned, "total_agents_died": self.total_agents_died, "total_money_in_circulation": total_money, "professions": profession_counts, # Wealth metrics "avg_money": round(avg_money, 1), "median_money": median_money, "richest_agent": richest, "poorest_agent": poorest, "gini_coefficient": round(gini, 3), # NEW: Religion and diplomacy stats "religions": religion_counts, "factions": faction_counts, "active_wars": active_wars, "avg_faith": round(avg_faith, 1), "total_wars": self.total_wars, "total_peace_treaties": self.total_peace_treaties, "total_conversions": self.total_conversions, } def get_state_snapshot(self) -> dict: """Get a full snapshot of the world state for API.""" return { "turn": self.current_turn, "day": self.current_day, "step_in_day": self.step_in_day, "time_of_day": self.time_of_day.value, "world_size": {"width": self.config.width, "height": self.config.height}, "agents": [a.to_dict() for a in self.agents], "statistics": self.get_statistics(), # NEW: Special locations "oil_fields": [ {"name": l.name, "position": l.position.to_dict(), "faction": l.faction.value if l.faction else None} for l in self.oil_fields ], "temples": [ {"name": l.name, "position": l.position.to_dict(), "religion": l.religion.value if l.religion else None} for l in self.temples ], # NEW: Faction relations summary "faction_relations": self.faction_relations.to_dict(), } def initialize(self) -> None: """Initialize the world with diverse starting agents. Creates a mix of agent archetypes to seed profession diversity. Now also seeds religious and faction diversity. """ # Reset faction relations self.faction_relations = reset_faction_relations() # Generate special locations self._generate_locations() n = self.config.initial_agents # Distribute archetypes for diversity archetypes = ( ["hunter"] * max(1, n // 7) + ["gatherer"] * max(1, n // 7) + ["trader"] * max(1, n // 7) + ["woodcutter"] * max(1, n // 10) ) while len(archetypes) < n: archetypes.append(None) random.shuffle(archetypes) for archetype in archetypes: self.spawn_agent(archetype=archetype) # Set up some initial faction tensions for drama self._create_initial_tensions() def _create_initial_tensions(self) -> None: """Create some initial diplomatic tensions for realistic starting conditions.""" # Some factions have historical rivalries rivalries = [ (FactionType.NORTHLANDS, FactionType.RIVERFOLK, -15), (FactionType.FORESTKIN, FactionType.MOUNTAINEER, -10), ] for faction1, faction2, modifier in rivalries: self.faction_relations.modify_relation(faction1, faction2, modifier) # Some factions have good relations friendships = [ (FactionType.RIVERFOLK, FactionType.PLAINSMEN, 10), (FactionType.PLAINSMEN, FactionType.FORESTKIN, 15), ] for faction1, faction2, modifier in friendships: self.faction_relations.modify_relation(faction1, faction2, modifier)