| """Entropy management β keeps the simulation diverse and interesting.""" |
|
|
| from __future__ import annotations |
|
|
| import random |
| import logging |
| from typing import TYPE_CHECKING |
|
|
| if TYPE_CHECKING: |
| from soci.agents.agent import Agent |
| from soci.world.clock import SimClock |
| from soci.world.events import EventSystem |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class EntropyManager: |
| """Manages simulation entropy to prevent bland, repetitive behavior.""" |
|
|
| def __init__(self) -> None: |
| |
| self.event_injection_interval: int = 10 |
| |
| self._ticks_since_event: int = 0 |
| |
| self._action_history: dict[str, list[str]] = {} |
| self._history_window: int = 20 |
|
|
| def tick( |
| self, |
| agents: list[Agent], |
| event_system: EventSystem, |
| clock: SimClock, |
| city_location_ids: list[str], |
| ) -> list[str]: |
| """Process one tick of entropy management. Returns list of notable events/messages.""" |
| messages: list[str] = [] |
| self._ticks_since_event += 1 |
|
|
| |
| for agent in agents: |
| if agent.current_action: |
| history = self._action_history.setdefault(agent.id, []) |
| history.append(agent.current_action.type) |
| if len(history) > self._history_window: |
| self._action_history[agent.id] = history[-self._history_window:] |
|
|
| |
| for agent in agents: |
| if self._is_stuck_in_loop(agent.id): |
| messages.append( |
| f"[ENTROPY] {agent.name} seems stuck in a behavioral loop β " |
| f"injecting stimulus." |
| ) |
| self._inject_personal_stimulus(agent, clock) |
|
|
| |
| if self._ticks_since_event >= self.event_injection_interval: |
| new_events = event_system.tick(city_location_ids) |
| self._ticks_since_event = 0 |
| for evt in new_events: |
| messages.append(f"[EVENT] {evt.name}: {evt.description}") |
| else: |
| |
| event_system.tick(city_location_ids) |
|
|
| |
| if clock.hour == 12 and clock.minute == 0: |
| messages.append("[RHYTHM] Noon β the city bustles with lunch crowds.") |
| elif clock.hour == 18 and clock.minute == 0: |
| messages.append("[RHYTHM] Evening β people head home or to the bar.") |
| elif clock.hour == 22 and clock.minute == 0: |
| messages.append("[RHYTHM] Late night β the city quiets down.") |
|
|
| return messages |
|
|
| def _is_stuck_in_loop(self, agent_id: str) -> bool: |
| """Detect if an agent is repeating the same actions.""" |
| history = self._action_history.get(agent_id, []) |
| if len(history) < 10: |
| return False |
| |
| recent = history[-10:] |
| unique = set(recent) |
| return len(unique) <= 2 and "sleep" not in unique |
|
|
| def _inject_personal_stimulus(self, agent: Agent, clock: SimClock) -> None: |
| """Inject a personal event to break an agent out of a loop.""" |
| stimuli = [ |
| f"{agent.name} suddenly remembers something important they forgot to do.", |
| f"{agent.name} gets an unexpected phone call from an old friend.", |
| f"{agent.name} notices something unusual in their surroundings.", |
| f"{agent.name} overhears an interesting conversation nearby.", |
| f"{agent.name} finds a forgotten note in their pocket.", |
| f"{agent.name} suddenly craves something completely different.", |
| ] |
| stimulus = random.choice(stimuli) |
| agent.add_observation( |
| tick=clock.total_ticks, |
| day=clock.day, |
| time_str=clock.time_str, |
| content=stimulus, |
| importance=7, |
| ) |
|
|
| def get_conflict_catalysts(self, agents: list[Agent]) -> list[tuple[str, str, str]]: |
| """Identify potential conflicts between agents based on their personas. |
| Returns list of (agent1_id, agent2_id, tension_description) tuples. |
| """ |
| catalysts = [] |
|
|
| |
| for i, a in enumerate(agents): |
| for b in agents[i + 1:]: |
| tension = self._find_tension(a, b) |
| if tension: |
| catalysts.append((a.id, b.id, tension)) |
|
|
| return catalysts |
|
|
| def _find_tension(self, a: Agent, b: Agent) -> str | None: |
| """Find natural tension between two agents.""" |
| |
| extraversion_gap = abs(a.persona.extraversion - b.persona.extraversion) |
| agreeableness_gap = abs(a.persona.agreeableness - b.persona.agreeableness) |
|
|
| if extraversion_gap >= 6 and agreeableness_gap >= 4: |
| return "personality clash β one is outgoing and blunt, the other is reserved and sensitive" |
|
|
| |
| a_values = set(a.persona.values) |
| b_values = set(b.persona.values) |
| if a_values and b_values and not a_values.intersection(b_values): |
| return f"different values β {a.name} values {', '.join(a.persona.values)}, while {b.name} values {', '.join(b.persona.values)}" |
|
|
| return None |
|
|
| def to_dict(self) -> dict: |
| return { |
| "event_injection_interval": self.event_injection_interval, |
| "ticks_since_event": self._ticks_since_event, |
| "action_history": dict(self._action_history), |
| } |
|
|
| @classmethod |
| def from_dict(cls, data: dict) -> EntropyManager: |
| mgr = cls() |
| mgr.event_injection_interval = data.get("event_injection_interval", 10) |
| mgr._ticks_since_event = data.get("ticks_since_event", 0) |
| mgr._action_history = data.get("action_history", {}) |
| return mgr |
|
|