feat(narrative): add WorldStateStore for rules, locations, event log
This commit is contained in:
parent
3cf1e28ab9
commit
1a73b7f672
|
|
@ -0,0 +1,59 @@
|
|||
"""World state CRUD: rules, locations, event log.
|
||||
|
||||
Stored in narrative/world_state.json. Missing file is treated as an empty
|
||||
world so existing simulations (from pre-God-Mode versions) continue to work
|
||||
without migration.
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
|
||||
|
||||
class WorldStateStore:
|
||||
"""Manages narrative/world_state.json for a single simulation."""
|
||||
|
||||
def __init__(self, sim_dir: str):
|
||||
self.sim_dir = sim_dir
|
||||
self.narrative_dir = os.path.join(sim_dir, "narrative")
|
||||
self.path = os.path.join(self.narrative_dir, "world_state.json")
|
||||
|
||||
def _ensure_dir(self) -> None:
|
||||
os.makedirs(self.narrative_dir, exist_ok=True)
|
||||
|
||||
def load(self) -> dict:
|
||||
if not os.path.exists(self.path):
|
||||
return {"rules": [], "locations": {}, "event_log": []}
|
||||
with open(self.path, "r", encoding="utf-8") as f:
|
||||
world = json.load(f)
|
||||
# Fill in missing keys for forward compatibility
|
||||
world.setdefault("rules", [])
|
||||
world.setdefault("locations", {})
|
||||
world.setdefault("event_log", [])
|
||||
return world
|
||||
|
||||
def save(self, world: dict) -> None:
|
||||
self._ensure_dir()
|
||||
with open(self.path, "w", encoding="utf-8") as f:
|
||||
json.dump(world, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def set_rules(self, rules: list[str]) -> None:
|
||||
world = self.load()
|
||||
world["rules"] = list(rules)
|
||||
self.save(world)
|
||||
|
||||
def upsert_location(self, location: dict) -> dict:
|
||||
"""Insert or update a location by id. Returns the stored entry."""
|
||||
if "id" not in location:
|
||||
raise ValueError("location requires 'id'")
|
||||
world = self.load()
|
||||
world["locations"][location["id"]] = location
|
||||
self.save(world)
|
||||
return location
|
||||
|
||||
def append_event(self, event: dict) -> dict:
|
||||
"""Append an event to event_log, assigning evt_N id automatically."""
|
||||
world = self.load()
|
||||
event = dict(event)
|
||||
event["id"] = f"evt_{len(world['event_log']) + 1}"
|
||||
world["event_log"].append(event)
|
||||
self.save(world)
|
||||
return event
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from app.services.narrative.world_state import WorldStateStore
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_sim_dir():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
sim_dir = os.path.join(d, "sim_test")
|
||||
os.makedirs(sim_dir)
|
||||
yield sim_dir
|
||||
|
||||
|
||||
def test_load_returns_empty_world_when_missing(temp_sim_dir):
|
||||
store = WorldStateStore(temp_sim_dir)
|
||||
world = store.load()
|
||||
assert world == {"rules": [], "locations": {}, "event_log": []}
|
||||
|
||||
|
||||
def test_set_rules_replaces_previous(temp_sim_dir):
|
||||
store = WorldStateStore(temp_sim_dir)
|
||||
store.set_rules(["rule 1", "rule 2"])
|
||||
assert store.load()["rules"] == ["rule 1", "rule 2"]
|
||||
store.set_rules(["only rule"])
|
||||
assert store.load()["rules"] == ["only rule"]
|
||||
|
||||
|
||||
def test_upsert_location_adds_and_updates(temp_sim_dir):
|
||||
store = WorldStateStore(temp_sim_dir)
|
||||
store.upsert_location({"id": "tower", "name": "The Tower", "description": "tall"})
|
||||
assert store.load()["locations"]["tower"]["name"] == "The Tower"
|
||||
|
||||
store.upsert_location({"id": "tower", "name": "The Iron Tower", "description": "dark"})
|
||||
assert store.load()["locations"]["tower"]["name"] == "The Iron Tower"
|
||||
|
||||
|
||||
def test_append_event_auto_ids_sequentially(temp_sim_dir):
|
||||
store = WorldStateStore(temp_sim_dir)
|
||||
e1 = store.append_event({"type": "custom", "description": "one", "round": 1})
|
||||
e2 = store.append_event({"type": "custom", "description": "two", "round": 2})
|
||||
|
||||
assert e1["id"] == "evt_1"
|
||||
assert e2["id"] == "evt_2"
|
||||
assert store.load()["event_log"][-1]["description"] == "two"
|
||||
Loading…
Reference in New Issue