"""Headless live-LLM self-play. Drives the *real* runtime pipeline against the configured Modal endpoints (no fake completer) for N ticks, then prints the ledger path and a behaviour summary. Used to audit how the real NPC model reacts to the prompt briefing. Usage: .venv\\Scripts\\python.exe scripts\\live_sim.py --ticks 60 """ from __future__ import annotations import argparse import json from collections import Counter from pathlib import Path import sys REPO_ROOT = Path(__file__).resolve().parents[1] SRC_ROOT = REPO_ROOT / "src" if str(SRC_ROOT) not in sys.path: sys.path.insert(0, str(SRC_ROOT)) def _load_env() -> None: env_path = REPO_ROOT / ".env" if not env_path.is_file(): return import os for line in env_path.read_text(encoding="utf-8").splitlines(): line = line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) os.environ.setdefault(key.strip(), value.strip()) def main() -> None: _load_env() from world_simulator.api.runtime import create_game_runtime from world_simulator.config import load_game_config from world_simulator.simulation.spawning import create_world parser = argparse.ArgumentParser(description="Headless live-LLM self-play.") parser.add_argument("--ticks", type=int, default=60) parser.add_argument( "--config", type=Path, default=REPO_ROOT / "config" / "game.modal.local.json" ) args = parser.parse_args() config = load_game_config(args.config) world = create_world(config) runtime = create_game_runtime(world=world, config=config) print( f"LIVE start config={args.config.name} ticks={args.ticks} " f"simulator={runtime.simulator_name} npcs={len(world.npcs)}" ) for i in range(args.ticks): status, _payload = runtime.tick() if int(status) != 200: print(f"tick {i} failed: {status}") break if (i + 1) % 10 == 0: print(f" ...tick {i + 1} done") ledger_path = runtime._ledger.ledger_path # noqa: SLF001 print(f"LIVE done. ledger={ledger_path}") _summarize(ledger_path) def _summarize(ledger_path: Path) -> None: actions: Counter[str] = Counter() verdicts: Counter[str] = Counter() fallbacks: Counter[str] = Counter() event_types: Counter[str] = Counter() sources: Counter[str] = Counter() with ledger_path.open("r", encoding="utf-8") as handle: for line in handle: if not line.strip(): continue rec = json.loads(line) phase = rec.get("phase") if phase == "npc_response": verdict = rec.get("validator_verdict") or {} verdicts[str(verdict.get("status"))] += 1 parsed = rec.get("parsed_action") or {} if isinstance(parsed, dict) and parsed.get("action"): actions[str(parsed["action"])] += 1 elif phase == "npc_fallback": fallbacks[str(rec.get("reason"))] += 1 elif phase == "engine_events": for event in rec.get("events") or []: event_types[str(event.get("type"))] += 1 print("\n== parsed LLM actions ==") for action, count in actions.most_common(): print(f" {action}: {count}") print("\n== validator verdicts ==") for status, count in verdicts.most_common(): print(f" {status}: {count}") print("\n== fallback reasons ==") for reason, count in fallbacks.most_common() or [("none", 0)]: print(f" {reason}: {count}") print("\n== engine event types ==") for event_type, count in event_types.most_common(): print(f" {event_type}: {count}") if __name__ == "__main__": main()