feat(narrative): add world + god mode API endpoints
Six new endpoints under /api/narrative/*: - GET /world/<sim_id> — full world state - POST /world/<sim_id>/rules — replace rules list - POST /world/<sim_id>/locations — upsert location - POST /godmode/<sim_id>/inject-event — inject world event - POST /godmode/<sim_id>/modify-emotion — overwrite emotions - POST /godmode/<sim_id>/kill — kill character Validation: - 400 on missing required fields (description, character_id, rules) - 400 on invalid round (must be non-negative int or null) - 404 on character not found (from ValueError in handlers) Smoke-tested via Flask test client — all 11 response codes correct. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aabe9b42a2
commit
2eb98a00bd
|
|
@ -12,6 +12,12 @@ from ..config import Config
|
|||
from ..services.narrative.story_store import StoryStore
|
||||
from ..services.narrative.narrative_translator import translate_round
|
||||
from ..services.narrative.character_engine import CharacterEngine
|
||||
from ..services.narrative.world_state import WorldStateStore
|
||||
from ..services.narrative.god_mode import (
|
||||
inject_event as gm_inject_event,
|
||||
modify_emotion as gm_modify_emotion,
|
||||
kill_character as gm_kill_character,
|
||||
)
|
||||
|
||||
|
||||
def _sim_dir(sim_id: str) -> str:
|
||||
|
|
@ -80,3 +86,95 @@ def initialize_characters(sim_id):
|
|||
engine = CharacterEngine(store)
|
||||
characters = engine.initialize_from_profiles(profiles)
|
||||
return jsonify({"count": len(characters), "characters": characters})
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# World State endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@narrative_bp.route('/world/<sim_id>', methods=['GET'])
|
||||
def get_world(sim_id):
|
||||
"""Return the full world_state.json for this simulation."""
|
||||
store = WorldStateStore(_sim_dir(sim_id))
|
||||
return jsonify(store.load())
|
||||
|
||||
|
||||
@narrative_bp.route('/world/<sim_id>/rules', methods=['POST'])
|
||||
def set_rules(sim_id):
|
||||
"""Replace the full rules list. Body: {"rules": [str, ...]}."""
|
||||
data = request.get_json() or {}
|
||||
rules = data.get('rules')
|
||||
if not isinstance(rules, list):
|
||||
return jsonify({"error": "rules must be a list of strings"}), 400
|
||||
store = WorldStateStore(_sim_dir(sim_id))
|
||||
store.set_rules([str(r) for r in rules])
|
||||
return jsonify(store.load())
|
||||
|
||||
|
||||
@narrative_bp.route('/world/<sim_id>/locations', methods=['POST'])
|
||||
def upsert_location(sim_id):
|
||||
"""Insert or update a location. Body: {"id", "name", "description"}."""
|
||||
data = request.get_json() or {}
|
||||
if not data.get('id') or not data.get('name'):
|
||||
return jsonify({"error": "id and name are required"}), 400
|
||||
store = WorldStateStore(_sim_dir(sim_id))
|
||||
loc = store.upsert_location({
|
||||
"id": str(data['id']),
|
||||
"name": str(data['name']),
|
||||
"description": str(data.get('description', '')),
|
||||
})
|
||||
return jsonify(loc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# God Mode endpoints
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@narrative_bp.route('/godmode/<sim_id>/inject-event', methods=['POST'])
|
||||
def godmode_inject_event(sim_id):
|
||||
"""Inject a world event. Body: {"description", "round"? (optional, >=0)}."""
|
||||
data = request.get_json() or {}
|
||||
description = data.get('description')
|
||||
if not description:
|
||||
return jsonify({"error": "description is required"}), 400
|
||||
|
||||
round_num = data.get('round')
|
||||
if round_num is not None:
|
||||
try:
|
||||
round_num = int(round_num)
|
||||
if round_num < 0:
|
||||
raise ValueError()
|
||||
except (TypeError, ValueError):
|
||||
return jsonify({"error": "round must be a non-negative integer"}), 400
|
||||
|
||||
evt = gm_inject_event(_sim_dir(sim_id), description=str(description), round_num=round_num)
|
||||
return jsonify(evt)
|
||||
|
||||
|
||||
@narrative_bp.route('/godmode/<sim_id>/modify-emotion', methods=['POST'])
|
||||
def godmode_modify_emotion(sim_id):
|
||||
"""Overwrite emotion values. Body: {"character_id", "emotions": {name: float}}."""
|
||||
data = request.get_json() or {}
|
||||
char_id = data.get('character_id')
|
||||
emotions = data.get('emotions')
|
||||
if not char_id or not isinstance(emotions, dict):
|
||||
return jsonify({"error": "character_id and emotions are required"}), 400
|
||||
try:
|
||||
char = gm_modify_emotion(_sim_dir(sim_id), str(char_id), emotions)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 404
|
||||
return jsonify(char)
|
||||
|
||||
|
||||
@narrative_bp.route('/godmode/<sim_id>/kill', methods=['POST'])
|
||||
def godmode_kill(sim_id):
|
||||
"""Mark a character as dead. Body: {"character_id"}."""
|
||||
data = request.get_json() or {}
|
||||
char_id = data.get('character_id')
|
||||
if not char_id:
|
||||
return jsonify({"error": "character_id is required"}), 400
|
||||
try:
|
||||
char = gm_kill_character(_sim_dir(sim_id), str(char_id))
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 404
|
||||
return jsonify(char)
|
||||
|
|
|
|||
Loading…
Reference in New Issue