"""World container for the Village Simulation.""" import random from dataclasses import dataclass, field from enum import Enum from typing import Optional from backend.domain.agent import Agent, Position, Profession class TimeOfDay(Enum): """Current time of day in the simulation.""" DAY = "day" NIGHT = "night" @dataclass class WorldConfig: """Configuration for the world.""" width: int = 20 height: int = 20 initial_agents: int = 8 day_steps: int = 10 night_steps: int = 1 @dataclass class World: """Container for all entities in the simulation.""" config: WorldConfig = field(default_factory=WorldConfig) 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 # Statistics total_agents_spawned: int = 0 total_agents_died: int = 0 def spawn_agent( self, name: Optional[str] = None, profession: Optional[Profession] = None, position: Optional[Position] = None, ) -> Agent: """Spawn a new agent in the world.""" # All agents are now generic villagers - profession is not used for decisions if profession is None: profession = Profession.VILLAGER if position is None: position = Position( x=random.randint(0, self.config.width - 1), y=random.randint(0, self.config.height - 1), ) agent = Agent( name=name or f"Villager_{self.total_agents_spawned + 1}", profession=profession, position=position, ) 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 remove_dead_agents(self) -> list[Agent]: """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. """ 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 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 # Determine time of day if self.step_in_day <= self.config.day_steps: self.time_of_day = TimeOfDay.DAY else: self.time_of_day = TimeOfDay.NIGHT 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.""" living = self.get_living_agents() total_money = sum(a.money for a in living) profession_counts = {} for agent in living: prof = agent.profession.value profession_counts[prof] = profession_counts.get(prof, 0) + 1 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, } 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(), } def initialize(self) -> None: """Initialize the world with starting agents.""" for _ in range(self.config.initial_agents): self.spawn_agent()