Source code for axiom.missions

"""Per-character belief missions (Phase 4, B-3).

A *belief mission* is a memory style for the consolidator: what a character
tends to remember and dwell on (a rancorous NPC remembers betrayals, a merchant
remembers transactions, a loyal guard remembers favours). It biases which
beliefs :mod:`axiom.consolidate` forms about each subject, so NPCs "remember
differently" according to their nature.

Stored in ``Universe_Meta`` (no schema change; it round-trips losslessly through
the Universe-as-Code ``[extra]`` mechanism, is copied into saves, and is
packaged):

- ``belief_mission``  — the universe-wide default mission (one string);
- ``belief_missions`` — JSON ``{entity_name: mission}`` per-character overrides.

Keyed by entity *name* because a belief's ``subject`` is a name and authoring
"Name: mission" is natural. Reading degrades gracefully (missing/blank/malformed
→ sensible empties) so the consolidator always has something usable.
"""

from __future__ import annotations

import json

from axiom.schema import get_connection


def _read_meta(db_path: str, key: str) -> str:
    try:
        with get_connection(db_path) as conn:
            row = conn.execute(
                "SELECT value FROM Universe_Meta WHERE key = ?;", (key,)
            ).fetchone()
        return str(row[0]) if row and row[0] is not None else ""
    except Exception:
        return ""


[docs] def get_universe_mission(db_path: str) -> str: """The universe-wide default belief mission ("" when unset).""" return _read_meta(db_path, "belief_mission").strip()
[docs] def get_belief_missions_from_value(raw: str) -> dict[str, str]: """Parse a ``belief_missions`` JSON value into ``{entity_name: mission}``. Tolerates blank/malformed JSON or non-string values — anything unusable is dropped rather than raised. Used when the meta value is already in hand (e.g. the Studio's loaded meta dict). """ if not raw or not raw.strip(): return {} try: data = json.loads(raw) except (ValueError, TypeError): return {} if not isinstance(data, dict): return {} out: dict[str, str] = {} for name, mission in data.items(): name_s = str(name).strip() mission_s = str(mission).strip() if name_s and mission_s: out[name_s] = mission_s return out
[docs] def get_belief_missions(db_path: str) -> dict[str, str]: """Per-character missions as ``{entity_name: mission}`` read from the DB.""" return get_belief_missions_from_value(_read_meta(db_path, "belief_missions"))
[docs] def parse_missions_text(text: str) -> dict[str, str]: """Parse a 'Name: mission' per-line block into ``{name: mission}``. The GUI Metadata field stores missions this way (one entity per line). Lines without a colon, or with an empty name/mission, are skipped. The first colon splits — a mission may itself contain colons. """ out: dict[str, str] = {} for line in (text or "").splitlines(): line = line.strip() if not line or ":" not in line: continue name, _, mission = line.partition(":") name, mission = name.strip(), mission.strip() if name and mission: out[name] = mission return out
[docs] def missions_to_text(missions: dict[str, str]) -> str: """Inverse of :func:`parse_missions_text` for displaying in the GUI field.""" return "\n".join(f"{name}: {mission}" for name, mission in missions.items())