"""Deterministic autopilot for social (non-survival) worlds. The autopilot fills gaps: it produces one safe, goal-consistent primitive when an NPC has no usable directive (LLM error, unknown action, omitted NPC). It is a fallback, never a filter — it does not constrain what a planner may choose. """ from __future__ import annotations from world_simulator.domain import CitizenGoal, Npc, WorldState from world_simulator.simulation.connectors.base import NpcDirective from world_simulator.simulation.mechanics import ( ATTACK_RADIUS, distance_between, is_alive, walk_away_target, ) from world_simulator.simulation.perception import CitizenPerception, nearby_threat_npc def autopilot_directive( world: WorldState, npc: Npc, directive: NpcDirective, perception: CitizenPerception, goal: CitizenGoal | None, ) -> NpcDirective: target = target_from_directive_or_goal(world, npc, directive, goal, perception) if ( goal is not None and goal.goal_type in ("pursue_and_attack_target", "retaliate") and target is not None ): return strike_or_approach_target( npc, directive, target, memory="Autopilot pursued the hostile target.", ) if goal is not None and goal.goal_type in ("survive", "call_for_help", "help_or_defend"): if goal.goal_type == "call_for_help": return NpcDirective( npc_id=npc.id, action="speak", message="Help! I am in danger!", memory="Autopilot called for help.", communication_intent="help_request", intent="call_for_help", ) if target is not None: return flee_directive(world, npc, target, directive) return idle_directive( directive, memory="Autopilot held a defensive stance.", intent="defend", ) if ( goal is not None and goal.goal_type in ("respond_to_event", "investigate") and target is not None ): return move_toward_target_directive(npc, directive, target, intent="investigate") return idle_directive(directive, memory=None, intent="observe") def target_from_directive_or_goal( world: WorldState, npc: Npc, directive: NpcDirective, goal: CitizenGoal | None, perception: CitizenPerception, ) -> Npc | None: if directive.target_npc_id: target = _npc_by_id(world, directive.target_npc_id) if target is not None and target.id != npc.id and is_alive(target): return target if goal is not None and goal.target_npc_id: target = _npc_by_id(world, goal.target_npc_id) if target is not None and target.id != npc.id and is_alive(target): return target threat = nearby_threat_npc(world, perception) if threat is not None and threat.id != npc.id and is_alive(threat): return threat return None def strike_or_approach_target( npc: Npc, directive: NpcDirective, target: Npc, *, memory: str, ) -> NpcDirective: if distance_between(npc, target) <= ATTACK_RADIUS: return NpcDirective( npc_id=npc.id, action="attack", target_npc_id=target.id, message=directive.message, memory=memory, intent="hostile_attack", confidence=directive.confidence, ) return move_toward_target_directive( npc, directive, target, intent="approach_target_for_attack", memory=memory, ) def move_toward_target_directive( npc: Npc, directive: NpcDirective, target: Npc, *, intent: str, memory: str | None = None, ) -> NpcDirective: return NpcDirective( npc_id=npc.id, action="move", target=target.position, target_npc_id=target.id, message=directive.message, memory=memory or f"You moved toward {target.name}.", intent=intent, confidence=directive.confidence, ) def flee_directive( world: WorldState, npc: Npc, threat: Npc, directive: NpcDirective, ) -> NpcDirective: half_width = world.terrain.width / 2 half_depth = world.terrain.depth / 2 return NpcDirective( npc_id=npc.id, action="move", target=walk_away_target(npc, threat, half_width=half_width, half_depth=half_depth), target_npc_id=threat.id, message=directive.message, memory=f"You backed away from {threat.name}.", intent="flee_from_threat", confidence=directive.confidence, ) def idle_directive( directive: NpcDirective, *, memory: str | None, intent: str, ) -> NpcDirective: return NpcDirective( npc_id=directive.npc_id, action="idle", target=None, target_npc_id=directive.target_npc_id, message=directive.message, memory=memory, intent=intent, confidence=directive.confidence, ) def _npc_by_id(world: WorldState, npc_id: str) -> Npc | None: return next((candidate for candidate in world.living_npcs() if candidate.id == npc_id), None)