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