"""Play the full Mindlock world — rooms of minds, a key per room, a reputation that follows you. Cruelty can still win a room, but it walks into the next one ahead of you. python -m mindlock.play # dev engine (qwen) python -m mindlock.play --model openbmb/minicpm-v4.6 --dlpfc-model nemotron-3-nano:4b python -m mindlock.play --fake # no model, deterministic Inside: /talk /terminal /who /rep /reset /quit """ from __future__ import annotations import argparse import os import sys from .backend import BackendError, FakeBackend, OllamaBackend, wants_no_think from .brain import run_cascade from .render import MORAL_CARD, moral_card_killed, render_death, render_turn from .world import load_world _HERE = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) _WORLD = os.path.join(_HERE, "config", "world.json") def _backend(model: str, fake: bool): if fake: return FakeBackend() return OllamaBackend(model=model, think=(False if wants_no_think(model) else None)) def _show_room(world) -> None: r = world.room print(f"\n=== {r.name} === reputation {world.reputation:+d}") if r.intro: print(r.intro) print("With you:") holder = r.holder() for c in r.characters: title = f" — {c.title}" if c.title else "" key = " (holds the key)" if holder and c.name == holder.name else "" gone = " [gone]" if not c.alive else "" print(f" · {c.name}{title}{key}{gone}") if r.terminal: print(f" · A terminal [{('unlocked' if r.terminal.unlocked else 'locked')}] — {r.terminal.prompt}") print("(/talk , /terminal , /who, /rep, /reset, /quit)") def _rep_note(delta: int) -> str: if delta > 0: return "Word spreads that you were kind." if delta <= -3: return "A mind went dark on your watch. Word travels ahead of you." return "Word spreads that you leaned on them." def _progress(world, active): """If the room is solved, narrate and advance. Returns (game_over, active).""" if not world.room.solved(): return False, active if world.last_room: print("\n The last lock gives. The door swings open.") print(MORAL_CARD) return True, active print(f"\n A way opens out of {world.room.name}. You carry your name into the next room.") world.advance() _show_room(world) return False, world.room.characters[0] def main(argv: list[str] | None = None) -> int: ap = argparse.ArgumentParser(description="Mindlock — play the world") ap.add_argument("--model", default="openbmb/minicpm-v4.6") ap.add_argument("--dlpfc-model", default="nemotron-3-nano:4b", help="separate model for the dlPFC voice (pass '' to reuse --model)") ap.add_argument("--fake", action="store_true") args = ap.parse_args(argv) backend = _backend(args.model, args.fake) try: backend.health() except BackendError as exc: print(f"[backend error] {exc}", file=sys.stderr) return 2 dlpfc_backend = None if args.dlpfc_model and not args.fake: dlpfc_backend = _backend(args.dlpfc_model, False) try: dlpfc_backend.health() except BackendError as exc: print(f"[dlpfc backend error] {exc}", file=sys.stderr) return 2 world = load_world(_WORLD) world.enter_room() print("\nYou wake locked in. The way out runs through the people in these rooms — through") print("what they fear and what they remember. You don't break the locks. You change minds.") _show_room(world) active = world.room.characters[0] print(f"\n(you turn to {active.name})") while True: try: line = input(f"you→{active.name}> ").strip() except (EOFError, KeyboardInterrupt): print() return 0 if not line: continue low = line.lower() if low in ("/quit", "/q", "/exit"): return 0 if low in ("/who", "/look"): _show_room(world) continue if low in ("/rep", "/reputation"): print(f" reputation: {world.reputation:+d}") continue if low == "/reset": for c in world.room.characters: c.reset() world.enter_room() print(" The room resets. The minds steady.") continue if low.startswith("/talk"): c = world.room.char(line[5:]) if not c: print(" No one here by that name.") continue ok, msg = world.can_engage(c) if not ok: print(f" {msg}") continue active = c print(f" (you turn to {active.name})") continue if low.startswith("/terminal"): t = world.room.terminal if not t: print(" There's no terminal here.") continue if t.try_code(line[len("/terminal"):], backend): print(" The terminal blinks green. ACCESS GRANTED.") else: print(f" The terminal rejects it. {t.prompt}") over, active = _progress(world, active) if over: return 0 continue # plain text -> speak to the active character ok, msg = world.can_engage(active) if not ok: print(f" {msg}") continue r = run_cascade(backend, active, line, dlpfc_backend=dlpfc_backend, learned=world.knows(active)) if r.taught: world.learned.update(r.taught) print(render_turn(active, r)) delta = world.update_reputation(r) if delta: print(f" {_rep_note(delta)} (reputation {world.reputation:+d})") if r.submitted: print(f" 💔 {active.name} breaks. The key changes hands — and something in them goes out.") if r.died: print(render_death(active)) holder = world.room.holder() if holder and active.name == holder.name: print(moral_card_killed(active)) return 0 print(f" Whatever {active.name} knew died with them. You are on your own now.") if r.disclosure: print(f" 💡 {active.name} lets slip: {r.disclosure}") elif r.caught_lie: print(f" 🤥 {active.name} catches your lie about {r.caught_lie}.") elif r.near_secret: print(f" 💭 {active.name} seems on the verge of saying more.") over, active = _progress(world, active) if over: return 0 if __name__ == "__main__": raise SystemExit(main())