villsim/backend/core/world.py

147 lines
4.7 KiB
Python

"""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()